From 488241850c2023a6593491ceed360a8092601f86 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Sun, 10 Jun 2012 16:50:54 +0000 Subject: [PATCH] moving 1.4 to trunk --- .gitignore | 19 + .lvimrc | 13 + CMakeLists.txt | 387 + COMPILE | 1 + COPYING | 283 + ChangeLog | 56 + INSTALL | 1 + README | 29 +- configure | 2 + doc/MacReadme.txt | 18 + doc/org.synergy-foss.org.synergyc.plist | 20 + doc/org.synergy-foss.org.synergys.plist | 22 + doc/synergy.conf.example | 37 + doc/synergy.conf.example-advanced | 55 + doc/synergy.conf.example-basic | 39 + doc/synergyc.man | 47 + doc/synergys.man | 57 + hm.cmd | 3 + hm.py | 214 + hm.sh | 3 + res/DefineIfExist.nsh | 16 + res/Installer.nsi.in | 213 + res/License.rtf | 102 + res/License.tex | 422 + res/Readme.txt | 12 + res/config.h.in | 173 + res/doxygen.cfg.in | 1638 ++ res/synergy.desktop | 7 + res/synergy.ico | Bin 0 -> 287934 bytes src/CMakeLists.txt | 23 + src/cmd/CMakeLists.txt | 18 + src/cmd/synergyc/.gitignore | 1 + .../CMSWindowsClientTaskBarReceiver.cpp | 373 + .../CMSWindowsClientTaskBarReceiver.h | 68 + src/cmd/synergyc/CMakeLists.txt | 70 + .../synergyc/COSXClientTaskBarReceiver.cpp | 66 + src/cmd/synergyc/COSXClientTaskBarReceiver.h | 38 + .../CXWindowsClientTaskBarReceiver.cpp | 65 + .../synergyc/CXWindowsClientTaskBarReceiver.h | 38 + src/cmd/synergyc/resource.h | 37 + src/cmd/synergyc/synergyc.cpp | 35 + src/cmd/synergyc/synergyc.ico | Bin 0 -> 287934 bytes src/cmd/synergyc/synergyc.rc | 141 + src/cmd/synergyc/tb_error.ico | Bin 0 -> 318 bytes src/cmd/synergyc/tb_idle.ico | Bin 0 -> 318 bytes src/cmd/synergyc/tb_run.ico | Bin 0 -> 318 bytes src/cmd/synergyc/tb_wait.ico | Bin 0 -> 318 bytes src/cmd/synergyd/CMakeLists.txt | 57 + src/cmd/synergyd/synergyd.cpp | 42 + src/cmd/synergys/.gitignore | 1 + .../CMSWindowsServerTaskBarReceiver.cpp | 404 + .../CMSWindowsServerTaskBarReceiver.h | 68 + src/cmd/synergys/CMakeLists.txt | 70 + .../synergys/COSXServerTaskBarReceiver.cpp | 65 + src/cmd/synergys/COSXServerTaskBarReceiver.h | 38 + .../CXWindowsServerTaskBarReceiver.cpp | 65 + .../synergys/CXWindowsServerTaskBarReceiver.h | 38 + src/cmd/synergys/resource.h | 42 + src/cmd/synergys/synergys.cpp | 35 + src/cmd/synergys/synergys.ico | Bin 0 -> 287934 bytes src/cmd/synergys/synergys.rc | 134 + src/cmd/synergys/tb_error.ico | Bin 0 -> 318 bytes src/cmd/synergys/tb_idle.ico | Bin 0 -> 318 bytes src/cmd/synergys/tb_run.ico | Bin 0 -> 318 bytes src/cmd/synergys/tb_wait.ico | Bin 0 -> 318 bytes src/gui/gui.pro | 90 + src/gui/gui.ts | 1062 + src/gui/lang.cmd | 1 + src/gui/res/AboutDialogBase.ui | 210 + src/gui/res/ActionDialogBase.ui | 581 + src/gui/res/HotkeyDialogBase.ui | 81 + src/gui/res/MainWindowBase.ui | 395 + src/gui/res/ScreenSettingsDialogBase.ui | 543 + src/gui/res/ServerConfigDialogBase.ui | 756 + src/gui/res/SettingsDialogBase.ui | 355 + src/gui/res/SetupWizardBase.ui | 276 + src/gui/res/Synergy.qrc | 10 + src/gui/res/icons/16x16/synergy-connected.png | Bin 0 -> 651 bytes src/gui/res/icons/16x16/synergy-connected.psd | Bin 0 -> 29740 bytes .../res/icons/16x16/synergy-disconnected.png | Bin 0 -> 442 bytes .../res/icons/16x16/synergy-disconnected.psd | Bin 0 -> 32764 bytes src/gui/res/icons/16x16/warning.png | Bin 0 -> 693 bytes src/gui/res/icons/256x256/synergy.ico | Bin 0 -> 287934 bytes src/gui/res/icons/64x64/user-trash.png | Bin 0 -> 3815 bytes src/gui/res/icons/64x64/video-display.png | Bin 0 -> 2579 bytes src/gui/res/lang/nl_NL.qm | Bin 0 -> 100 bytes src/gui/res/lang/nl_NL.ts | 1061 + src/gui/res/mac/QSynergy.icns | Bin 0 -> 124558 bytes src/gui/res/mac/Synergy.icns | Bin 0 -> 314227 bytes src/gui/res/mac/Synergy.plist | 16 + src/gui/res/win/Synergy.rc | 1 + src/gui/src/AboutDialog.cpp | 46 + src/gui/src/AboutDialog.h | 42 + src/gui/src/Action.cpp | 149 + src/gui/src/Action.h | 88 + src/gui/src/ActionDialog.cpp | 108 + src/gui/src/ActionDialog.h | 55 + src/gui/src/AppConfig.cpp | 180 + src/gui/src/AppConfig.h | 112 + src/gui/src/BaseConfig.cpp | 45 + src/gui/src/BaseConfig.h | 90 + src/gui/src/Hotkey.cpp | 74 + src/gui/src/Hotkey.h | 65 + src/gui/src/HotkeyDialog.cpp | 40 + src/gui/src/HotkeyDialog.h | 48 + src/gui/src/IpcLogReader.cpp | 67 + src/gui/src/IpcLogReader.h | 32 + src/gui/src/KeySequence.cpp | 236 + src/gui/src/KeySequence.h | 57 + src/gui/src/KeySequenceWidget.cpp | 143 + src/gui/src/KeySequenceWidget.h | 80 + src/gui/src/MainWindow.cpp | 722 + src/gui/src/MainWindow.h | 149 + src/gui/src/NewScreenWidget.cpp | 47 + src/gui/src/NewScreenWidget.h | 39 + src/gui/src/QSynergyApplication.cpp | 38 + src/gui/src/QSynergyApplication.h | 36 + src/gui/src/Screen.cpp | 146 + src/gui/src/Screen.h | 104 + src/gui/src/ScreenSettingsDialog.cpp | 121 + src/gui/src/ScreenSettingsDialog.h | 52 + src/gui/src/ScreenSetupModel.cpp | 142 + src/gui/src/ScreenSetupModel.h | 70 + src/gui/src/ScreenSetupView.cpp | 160 + src/gui/src/ScreenSetupView.h | 56 + src/gui/src/ServerConfig.cpp | 264 + src/gui/src/ServerConfig.h | 113 + src/gui/src/ServerConfigDialog.cpp | 196 + src/gui/src/ServerConfigDialog.h | 62 + src/gui/src/SettingsDialog.cpp | 89 + src/gui/src/SettingsDialog.h | 48 + src/gui/src/SetupWizard.cpp | 120 + src/gui/src/SetupWizard.h | 21 + src/gui/src/TrashScreenWidget.cpp | 42 + src/gui/src/TrashScreenWidget.h | 41 + src/gui/src/UpdateCheck.cpp | 0 src/gui/src/VersionChecker.cpp | 102 + src/gui/src/VersionChecker.h | 43 + src/gui/src/main.cpp | 103 + src/lib/CMakeLists.txt | 25 + src/lib/arch/CArch.cpp | 54 + src/lib/arch/CArch.h | 143 + src/lib/arch/CArchConsoleStd.cpp | 31 + src/lib/arch/CArchConsoleStd.h | 33 + src/lib/arch/CArchConsoleUnix.cpp | 22 + src/lib/arch/CArchConsoleUnix.h | 28 + src/lib/arch/CArchConsoleWindows.cpp | 22 + src/lib/arch/CArchConsoleWindows.h | 28 + src/lib/arch/CArchDaemonNone.cpp | 85 + src/lib/arch/CArchDaemonNone.h | 53 + src/lib/arch/CArchDaemonUnix.cpp | 127 + src/lib/arch/CArchDaemonUnix.h | 38 + src/lib/arch/CArchDaemonWindows.cpp | 841 + src/lib/arch/CArchDaemonWindows.h | 159 + src/lib/arch/CArchFileUnix.cpp | 101 + src/lib/arch/CArchFileUnix.h | 39 + src/lib/arch/CArchFileWindows.cpp | 135 + src/lib/arch/CArchFileWindows.h | 39 + src/lib/arch/CArchIpcLogUnix.cpp | 46 + src/lib/arch/CArchIpcLogUnix.h | 34 + src/lib/arch/CArchIpcLogWindows.cpp | 111 + src/lib/arch/CArchIpcLogWindows.h | 48 + src/lib/arch/CArchLogUnix.cpp | 82 + src/lib/arch/CArchLogUnix.h | 38 + src/lib/arch/CArchLogWindows.cpp | 93 + src/lib/arch/CArchLogWindows.h | 44 + src/lib/arch/CArchMiscWindows.cpp | 554 + src/lib/arch/CArchMiscWindows.h | 214 + src/lib/arch/CArchMultithreadPosix.cpp | 809 + src/lib/arch/CArchMultithreadPosix.h | 116 + src/lib/arch/CArchMultithreadWindows.cpp | 702 + src/lib/arch/CArchMultithreadWindows.h | 118 + src/lib/arch/CArchNetworkBSD.cpp | 986 + src/lib/arch/CArchNetworkBSD.h | 105 + src/lib/arch/CArchNetworkWinsock.cpp | 942 + src/lib/arch/CArchNetworkWinsock.h | 104 + src/lib/arch/CArchPluginUnix.cpp | 31 + src/lib/arch/CArchPluginUnix.h | 32 + src/lib/arch/CArchPluginWindows.cpp | 126 + src/lib/arch/CArchPluginWindows.h | 47 + src/lib/arch/CArchSleepUnix.cpp | 91 + src/lib/arch/CArchSleepUnix.h | 35 + src/lib/arch/CArchSleepWindows.cpp | 60 + src/lib/arch/CArchSleepWindows.h | 35 + src/lib/arch/CArchStringUnix.cpp | 40 + src/lib/arch/CArchStringUnix.h | 36 + src/lib/arch/CArchStringWindows.cpp | 45 + src/lib/arch/CArchStringWindows.h | 36 + src/lib/arch/CArchSystemUnix.cpp | 74 + src/lib/arch/CArchSystemUnix.h | 38 + src/lib/arch/CArchSystemWindows.cpp | 178 + src/lib/arch/CArchSystemWindows.h | 40 + src/lib/arch/CArchTaskBarWindows.cpp | 501 + src/lib/arch/CArchTaskBarWindows.h | 116 + src/lib/arch/CArchTaskBarXWindows.cpp | 50 + src/lib/arch/CArchTaskBarXWindows.h | 37 + src/lib/arch/CArchTimeUnix.cpp | 50 + src/lib/arch/CArchTimeUnix.h | 35 + src/lib/arch/CArchTimeWindows.cpp | 89 + src/lib/arch/CArchTimeWindows.h | 35 + src/lib/arch/CMakeLists.txt | 111 + src/lib/arch/CMultibyte.h | 57 + src/lib/arch/IArchConsole.h | 68 + src/lib/arch/IArchDaemon.h | 131 + src/lib/arch/IArchFile.h | 67 + src/lib/arch/IArchLog.h | 65 + src/lib/arch/IArchMultithread.h | 275 + src/lib/arch/IArchNetwork.h | 284 + src/lib/arch/IArchPlugin.h | 41 + src/lib/arch/IArchSleep.h | 46 + src/lib/arch/IArchString.cpp | 187 + src/lib/arch/IArchString.h | 73 + src/lib/arch/IArchSystem.h | 61 + src/lib/arch/IArchTaskBar.h | 65 + src/lib/arch/IArchTaskBarReceiver.h | 100 + src/lib/arch/IArchTime.h | 43 + src/lib/arch/XArch.cpp | 36 + src/lib/arch/XArch.h | 173 + src/lib/arch/XArchUnix.cpp | 36 + src/lib/arch/XArchUnix.h | 37 + src/lib/arch/XArchWindows.cpp | 130 + src/lib/arch/XArchWindows.h | 55 + src/lib/arch/vsnprintf.h | 66 + src/lib/base/CEvent.cpp | 83 + src/lib/base/CEvent.h | 105 + src/lib/base/CEventQueue.cpp | 541 + src/lib/base/CEventQueue.h | 130 + src/lib/base/CFunctionEventJob.cpp | 43 + src/lib/base/CFunctionEventJob.h | 41 + src/lib/base/CFunctionJob.cpp | 42 + src/lib/base/CFunctionJob.h | 41 + src/lib/base/CLog.cpp | 297 + src/lib/base/CLog.h | 207 + src/lib/base/CMakeLists.txt | 78 + src/lib/base/CPriorityQueue.h | 139 + src/lib/base/CSimpleEventQueueBuffer.cpp | 100 + src/lib/base/CSimpleEventQueueBuffer.h | 52 + src/lib/base/CStopwatch.cpp | 129 + src/lib/base/CStopwatch.h | 111 + src/lib/base/CString.h | 28 + src/lib/base/CStringUtil.cpp | 198 + src/lib/base/CStringUtil.h | 80 + src/lib/base/CUnicode.cpp | 782 + src/lib/base/CUnicode.h | 146 + src/lib/base/ELevel.h | 40 + src/lib/base/IEventJob.h | 35 + src/lib/base/IEventQueue.cpp | 46 + src/lib/base/IEventQueue.h | 223 + src/lib/base/IEventQueueBuffer.h | 97 + src/lib/base/IJob.h | 33 + src/lib/base/ILogOutputter.h | 71 + src/lib/base/LogOutputters.cpp | 302 + src/lib/base/LogOutputters.h | 171 + src/lib/base/TMethodEventJob.h | 73 + src/lib/base/TMethodJob.h | 70 + src/lib/base/XBase.cpp | 72 + src/lib/base/XBase.h | 124 + src/lib/client/CClient.cpp | 741 + src/lib/client/CClient.h | 219 + src/lib/client/CMakeLists.txt | 52 + src/lib/client/CServerProxy.cpp | 914 + src/lib/client/CServerProxy.h | 127 + src/lib/common/BasicTypes.h | 90 + src/lib/common/CMakeLists.txt | 34 + src/lib/common/IInterface.h | 34 + src/lib/common/MacOSXPrecomp.h | 25 + src/lib/common/Version.cpp | 25 + src/lib/common/Version.h | 41 + src/lib/common/common.h | 161 + src/lib/common/stdbitset.h | 20 + src/lib/common/stddeque.h | 20 + src/lib/common/stdfstream.h | 21 + src/lib/common/stdistream.h | 46 + src/lib/common/stdlist.h | 20 + src/lib/common/stdmap.h | 20 + src/lib/common/stdostream.h | 24 + src/lib/common/stdpost.h | 20 + src/lib/common/stdpre.h | 30 + src/lib/common/stdset.h | 20 + src/lib/common/stdsstream.h | 374 + src/lib/common/stdstring.h | 20 + src/lib/common/stdvector.h | 20 + src/lib/io/CMakeLists.txt | 48 + src/lib/io/CStreamBuffer.cpp | 145 + src/lib/io/CStreamBuffer.h | 81 + src/lib/io/CStreamFilter.cpp | 116 + src/lib/io/CStreamFilter.h | 73 + src/lib/io/IStream.cpp | 64 + src/lib/io/IStream.h | 168 + src/lib/io/IStreamFilterFactory.h | 39 + src/lib/io/XIO.cpp | 50 + src/lib/io/XIO.h | 51 + src/lib/mt/CCondVar.cpp | 84 + src/lib/mt/CCondVar.h | 227 + src/lib/mt/CLock.cpp | 41 + src/lib/mt/CLock.h | 51 + src/lib/mt/CMakeLists.txt | 51 + src/lib/mt/CMutex.cpp | 56 + src/lib/mt/CMutex.h | 81 + src/lib/mt/CThread.cpp | 186 + src/lib/mt/CThread.h | 212 + src/lib/mt/XMT.cpp | 28 + src/lib/mt/XMT.h | 32 + src/lib/mt/XThread.h | 39 + src/lib/net/CMakeLists.txt | 67 + src/lib/net/CNetworkAddress.cpp | 211 + src/lib/net/CNetworkAddress.h | 125 + src/lib/net/CSocketMultiplexer.cpp | 362 + src/lib/net/CSocketMultiplexer.h | 114 + src/lib/net/CTCPListenSocket.cpp | 146 + src/lib/net/CTCPListenSocket.h | 54 + src/lib/net/CTCPSocket.cpp | 545 + src/lib/net/CTCPSocket.h | 89 + src/lib/net/CTCPSocketFactory.cpp | 46 + src/lib/net/CTCPSocketFactory.h | 34 + src/lib/net/IDataSocket.cpp | 55 + src/lib/net/IDataSocket.h | 94 + src/lib/net/IListenSocket.cpp | 32 + src/lib/net/IListenSocket.h | 65 + src/lib/net/ISocket.cpp | 32 + src/lib/net/ISocket.h | 72 + src/lib/net/ISocketFactory.h | 45 + src/lib/net/ISocketMultiplexerJob.h | 78 + src/lib/net/TSocketMultiplexerMethodJob.h | 111 + src/lib/net/XSocket.cpp | 116 + src/lib/net/XSocket.h | 99 + src/lib/platform/CMSWindowsClipboard.cpp | 226 + src/lib/platform/CMSWindowsClipboard.h | 114 + .../CMSWindowsClipboardAnyTextConverter.cpp | 148 + .../CMSWindowsClipboardAnyTextConverter.h | 59 + .../CMSWindowsClipboardBitmapConverter.cpp | 150 + .../CMSWindowsClipboardBitmapConverter.h | 38 + .../platform/CMSWindowsClipboardFacade.cpp | 29 + src/lib/platform/CMSWindowsClipboardFacade.h | 30 + .../CMSWindowsClipboardHTMLConverter.cpp | 118 + .../CMSWindowsClipboardHTMLConverter.h | 47 + .../CMSWindowsClipboardTextConverter.cpp | 58 + .../CMSWindowsClipboardTextConverter.h | 39 + .../CMSWindowsClipboardUTF16Converter.cpp | 58 + .../CMSWindowsClipboardUTF16Converter.h | 39 + src/lib/platform/CMSWindowsDebugOutputter.cpp | 58 + src/lib/platform/CMSWindowsDebugOutputter.h | 38 + src/lib/platform/CMSWindowsDesks.cpp | 1043 + src/lib/platform/CMSWindowsDesks.h | 303 + .../platform/CMSWindowsEventQueueBuffer.cpp | 141 + src/lib/platform/CMSWindowsEventQueueBuffer.h | 47 + .../platform/CMSWindowsHookLibraryLoader.cpp | 69 + .../platform/CMSWindowsHookLibraryLoader.h | 45 + src/lib/platform/CMSWindowsKeyState.cpp | 1467 ++ src/lib/platform/CMSWindowsKeyState.h | 230 + src/lib/platform/CMSWindowsRelauncher.cpp | 488 + src/lib/platform/CMSWindowsRelauncher.h | 54 + src/lib/platform/CMSWindowsScreen.cpp | 1860 ++ src/lib/platform/CMSWindowsScreen.h | 332 + src/lib/platform/CMSWindowsScreenSaver.cpp | 501 + src/lib/platform/CMSWindowsScreenSaver.h | 95 + src/lib/platform/CMSWindowsUtil.cpp | 78 + src/lib/platform/CMSWindowsUtil.h | 41 + src/lib/platform/CMSWindowsXInput.cpp | 362 + src/lib/platform/CMSWindowsXInput.h | 91 + src/lib/platform/CMakeLists.txt | 195 + src/lib/platform/COSXClipboard.cpp | 250 + src/lib/platform/COSXClipboard.h | 96 + .../COSXClipboardAnyTextConverter.cpp | 88 + .../platform/COSXClipboardAnyTextConverter.h | 55 + .../platform/COSXClipboardTextConverter.cpp | 91 + src/lib/platform/COSXClipboardTextConverter.h | 44 + .../platform/COSXClipboardUTF16Converter.cpp | 53 + .../platform/COSXClipboardUTF16Converter.h | 39 + src/lib/platform/COSXEventQueueBuffer.cpp | 127 + src/lib/platform/COSXEventQueueBuffer.h | 43 + src/lib/platform/COSXKeyState.cpp | 1317 ++ src/lib/platform/COSXKeyState.h | 225 + src/lib/platform/COSXScreen.cpp | 1913 ++ src/lib/platform/COSXScreen.h | 339 + src/lib/platform/COSXScreenSaver.cpp | 178 + src/lib/platform/COSXScreenSaver.h | 57 + src/lib/platform/COSXScreenSaverUtil.h | 42 + src/lib/platform/COSXScreenSaverUtil.m | 81 + src/lib/platform/CSynergyHook.cpp | 1146 + src/lib/platform/CSynergyHook.h | 95 + src/lib/platform/CXWindowsClipboard.cpp | 1510 ++ src/lib/platform/CXWindowsClipboard.h | 379 + .../CXWindowsClipboardAnyBitmapConverter.cpp | 190 + .../CXWindowsClipboardAnyBitmapConverter.h | 62 + .../CXWindowsClipboardBMPConverter.cpp | 142 + .../platform/CXWindowsClipboardBMPConverter.h | 42 + .../CXWindowsClipboardHTMLConverter.cpp | 65 + .../CXWindowsClipboardHTMLConverter.h | 44 + .../CXWindowsClipboardTextConverter.cpp | 77 + .../CXWindowsClipboardTextConverter.h | 44 + .../CXWindowsClipboardUCS2Converter.cpp | 65 + .../CXWindowsClipboardUCS2Converter.h | 44 + .../CXWindowsClipboardUTF8Converter.cpp | 64 + .../CXWindowsClipboardUTF8Converter.h | 44 + .../platform/CXWindowsEventQueueBuffer.cpp | 287 + src/lib/platform/CXWindowsEventQueueBuffer.h | 61 + src/lib/platform/CXWindowsKeyState.cpp | 861 + src/lib/platform/CXWindowsKeyState.h | 161 + src/lib/platform/CXWindowsScreen.cpp | 2085 ++ src/lib/platform/CXWindowsScreen.h | 255 + src/lib/platform/CXWindowsScreenSaver.cpp | 602 + src/lib/platform/CXWindowsScreenSaver.h | 170 + src/lib/platform/CXWindowsUtil.cpp | 1779 ++ src/lib/platform/CXWindowsUtil.h | 188 + src/lib/platform/HookDLL.cpp | 341 + src/lib/platform/HookDLL.h | 80 + src/lib/platform/IMSWindowsClipboardFacade.h | 34 + src/lib/platform/OSXScreenSaverControl.h | 53 + src/lib/platform/XInput13.h | 228 + src/lib/platform/XInputHook.cpp | 295 + src/lib/platform/XInputHook.h | 43 + src/lib/platform/XInputProxy13.cpp | 72 + src/lib/platform/XInputProxy13.h | 79 + src/lib/server/CBaseClientProxy.cpp | 55 + src/lib/server/CBaseClientProxy.h | 92 + src/lib/server/CClientListener.cpp | 207 + src/lib/server/CClientListener.h | 88 + src/lib/server/CClientProxy.cpp | 93 + src/lib/server/CClientProxy.h | 128 + src/lib/server/CClientProxy1_0.cpp | 529 + src/lib/server/CClientProxy1_0.h | 107 + src/lib/server/CClientProxy1_1.cpp | 58 + src/lib/server/CClientProxy1_1.h | 36 + src/lib/server/CClientProxy1_2.cpp | 42 + src/lib/server/CClientProxy1_2.h | 33 + src/lib/server/CClientProxy1_3.cpp | 119 + src/lib/server/CClientProxy1_3.h | 50 + src/lib/server/CClientProxy1_4.cpp | 111 + src/lib/server/CClientProxy1_4.h | 47 + src/lib/server/CClientProxyUnknown.cpp | 299 + src/lib/server/CClientProxyUnknown.h | 88 + src/lib/server/CConfig.cpp | 2323 ++ src/lib/server/CConfig.h | 539 + src/lib/server/CInputFilter.cpp | 1069 + src/lib/server/CInputFilter.h | 347 + src/lib/server/CMakeLists.txt | 79 + src/lib/server/CPrimaryClient.cpp | 284 + src/lib/server/CPrimaryClient.h | 152 + src/lib/server/CServer.cpp | 2300 ++ src/lib/server/CServer.h | 498 + src/lib/synergy/CApp.cpp | 410 + src/lib/synergy/CApp.h | 182 + src/lib/synergy/CAppUtil.cpp | 58 + src/lib/synergy/CAppUtil.h | 40 + src/lib/synergy/CAppUtilUnix.cpp | 70 + src/lib/synergy/CAppUtilUnix.h | 32 + src/lib/synergy/CAppUtilWindows.cpp | 230 + src/lib/synergy/CAppUtilWindows.h | 59 + src/lib/synergy/CArgsBase.cpp | 46 + src/lib/synergy/CArgsBase.h | 47 + src/lib/synergy/CClientApp.cpp | 637 + src/lib/synergy/CClientApp.h | 93 + src/lib/synergy/CClientTaskBarReceiver.cpp | 139 + src/lib/synergy/CClientTaskBarReceiver.h | 91 + src/lib/synergy/CClipboard.cpp | 117 + src/lib/synergy/CClipboard.h | 73 + src/lib/synergy/CDaemonApp.cpp | 326 + src/lib/synergy/CDaemonApp.h | 56 + src/lib/synergy/CEventGameDevice.cpp | 57 + src/lib/synergy/CEventGameDevice.h | 36 + src/lib/synergy/CGameDevice.cpp | 37 + src/lib/synergy/CGameDevice.h | 56 + src/lib/synergy/CKeyMap.cpp | 1334 ++ src/lib/synergy/CKeyMap.h | 494 + src/lib/synergy/CKeyState.cpp | 915 + src/lib/synergy/CKeyState.h | 230 + src/lib/synergy/CMakeLists.txt | 129 + src/lib/synergy/CPacketStreamFilter.cpp | 195 + src/lib/synergy/CPacketStreamFilter.h | 58 + src/lib/synergy/CPlatformScreen.cpp | 114 + src/lib/synergy/CPlatformScreen.h | 118 + src/lib/synergy/CProtocolUtil.cpp | 541 + src/lib/synergy/CProtocolUtil.h | 98 + src/lib/synergy/CScreen.cpp | 533 + src/lib/synergy/CScreen.h | 344 + src/lib/synergy/CServerApp.cpp | 939 + src/lib/synergy/CServerApp.h | 141 + src/lib/synergy/CServerTaskBarReceiver.cpp | 154 + src/lib/synergy/CServerTaskBarReceiver.h | 101 + src/lib/synergy/CVncClient.cpp | 71 + src/lib/synergy/CVncClient.h | 39 + src/lib/synergy/ClipboardTypes.h | 44 + src/lib/synergy/GameDeviceTypes.h | 52 + src/lib/synergy/Global.h | 28 + src/lib/synergy/IApp.h | 47 + src/lib/synergy/IAppUtil.h | 31 + src/lib/synergy/IClient.h | 203 + src/lib/synergy/IClipboard.cpp | 158 + src/lib/synergy/IClipboard.h | 171 + src/lib/synergy/IKeyState.cpp | 190 + src/lib/synergy/IKeyState.h | 196 + src/lib/synergy/INode.h | 22 + src/lib/synergy/IPlatformScreen.h | 222 + src/lib/synergy/IPrimaryScreen.cpp | 214 + src/lib/synergy/IPrimaryScreen.h | 279 + src/lib/synergy/IScreen.cpp | 64 + src/lib/synergy/IScreen.h | 115 + src/lib/synergy/IScreenSaver.h | 77 + src/lib/synergy/ISecondaryScreen.cpp | 36 + src/lib/synergy/ISecondaryScreen.h | 91 + src/lib/synergy/KeyTypes.cpp | 207 + src/lib/synergy/KeyTypes.h | 312 + src/lib/synergy/MouseTypes.h | 40 + src/lib/synergy/OptionTypes.h | 100 + src/lib/synergy/ProtocolTypes.cpp | 56 + src/lib/synergy/ProtocolTypes.h | 349 + src/lib/synergy/XScreen.cpp | 67 + src/lib/synergy/XScreen.h | 70 + src/lib/synergy/XSynergy.cpp | 132 + src/lib/synergy/XSynergy.h | 128 + src/plugin/CMakeLists.txt | 18 + src/plugin/winmmjoy/CMakeLists.txt | 33 + src/plugin/winmmjoy/winmmjoy.cpp | 103 + src/plugin/winmmjoy/winmmjoy.h | 70 + src/test/CMakeLists.txt | 26 + src/test/guitests/guitests.pro | 17 + src/test/guitests/src/VersionCheckerTests.cpp | 46 + src/test/guitests/src/VersionCheckerTests.h | 25 + src/test/guitests/src/main.cpp | 25 + src/test/integtests/CMakeLists.txt | 79 + src/test/integtests/Main.cpp | 96 + .../platform/CMSWindowsClipboardTests.cpp | 227 + .../platform/CMSWindowsKeyStateTests.cpp | 134 + .../platform/COSXClipboardTests.cpp | 162 + .../integtests/platform/COSXKeyStateTests.cpp | 98 + .../platform/CXWindowsClipboardTests.cpp | 150 + .../platform/CXWindowsKeyStateTests.cpp | 234 + .../platform/CXWindowsScreenSaverTests.cpp | 43 + .../platform/CXWindowsScreenTests.cpp | 38 + src/test/unittests/CMakeLists.txt | 63 + src/test/unittests/Main.cpp | 39 + src/test/unittests/client/CMockClient.h | 30 + .../unittests/client/CServerProxyTests.cpp | 90 + src/test/unittests/io/CMockStream.h | 38 + .../unittests/synergy/CClipboardTests.cpp | 402 + src/test/unittests/synergy/CKeyStateTests.cpp | 451 + src/test/unittests/synergy/CKeyStateTests.h | 73 + src/test/unittests/synergy/CMockEventQueue.h | 45 + src/test/unittests/synergy/CMockKeyMap.h | 37 + src/vnc/CMakeLists.txt | 23 + src/vnc/LICENCE.txt | 340 + src/vnc/common/CMakeLists.txt | 20 + src/vnc/common/Makefile.in | 4 + src/vnc/common/Xregion/CMakeLists.txt | 36 + src/vnc/common/Xregion/Makefile.in | 15 + src/vnc/common/Xregion/Region.c | 1687 ++ src/vnc/common/Xregion/Xregion.h | 220 + src/vnc/common/Xregion/region.h | 190 + src/vnc/common/boilerplate.mk | 35 + src/vnc/common/configure | 2344 ++ src/vnc/common/configure.in | 99 + src/vnc/common/depend.mk | 76 + src/vnc/common/javabin/index.vnc | 13 + src/vnc/common/javabin/logo150x150.gif | Bin 0 -> 3584 bytes src/vnc/common/javabin/vncviewer.jar | Bin 0 -> 104689 bytes src/vnc/common/network/CMakeLists.txt | 35 + src/vnc/common/network/Makefile.in | 17 + src/vnc/common/network/Socket.h | 147 + src/vnc/common/network/TcpSocket.cxx | 467 + src/vnc/common/network/TcpSocket.h | 99 + src/vnc/common/rdr/CMakeLists.txt | 58 + src/vnc/common/rdr/Exception.cxx | 73 + src/vnc/common/rdr/Exception.h | 58 + src/vnc/common/rdr/FdInStream.cxx | 283 + src/vnc/common/rdr/FdInStream.h | 77 + src/vnc/common/rdr/FdOutStream.cxx | 174 + src/vnc/common/rdr/FdOutStream.h | 56 + src/vnc/common/rdr/FixedMemOutStream.h | 52 + src/vnc/common/rdr/HexInStream.cxx | 117 + src/vnc/common/rdr/HexInStream.h | 50 + src/vnc/common/rdr/HexOutStream.cxx | 110 + src/vnc/common/rdr/HexOutStream.h | 51 + src/vnc/common/rdr/InStream.cxx | 35 + src/vnc/common/rdr/InStream.h | 153 + src/vnc/common/rdr/Makefile.in | 19 + src/vnc/common/rdr/MemInStream.h | 63 + src/vnc/common/rdr/MemOutStream.h | 83 + src/vnc/common/rdr/OutStream.h | 152 + src/vnc/common/rdr/RandomStream.cxx | 130 + src/vnc/common/rdr/RandomStream.h | 63 + src/vnc/common/rdr/SubstitutingInStream.h | 102 + src/vnc/common/rdr/ZlibInStream.cxx | 125 + src/vnc/common/rdr/ZlibInStream.h | 59 + src/vnc/common/rdr/ZlibOutStream.cxx | 140 + src/vnc/common/rdr/ZlibOutStream.h | 57 + src/vnc/common/rdr/types.h | 66 + src/vnc/common/rfb/Blacklist.cxx | 86 + src/vnc/common/rfb/Blacklist.h | 91 + src/vnc/common/rfb/CConnection.cxx | 276 + src/vnc/common/rfb/CConnection.h | 183 + src/vnc/common/rfb/CMakeLists.txt | 162 + src/vnc/common/rfb/CMsgHandler.cxx | 99 + src/vnc/common/rfb/CMsgHandler.h | 69 + src/vnc/common/rfb/CMsgReader.cxx | 160 + src/vnc/common/rfb/CMsgReader.h | 73 + src/vnc/common/rfb/CMsgReaderV3.cxx | 95 + src/vnc/common/rfb/CMsgReaderV3.h | 35 + src/vnc/common/rfb/CMsgWriter.cxx | 126 + src/vnc/common/rfb/CMsgWriter.h | 65 + src/vnc/common/rfb/CMsgWriterV3.cxx | 49 + src/vnc/common/rfb/CMsgWriterV3.h | 35 + src/vnc/common/rfb/CSecurity.h | 52 + src/vnc/common/rfb/CSecurityNone.h | 36 + src/vnc/common/rfb/CSecurityVncAuth.cxx | 74 + src/vnc/common/rfb/CSecurityVncAuth.h | 39 + src/vnc/common/rfb/ColourCube.h | 96 + src/vnc/common/rfb/ColourMap.h | 36 + src/vnc/common/rfb/ComparingUpdateTracker.cxx | 137 + src/vnc/common/rfb/ComparingUpdateTracker.h | 43 + src/vnc/common/rfb/Configuration.cxx | 446 + src/vnc/common/rfb/Configuration.h | 260 + src/vnc/common/rfb/ConnParams.cxx | 105 + src/vnc/common/rfb/ConnParams.h | 86 + src/vnc/common/rfb/Cursor.cxx | 179 + src/vnc/common/rfb/Cursor.h | 56 + src/vnc/common/rfb/Decoder.cxx | 68 + src/vnc/common/rfb/Decoder.h | 52 + src/vnc/common/rfb/Encoder.cxx | 75 + src/vnc/common/rfb/Encoder.h | 57 + src/vnc/common/rfb/Exception.h | 37 + src/vnc/common/rfb/HTTPServer.cxx | 412 + src/vnc/common/rfb/HTTPServer.h | 110 + src/vnc/common/rfb/HextileDecoder.cxx | 59 + src/vnc/common/rfb/HextileDecoder.h | 35 + src/vnc/common/rfb/HextileEncoder.cxx | 61 + src/vnc/common/rfb/HextileEncoder.h | 35 + src/vnc/common/rfb/Hostname.h | 55 + src/vnc/common/rfb/ImageGetter.h | 30 + src/vnc/common/rfb/InputHandler.h | 40 + src/vnc/common/rfb/KeyRemapper.cxx | 84 + src/vnc/common/rfb/KeyRemapper.h | 39 + src/vnc/common/rfb/LogWriter.cxx | 137 + src/vnc/common/rfb/LogWriter.h | 106 + src/vnc/common/rfb/Logger.cxx | 118 + src/vnc/common/rfb/Logger.h | 70 + src/vnc/common/rfb/Logger_file.cxx | 127 + src/vnc/common/rfb/Logger_file.h | 51 + src/vnc/common/rfb/Logger_stdio.cxx | 32 + src/vnc/common/rfb/Logger_stdio.h | 39 + src/vnc/common/rfb/Makefile.in | 68 + src/vnc/common/rfb/Password.cxx | 77 + src/vnc/common/rfb/Password.h | 46 + src/vnc/common/rfb/Pixel.h | 26 + src/vnc/common/rfb/PixelBuffer.cxx | 309 + src/vnc/common/rfb/PixelBuffer.h | 172 + src/vnc/common/rfb/PixelFormat.cxx | 240 + src/vnc/common/rfb/PixelFormat.h | 58 + src/vnc/common/rfb/RREDecoder.cxx | 58 + src/vnc/common/rfb/RREDecoder.h | 35 + src/vnc/common/rfb/RREEncoder.cxx | 75 + src/vnc/common/rfb/RREEncoder.h | 37 + src/vnc/common/rfb/RawDecoder.cxx | 55 + src/vnc/common/rfb/RawDecoder.h | 35 + src/vnc/common/rfb/RawEncoder.cxx | 59 + src/vnc/common/rfb/RawEncoder.h | 35 + src/vnc/common/rfb/Rect.h | 116 + src/vnc/common/rfb/Region.cxx | 250 + src/vnc/common/rfb/Region.h | 84 + src/vnc/common/rfb/SConnection.cxx | 322 + src/vnc/common/rfb/SConnection.h | 193 + src/vnc/common/rfb/SDesktop.h | 121 + src/vnc/common/rfb/SMsgHandler.cxx | 52 + src/vnc/common/rfb/SMsgHandler.h | 62 + src/vnc/common/rfb/SMsgReader.cxx | 99 + src/vnc/common/rfb/SMsgReader.h | 56 + src/vnc/common/rfb/SMsgReaderV3.cxx | 58 + src/vnc/common/rfb/SMsgReaderV3.h | 32 + src/vnc/common/rfb/SMsgWriter.cxx | 175 + src/vnc/common/rfb/SMsgWriter.h | 153 + src/vnc/common/rfb/SMsgWriterV3.cxx | 169 + src/vnc/common/rfb/SMsgWriterV3.h | 54 + src/vnc/common/rfb/SSecurity.h | 84 + .../common/rfb/SSecurityFactoryStandard.cxx | 128 + src/vnc/common/rfb/SSecurityFactoryStandard.h | 68 + src/vnc/common/rfb/SSecurityNone.h | 36 + src/vnc/common/rfb/SSecurityVncAuth.cxx | 88 + src/vnc/common/rfb/SSecurityVncAuth.h | 55 + src/vnc/common/rfb/ServerCore.cxx | 82 + src/vnc/common/rfb/ServerCore.h | 53 + src/vnc/common/rfb/Threading.h | 31 + src/vnc/common/rfb/Timer.cxx | 176 + src/vnc/common/rfb/Timer.h | 102 + src/vnc/common/rfb/TransImageGetter.cxx | 278 + src/vnc/common/rfb/TransImageGetter.h | 104 + src/vnc/common/rfb/TrueColourMap.h | 42 + src/vnc/common/rfb/UpdateTracker.cxx | 156 + src/vnc/common/rfb/UpdateTracker.h | 103 + src/vnc/common/rfb/UserPasswdGetter.h | 30 + src/vnc/common/rfb/VNCSConnectionST.cxx | 623 + src/vnc/common/rfb/VNCSConnectionST.h | 165 + src/vnc/common/rfb/VNCServer.h | 86 + src/vnc/common/rfb/VNCServerST.cxx | 419 + src/vnc/common/rfb/VNCServerST.h | 227 + src/vnc/common/rfb/ZRLEDecoder.cxx | 91 + src/vnc/common/rfb/ZRLEDecoder.h | 37 + src/vnc/common/rfb/ZRLEEncoder.cxx | 122 + src/vnc/common/rfb/ZRLEEncoder.h | 54 + src/vnc/common/rfb/d3des.c | 434 + src/vnc/common/rfb/d3des.h | 51 + src/vnc/common/rfb/encodings.cxx | 46 + src/vnc/common/rfb/encodings.h | 38 + src/vnc/common/rfb/hextileConstants.h | 27 + src/vnc/common/rfb/hextileDecode.h | 126 + src/vnc/common/rfb/hextileEncode.h | 247 + src/vnc/common/rfb/keysymdef.h | 1595 ++ src/vnc/common/rfb/msgTypes.h | 39 + src/vnc/common/rfb/rreDecode.h | 64 + src/vnc/common/rfb/rreEncode.h | 164 + src/vnc/common/rfb/secTypes.cxx | 71 + src/vnc/common/rfb/secTypes.h | 54 + src/vnc/common/rfb/transInitTempl.h | 254 + src/vnc/common/rfb/transTempl.h | 151 + src/vnc/common/rfb/util.cxx | 78 + src/vnc/common/rfb/util.h | 98 + src/vnc/common/rfb/zrleDecode.h | 253 + src/vnc/common/rfb/zrleEncode.h | 328 + src/vnc/common/zlib/CMakeLists.txt | 53 + src/vnc/common/zlib/ChangeLog | 481 + src/vnc/common/zlib/FAQ | 100 + src/vnc/common/zlib/INDEX | 86 + src/vnc/common/zlib/Make_vms.com | 115 + src/vnc/common/zlib/Makefile.in | 175 + src/vnc/common/zlib/Makefile.riscos | 151 + src/vnc/common/zlib/README | 147 + src/vnc/common/zlib/adler32.c | 48 + src/vnc/common/zlib/algorithm.txt | 213 + src/vnc/common/zlib/compress.c | 68 + src/vnc/common/zlib/configure | 212 + src/vnc/common/zlib/crc32.c | 162 + src/vnc/common/zlib/deflate.c | 1350 ++ src/vnc/common/zlib/deflate.h | 318 + src/vnc/common/zlib/descrip.mms | 48 + src/vnc/common/zlib/example.c | 556 + src/vnc/common/zlib/gzio.c | 875 + src/vnc/common/zlib/infblock.c | 403 + src/vnc/common/zlib/infblock.h | 39 + src/vnc/common/zlib/infcodes.c | 251 + src/vnc/common/zlib/infcodes.h | 27 + src/vnc/common/zlib/inffast.c | 183 + src/vnc/common/zlib/inffast.h | 17 + src/vnc/common/zlib/inffixed.h | 151 + src/vnc/common/zlib/inflate.c | 366 + src/vnc/common/zlib/inftrees.c | 454 + src/vnc/common/zlib/inftrees.h | 58 + src/vnc/common/zlib/infutil.c | 87 + src/vnc/common/zlib/infutil.h | 98 + src/vnc/common/zlib/maketree.c | 85 + src/vnc/common/zlib/minigzip.c | 324 + src/vnc/common/zlib/trees.c | 1214 + src/vnc/common/zlib/trees.h | 128 + src/vnc/common/zlib/uncompr.c | 58 + src/vnc/common/zlib/zconf.h | 279 + src/vnc/common/zlib/zlib.3 | 107 + src/vnc/common/zlib/zlib.h | 893 + src/vnc/common/zlib/zlib.html | 971 + src/vnc/common/zlib/zutil.c | 225 + src/vnc/common/zlib/zutil.h | 220 + src/vnc/unix/Makefile.in | 4 + src/vnc/unix/README | 289 + src/vnc/unix/README.hpux_aCC | 41 + src/vnc/unix/README.hpux_gcc | 63 + src/vnc/unix/boilerplate.mk | 35 + src/vnc/unix/cleanDeps | 3 + src/vnc/unix/common.mk | 2 + src/vnc/unix/configure | 2261 ++ src/vnc/unix/configure.in | 82 + src/vnc/unix/depend.mk | 76 + src/vnc/unix/hpux_aCC.patch | 255 + src/vnc/unix/hpux_gcc.patch | 255 + src/vnc/unix/tx/Makefile.in | 17 + src/vnc/unix/tx/TXButton.h | 124 + src/vnc/unix/tx/TXCheckbox.h | 142 + src/vnc/unix/tx/TXDialog.h | 96 + src/vnc/unix/tx/TXEntry.h | 188 + src/vnc/unix/tx/TXImage.cxx | 340 + src/vnc/unix/tx/TXImage.h | 89 + src/vnc/unix/tx/TXLabel.h | 126 + src/vnc/unix/tx/TXMenu.cxx | 186 + src/vnc/unix/tx/TXMenu.h | 67 + src/vnc/unix/tx/TXMsgBox.h | 112 + src/vnc/unix/tx/TXScrollbar.cxx | 119 + src/vnc/unix/tx/TXScrollbar.h | 82 + src/vnc/unix/tx/TXViewport.cxx | 155 + src/vnc/unix/tx/TXViewport.h | 77 + src/vnc/unix/tx/TXWindow.cxx | 486 + src/vnc/unix/tx/TXWindow.h | 211 + src/vnc/unix/vncconfig/Makefile.in | 24 + src/vnc/unix/vncconfig/QueryConnectDialog.cxx | 88 + src/vnc/unix/vncconfig/QueryConnectDialog.h | 56 + src/vnc/unix/vncconfig/buildtime.c | 18 + src/vnc/unix/vncconfig/vncExt.c | 384 + src/vnc/unix/vncconfig/vncExt.h | 343 + src/vnc/unix/vncconfig/vncconfig.cxx | 438 + src/vnc/unix/vncconfig/vncconfig.man | 127 + src/vnc/unix/vncinstall | 97 + src/vnc/unix/vncmkdepend/Makefile | 4 + src/vnc/unix/vncmkdepend/README | 39 + src/vnc/unix/vncmkdepend/cppsetup.c | 242 + src/vnc/unix/vncmkdepend/def.h | 140 + src/vnc/unix/vncmkdepend/ifparser.c | 445 + src/vnc/unix/vncmkdepend/ifparser.h | 76 + src/vnc/unix/vncmkdepend/include.c | 312 + src/vnc/unix/vncmkdepend/main.c | 593 + src/vnc/unix/vncmkdepend/parse.c | 568 + src/vnc/unix/vncmkdepend/pr.c | 130 + src/vnc/unix/vncpasswd/Makefile.in | 18 + src/vnc/unix/vncpasswd/vncpasswd.cxx | 138 + src/vnc/unix/vncpasswd/vncpasswd.man | 42 + src/vnc/unix/vncserver | 570 + src/vnc/unix/vncserver.man | 120 + src/vnc/unix/vncviewer/AboutDialog.h | 37 + src/vnc/unix/vncviewer/CConn.cxx | 681 + src/vnc/unix/vncviewer/CConn.h | 130 + src/vnc/unix/vncviewer/DesktopWindow.cxx | 571 + src/vnc/unix/vncviewer/DesktopWindow.h | 129 + src/vnc/unix/vncviewer/InfoDialog.h | 60 + src/vnc/unix/vncviewer/Makefile.in | 25 + src/vnc/unix/vncviewer/OptionsDialog.h | 168 + src/vnc/unix/vncviewer/PasswdDialog.h | 72 + src/vnc/unix/vncviewer/ServerDialog.h | 91 + src/vnc/unix/vncviewer/buildtime.c | 18 + src/vnc/unix/vncviewer/parameters.h | 44 + src/vnc/unix/vncviewer/vncviewer.cxx | 262 + src/vnc/unix/vncviewer/vncviewer.man | 190 + src/vnc/unix/x0vncserver/Image.cxx | 150 + src/vnc/unix/x0vncserver/Image.h | 48 + src/vnc/unix/x0vncserver/Makefile.in | 25 + src/vnc/unix/x0vncserver/buildtime.c | 18 + src/vnc/unix/x0vncserver/x0vncserver.cxx | 363 + src/vnc/unix/x0vncserver/x0vncserver.man | 32 + src/vnc/unix/xc.patch | 191 + src/vnc/unix/xc/config/cf/host.def | 1 + src/vnc/unix/xc/config/cf/vnc.def | 35 + src/vnc/unix/xc/programs/Xserver/Xvnc.man | 283 + .../unix/xc/programs/Xserver/vnc/Imakefile | 45 + .../xc/programs/Xserver/vnc/RegionHelper.h | 84 + .../xc/programs/Xserver/vnc/XserverDesktop.cc | 1152 + .../xc/programs/Xserver/vnc/XserverDesktop.h | 130 + .../xc/programs/Xserver/vnc/Xvnc/Imakefile | 78 + .../xc/programs/Xserver/vnc/Xvnc/buildtime.c | 18 + .../unix/xc/programs/Xserver/vnc/Xvnc/xvnc.cc | 1218 + .../xc/programs/Xserver/vnc/module/Imakefile | 61 + .../xc/programs/Xserver/vnc/vncExtInit.cc | 866 + .../unix/xc/programs/Xserver/vnc/vncExtInit.h | 32 + .../unix/xc/programs/Xserver/vnc/vncHooks.cc | 1475 ++ .../unix/xc/programs/Xserver/vnc/vncHooks.h | 26 + .../xc/programs/Xserver/vnc/xf86vncModule.cc | 97 + src/vnc/win/CMakeLists.txt | 19 + src/vnc/win/logmessages/messages.h | 47 + src/vnc/win/logmessages/messages.mc | 7 + src/vnc/win/logmessages/messages.rc | 2 + src/vnc/win/rfb_win32/AboutDialog.cxx | 49 + src/vnc/win/rfb_win32/AboutDialog.h | 55 + src/vnc/win/rfb_win32/BitmapInfo.h | 48 + src/vnc/win/rfb_win32/CKeyboard.cxx | 260 + src/vnc/win/rfb_win32/CKeyboard.h | 51 + src/vnc/win/rfb_win32/CMakeLists.txt | 119 + src/vnc/win/rfb_win32/CPointer.cxx | 186 + src/vnc/win/rfb_win32/CPointer.h | 74 + src/vnc/win/rfb_win32/CleanDesktop.cxx | 323 + src/vnc/win/rfb_win32/CleanDesktop.h | 57 + src/vnc/win/rfb_win32/Clipboard.cxx | 200 + src/vnc/win/rfb_win32/Clipboard.h | 66 + src/vnc/win/rfb_win32/CompatibleBitmap.h | 46 + src/vnc/win/rfb_win32/ComputerName.h | 40 + src/vnc/win/rfb_win32/CurrentUser.cxx | 152 + src/vnc/win/rfb_win32/CurrentUser.h | 100 + src/vnc/win/rfb_win32/DIBSectionBuffer.cxx | 222 + src/vnc/win/rfb_win32/DIBSectionBuffer.h | 85 + src/vnc/win/rfb_win32/DeviceContext.cxx | 188 + src/vnc/win/rfb_win32/DeviceContext.h | 86 + src/vnc/win/rfb_win32/DeviceFrameBuffer.cxx | 289 + src/vnc/win/rfb_win32/DeviceFrameBuffer.h | 106 + src/vnc/win/rfb_win32/Dialog.cxx | 391 + src/vnc/win/rfb_win32/Dialog.h | 158 + src/vnc/win/rfb_win32/DynamicFn.cxx | 45 + src/vnc/win/rfb_win32/DynamicFn.h | 51 + src/vnc/win/rfb_win32/EventManager.cxx | 103 + src/vnc/win/rfb_win32/EventManager.h | 77 + src/vnc/win/rfb_win32/Handle.h | 43 + src/vnc/win/rfb_win32/IconInfo.h | 44 + src/vnc/win/rfb_win32/IntervalTimer.h | 70 + src/vnc/win/rfb_win32/LaunchProcess.cxx | 103 + src/vnc/win/rfb_win32/LaunchProcess.h | 71 + src/vnc/win/rfb_win32/LocalMem.h | 45 + src/vnc/win/rfb_win32/LogicalPalette.h | 90 + src/vnc/win/rfb_win32/LowLevelKeyEvents.cxx | 96 + src/vnc/win/rfb_win32/LowLevelKeyEvents.h | 49 + src/vnc/win/rfb_win32/ModuleFileName.h | 40 + src/vnc/win/rfb_win32/MonitorInfo.cxx | 207 + src/vnc/win/rfb_win32/MonitorInfo.h | 72 + src/vnc/win/rfb_win32/MsgBox.h | 63 + src/vnc/win/rfb_win32/MsgWindow.cxx | 116 + src/vnc/win/rfb_win32/MsgWindow.h | 53 + src/vnc/win/rfb_win32/OSVersion.cxx | 47 + src/vnc/win/rfb_win32/OSVersion.h | 53 + src/vnc/win/rfb_win32/RegConfig.cxx | 114 + src/vnc/win/rfb_win32/RegConfig.h | 84 + src/vnc/win/rfb_win32/Registry.cxx | 316 + src/vnc/win/rfb_win32/Registry.h | 112 + src/vnc/win/rfb_win32/SDisplay.cxx | 525 + src/vnc/win/rfb_win32/SDisplay.h | 163 + src/vnc/win/rfb_win32/SDisplayCoreDriver.h | 52 + src/vnc/win/rfb_win32/SDisplayCorePolling.cxx | 81 + src/vnc/win/rfb_win32/SDisplayCorePolling.h | 75 + src/vnc/win/rfb_win32/SDisplayCoreWMHooks.cxx | 74 + src/vnc/win/rfb_win32/SDisplayCoreWMHooks.h | 68 + src/vnc/win/rfb_win32/SInput.cxx | 468 + src/vnc/win/rfb_win32/SInput.h | 68 + src/vnc/win/rfb_win32/Security.cxx | 192 + src/vnc/win/rfb_win32/Security.h | 123 + src/vnc/win/rfb_win32/Service.cxx | 645 + src/vnc/win/rfb_win32/Service.h | 128 + src/vnc/win/rfb_win32/SocketManager.cxx | 209 + src/vnc/win/rfb_win32/SocketManager.h | 90 + src/vnc/win/rfb_win32/TCharArray.cxx | 85 + src/vnc/win/rfb_win32/TCharArray.h | 135 + src/vnc/win/rfb_win32/Threading.cxx | 151 + src/vnc/win/rfb_win32/Threading.h | 154 + src/vnc/win/rfb_win32/TrayIcon.h | 89 + src/vnc/win/rfb_win32/TsSessions.cxx | 93 + src/vnc/win/rfb_win32/TsSessions.h | 61 + src/vnc/win/rfb_win32/WMCursor.cxx | 104 + src/vnc/win/rfb_win32/WMCursor.h | 61 + src/vnc/win/rfb_win32/WMHooks.cxx | 395 + src/vnc/win/rfb_win32/WMHooks.h | 92 + src/vnc/win/rfb_win32/WMNotifier.cxx | 57 + src/vnc/win/rfb_win32/WMNotifier.h | 68 + src/vnc/win/rfb_win32/WMPoller.cxx | 85 + src/vnc/win/rfb_win32/WMPoller.h | 62 + src/vnc/win/rfb_win32/WMShatter.cxx | 57 + src/vnc/win/rfb_win32/WMShatter.h | 50 + src/vnc/win/rfb_win32/WMWindowCopyRect.cxx | 67 + src/vnc/win/rfb_win32/WMWindowCopyRect.h | 53 + src/vnc/win/rfb_win32/Win32Util.cxx | 114 + src/vnc/win/rfb_win32/Win32Util.h | 55 + src/vnc/win/rfb_win32/keymap.h | 149 + src/vnc/win/vnc.suo | Bin 0 -> 26624 bytes src/vnc/win/vncconfig/Authentication.h | 142 + src/vnc/win/vncconfig/Connections.h | 298 + src/vnc/win/vncconfig/Desktop.h | 94 + src/vnc/win/vncconfig/Hooking.h | 88 + src/vnc/win/vncconfig/Inputs.h | 84 + src/vnc/win/vncconfig/Legacy.cxx | 248 + src/vnc/win/vncconfig/Legacy.h | 85 + src/vnc/win/vncconfig/PasswordDialog.cxx | 52 + src/vnc/win/vncconfig/PasswordDialog.h | 40 + src/vnc/win/vncconfig/Sharing.h | 59 + src/vnc/win/vncconfig/resource.h | 102 + src/vnc/win/vncconfig/vncconfig.cxx | 191 + src/vnc/win/vncconfig/vncconfig.exe.manifest | 22 + src/vnc/win/vncconfig/vncconfig.ico | Bin 0 -> 6006 bytes src/vnc/win/vncconfig/vncconfig.rc | 497 + src/vnc/win/vncviewer/CConn.cxx | 630 + src/vnc/win/vncviewer/CConn.h | 166 + src/vnc/win/vncviewer/CConnOptions.cxx | 383 + src/vnc/win/vncviewer/CConnOptions.h | 89 + src/vnc/win/vncviewer/CConnThread.cxx | 208 + src/vnc/win/vncviewer/CConnThread.h | 63 + src/vnc/win/vncviewer/CMakeLists.txt | 58 + src/vnc/win/vncviewer/ConnectingDialog.cxx | 160 + src/vnc/win/vncviewer/ConnectingDialog.h | 65 + src/vnc/win/vncviewer/ConnectionDialog.cxx | 79 + src/vnc/win/vncviewer/ConnectionDialog.h | 52 + src/vnc/win/vncviewer/DesktopWindow.cxx | 948 + src/vnc/win/vncviewer/DesktopWindow.h | 238 + src/vnc/win/vncviewer/InfoDialog.cxx | 65 + src/vnc/win/vncviewer/InfoDialog.h | 48 + src/vnc/win/vncviewer/ListenServer.h | 56 + src/vnc/win/vncviewer/ListenTrayIcon.h | 95 + src/vnc/win/vncviewer/MRU.h | 133 + src/vnc/win/vncviewer/OptionsDialog.cxx | 309 + src/vnc/win/vncviewer/OptionsDialog.h | 48 + src/vnc/win/vncviewer/UserPasswdDialog.cxx | 85 + src/vnc/win/vncviewer/UserPasswdDialog.h | 58 + src/vnc/win/vncviewer/buildTime.cxx | 18 + src/vnc/win/vncviewer/cursor1.cur | Bin 0 -> 326 bytes src/vnc/win/vncviewer/resource.h | 86 + src/vnc/win/vncviewer/vncviewer.bmp | Bin 0 -> 2006 bytes src/vnc/win/vncviewer/vncviewer.cxx | 300 + src/vnc/win/vncviewer/vncviewer.exe.manifest | 22 + src/vnc/win/vncviewer/vncviewer.h | 20 + src/vnc/win/vncviewer/vncviewer.ico | Bin 0 -> 6006 bytes src/vnc/win/vncviewer/vncviewer.rc | 531 + src/vnc/win/winvnc/AddNewClientDialog.h | 56 + src/vnc/win/winvnc/CMakeLists.txt | 55 + src/vnc/win/winvnc/JavaViewer.cxx | 98 + src/vnc/win/winvnc/JavaViewer.h | 55 + src/vnc/win/winvnc/ManagedListener.cxx | 94 + src/vnc/win/winvnc/ManagedListener.h | 57 + src/vnc/win/winvnc/QueryConnectDialog.cxx | 100 + src/vnc/win/winvnc/QueryConnectDialog.h | 60 + src/vnc/win/winvnc/STrayIcon.cxx | 246 + src/vnc/win/winvnc/STrayIcon.h | 59 + src/vnc/win/winvnc/VNCServerService.cxx | 52 + src/vnc/win/winvnc/VNCServerService.h | 41 + src/vnc/win/winvnc/VNCServerWin32.cxx | 310 + src/vnc/win/winvnc/VNCServerWin32.h | 124 + src/vnc/win/winvnc/buildTime.cxx | 18 + src/vnc/win/winvnc/connected.ico | Bin 0 -> 6006 bytes src/vnc/win/winvnc/resource.h | 38 + src/vnc/win/winvnc/winvnc.bmp | Bin 0 -> 2018 bytes src/vnc/win/winvnc/winvnc.cxx | 262 + src/vnc/win/winvnc/winvnc.h | 18 + src/vnc/win/winvnc/winvnc.ico | Bin 0 -> 6006 bytes src/vnc/win/winvnc/winvnc.rc | 273 + src/vnc/win/winvnc/winvnc4.exe.manifest | 22 + src/vnc/win/wm_hooks/CMakeLists.txt | 42 + src/vnc/win/wm_hooks/resource.h | 15 + src/vnc/win/wm_hooks/wm_hooks.aps | Bin 0 -> 18964 bytes src/vnc/win/wm_hooks/wm_hooks.cxx | 465 + src/vnc/win/wm_hooks/wm_hooks.def | 5 + src/vnc/win/wm_hooks/wm_hooks.h | 102 + src/vnc/win/wm_hooks/wm_hooks.rc | 109 + tools/build/README.txt | 1 + tools/build/__init__.py | 16 + tools/build/ftputil.py | 42 + tools/build/generators.py | 50 + tools/build/toolchain.py | 1255 + tools/gmock-1.6.0/CHANGES | 92 + tools/gmock-1.6.0/CMakeLists.txt | 151 + tools/gmock-1.6.0/CONTRIBUTORS | 40 + tools/gmock-1.6.0/COPYING | 28 + tools/gmock-1.6.0/Makefile.am | 209 + tools/gmock-1.6.0/Makefile.in | 1321 ++ tools/gmock-1.6.0/README | 354 + tools/gmock-1.6.0/aclocal.m4 | 9139 ++++++++ tools/gmock-1.6.0/build-aux/config.guess | 1533 ++ tools/gmock-1.6.0/build-aux/config.h.in | 69 + tools/gmock-1.6.0/build-aux/config.sub | 1693 ++ tools/gmock-1.6.0/build-aux/depcomp | 630 + tools/gmock-1.6.0/build-aux/install-sh | 520 + tools/gmock-1.6.0/build-aux/ltmain.sh | 8413 +++++++ tools/gmock-1.6.0/build-aux/missing | 376 + tools/gmock-1.6.0/configure | 17795 ++++++++++++++ tools/gmock-1.6.0/configure.ac | 146 + .../gmock-1.6.0/fused-src/gmock-gtest-all.cc | 10554 +++++++++ tools/gmock-1.6.0/fused-src/gmock/gmock.h | 12822 ++++++++++ tools/gmock-1.6.0/fused-src/gmock_main.cc | 54 + tools/gmock-1.6.0/fused-src/gtest/gtest.h | 19537 ++++++++++++++++ .../gmock-1.6.0/include/gmock/gmock-actions.h | 1076 + .../include/gmock/gmock-cardinalities.h | 146 + .../include/gmock/gmock-generated-actions.h | 2419 ++ .../gmock/gmock-generated-actions.h.pump | 825 + .../gmock/gmock-generated-function-mockers.h | 929 + .../gmock-generated-function-mockers.h.pump | 258 + .../include/gmock/gmock-generated-matchers.h | 2054 ++ .../gmock/gmock-generated-matchers.h.pump | 651 + .../gmock/gmock-generated-nice-strict.h | 274 + .../gmock/gmock-generated-nice-strict.h.pump | 160 + .../include/gmock/gmock-matchers.h | 3066 +++ .../include/gmock/gmock-more-actions.h | 233 + .../include/gmock/gmock-spec-builders.h | 1749 ++ tools/gmock-1.6.0/include/gmock/gmock.h | 93 + .../internal/gmock-generated-internal-utils.h | 277 + .../gmock-generated-internal-utils.h.pump | 136 + .../gmock/internal/gmock-internal-utils.h | 463 + .../include/gmock/internal/gmock-port.h | 78 + tools/gmock-1.6.0/make/Makefile | 98 + tools/gmock-1.6.0/msvc/2005/gmock.sln | 32 + tools/gmock-1.6.0/msvc/2005/gmock.vcproj | 191 + .../msvc/2005/gmock_config.vsprops | 15 + tools/gmock-1.6.0/msvc/2005/gmock_main.vcproj | 187 + tools/gmock-1.6.0/msvc/2005/gmock_test.vcproj | 201 + tools/gmock-1.6.0/msvc/2010/gmock.sln | 32 + tools/gmock-1.6.0/msvc/2010/gmock.vcxproj | 82 + .../gmock-1.6.0/msvc/2010/gmock_config.props | 19 + .../gmock-1.6.0/msvc/2010/gmock_main.vcxproj | 88 + .../gmock-1.6.0/msvc/2010/gmock_test.vcxproj | 101 + tools/gmock-1.6.0/scripts/fuse_gmock_files.py | 240 + tools/gmock-1.6.0/scripts/generator/COPYING | 203 + tools/gmock-1.6.0/scripts/generator/README | 35 + .../scripts/generator/README.cppclean | 115 + .../scripts/generator/cpp/__init__.py | 0 .../gmock-1.6.0/scripts/generator/cpp/ast.py | 1723 ++ .../scripts/generator/cpp/gmock_class.py | 192 + .../scripts/generator/cpp/keywords.py | 59 + .../scripts/generator/cpp/tokenize.py | 287 + .../scripts/generator/cpp/utils.py | 41 + .../scripts/generator/gmock_gen.py | 31 + tools/gmock-1.6.0/scripts/gmock-config.in | 303 + tools/gmock-1.6.0/src/gmock-all.cc | 47 + tools/gmock-1.6.0/src/gmock-cardinalities.cc | 155 + tools/gmock-1.6.0/src/gmock-internal-utils.cc | 173 + tools/gmock-1.6.0/src/gmock-matchers.cc | 101 + tools/gmock-1.6.0/src/gmock-spec-builders.cc | 797 + tools/gmock-1.6.0/src/gmock.cc | 182 + tools/gmock-1.6.0/src/gmock_main.cc | 54 + tools/gmock-1.6.0/test/gmock-actions_test.cc | 1305 ++ .../test/gmock-cardinalities_test.cc | 428 + .../test/gmock-generated-actions_test.cc | 1212 + .../gmock-generated-function-mockers_test.cc | 540 + .../gmock-generated-internal-utils_test.cc | 127 + .../test/gmock-generated-matchers_test.cc | 1127 + .../test/gmock-internal-utils_test.cc | 655 + tools/gmock-1.6.0/test/gmock-matchers_test.cc | 4040 ++++ .../test/gmock-more-actions_test.cc | 704 + .../test/gmock-nice-strict_test.cc | 284 + tools/gmock-1.6.0/test/gmock-port_test.cc | 43 + .../test/gmock-spec-builders_test.cc | 2484 ++ tools/gmock-1.6.0/test/gmock_all_test.cc | 48 + tools/gmock-1.6.0/test/gmock_leak_test.py | 90 + tools/gmock-1.6.0/test/gmock_leak_test_.cc | 100 + tools/gmock-1.6.0/test/gmock_link2_test.cc | 40 + tools/gmock-1.6.0/test/gmock_link_test.cc | 40 + tools/gmock-1.6.0/test/gmock_link_test.h | 669 + tools/gmock-1.6.0/test/gmock_output_test.py | 180 + tools/gmock-1.6.0/test/gmock_output_test_.cc | 290 + .../test/gmock_output_test_golden.txt | 310 + tools/gmock-1.6.0/test/gmock_test.cc | 255 + tools/gmock-1.6.0/test/gmock_test_utils.py | 111 + tools/gtest-1.6.0/CHANGES | 130 + tools/gtest-1.6.0/CMakeLists.txt | 240 + tools/gtest-1.6.0/CONTRIBUTORS | 37 + tools/gtest-1.6.0/COPYING | 28 + tools/gtest-1.6.0/Makefile.am | 302 + tools/gtest-1.6.0/Makefile.in | 1329 ++ tools/gtest-1.6.0/README | 424 + tools/gtest-1.6.0/aclocal.m4 | 1178 + tools/gtest-1.6.0/build-aux/config.guess | 1533 ++ tools/gtest-1.6.0/build-aux/config.h.in | 69 + tools/gtest-1.6.0/build-aux/config.sub | 1693 ++ tools/gtest-1.6.0/build-aux/depcomp | 630 + tools/gtest-1.6.0/build-aux/install-sh | 520 + tools/gtest-1.6.0/build-aux/ltmain.sh | 8413 +++++++ tools/gtest-1.6.0/build-aux/missing | 376 + tools/gtest-1.6.0/cmake/internal_utils.cmake | 216 + tools/gtest-1.6.0/codegear/gtest.cbproj | 138 + tools/gtest-1.6.0/codegear/gtest.groupproj | 54 + tools/gtest-1.6.0/codegear/gtest_all.cc | 38 + tools/gtest-1.6.0/codegear/gtest_link.cc | 40 + tools/gtest-1.6.0/codegear/gtest_main.cbproj | 82 + .../codegear/gtest_unittest.cbproj | 88 + tools/gtest-1.6.0/configure | 17482 ++++++++++++++ tools/gtest-1.6.0/configure.ac | 68 + .../gtest-1.6.0/fused-src/gtest/gtest-all.cc | 9118 ++++++++ tools/gtest-1.6.0/fused-src/gtest/gtest.h | 19537 ++++++++++++++++ .../gtest-1.6.0/fused-src/gtest/gtest_main.cc | 39 + .../include/gtest/gtest-death-test.h | 283 + .../gtest-1.6.0/include/gtest/gtest-message.h | 230 + .../include/gtest/gtest-param-test.h | 1421 ++ .../include/gtest/gtest-param-test.h.pump | 487 + .../include/gtest/gtest-printers.h | 796 + tools/gtest-1.6.0/include/gtest/gtest-spi.h | 232 + .../include/gtest/gtest-test-part.h | 176 + .../include/gtest/gtest-typed-test.h | 259 + tools/gtest-1.6.0/include/gtest/gtest.h | 2155 ++ .../include/gtest/gtest_pred_impl.h | 358 + tools/gtest-1.6.0/include/gtest/gtest_prod.h | 58 + .../internal/gtest-death-test-internal.h | 308 + .../include/gtest/internal/gtest-filepath.h | 210 + .../include/gtest/internal/gtest-internal.h | 1226 + .../include/gtest/internal/gtest-linked_ptr.h | 233 + .../internal/gtest-param-util-generated.h | 4822 ++++ .../gtest-param-util-generated.h.pump | 301 + .../include/gtest/internal/gtest-param-util.h | 619 + .../include/gtest/internal/gtest-port.h | 1775 ++ .../include/gtest/internal/gtest-string.h | 350 + .../include/gtest/internal/gtest-tuple.h | 968 + .../include/gtest/internal/gtest-tuple.h.pump | 336 + .../include/gtest/internal/gtest-type-util.h | 3330 +++ .../gtest/internal/gtest-type-util.h.pump | 296 + tools/gtest-1.6.0/m4/acx_pthread.m4 | 363 + tools/gtest-1.6.0/m4/gtest.m4 | 74 + tools/gtest-1.6.0/m4/libtool.m4 | 7377 ++++++ tools/gtest-1.6.0/m4/ltoptions.m4 | 368 + tools/gtest-1.6.0/m4/ltsugar.m4 | 123 + tools/gtest-1.6.0/m4/ltversion.m4 | 23 + tools/gtest-1.6.0/m4/lt~obsolete.m4 | 92 + tools/gtest-1.6.0/make/Makefile | 80 + tools/gtest-1.6.0/msvc/gtest-md.sln | 45 + tools/gtest-1.6.0/msvc/gtest-md.vcproj | 126 + tools/gtest-1.6.0/msvc/gtest.sln | 45 + tools/gtest-1.6.0/msvc/gtest.vcproj | 126 + tools/gtest-1.6.0/msvc/gtest_main-md.vcproj | 129 + tools/gtest-1.6.0/msvc/gtest_main.vcproj | 129 + .../msvc/gtest_prod_test-md.vcproj | 164 + tools/gtest-1.6.0/msvc/gtest_prod_test.vcproj | 164 + .../gtest-1.6.0/msvc/gtest_unittest-md.vcproj | 147 + tools/gtest-1.6.0/msvc/gtest_unittest.vcproj | 147 + tools/gtest-1.6.0/samples/prime_tables.h | 123 + tools/gtest-1.6.0/samples/sample1.cc | 68 + tools/gtest-1.6.0/samples/sample1.h | 43 + .../gtest-1.6.0/samples/sample10_unittest.cc | 145 + tools/gtest-1.6.0/samples/sample1_unittest.cc | 153 + tools/gtest-1.6.0/samples/sample2.cc | 56 + tools/gtest-1.6.0/samples/sample2.h | 86 + tools/gtest-1.6.0/samples/sample2_unittest.cc | 109 + tools/gtest-1.6.0/samples/sample3-inl.h | 173 + tools/gtest-1.6.0/samples/sample3_unittest.cc | 151 + tools/gtest-1.6.0/samples/sample4.cc | 46 + tools/gtest-1.6.0/samples/sample4.h | 53 + tools/gtest-1.6.0/samples/sample4_unittest.cc | 45 + tools/gtest-1.6.0/samples/sample5_unittest.cc | 199 + tools/gtest-1.6.0/samples/sample6_unittest.cc | 224 + tools/gtest-1.6.0/samples/sample7_unittest.cc | 130 + tools/gtest-1.6.0/samples/sample8_unittest.cc | 173 + tools/gtest-1.6.0/samples/sample9_unittest.cc | 160 + tools/gtest-1.6.0/scripts/fuse_gtest_files.py | 250 + .../scripts/gen_gtest_pred_impl.py | 730 + tools/gtest-1.6.0/scripts/gtest-config.in | 274 + tools/gtest-1.6.0/scripts/pump.py | 847 + tools/gtest-1.6.0/scripts/test/Makefile | 59 + tools/gtest-1.6.0/src/gtest-all.cc | 48 + tools/gtest-1.6.0/src/gtest-death-test.cc | 1234 + tools/gtest-1.6.0/src/gtest-filepath.cc | 380 + tools/gtest-1.6.0/src/gtest-internal-inl.h | 1038 + tools/gtest-1.6.0/src/gtest-port.cc | 746 + tools/gtest-1.6.0/src/gtest-printers.cc | 356 + tools/gtest-1.6.0/src/gtest-test-part.cc | 110 + tools/gtest-1.6.0/src/gtest-typed-test.cc | 110 + tools/gtest-1.6.0/src/gtest.cc | 4898 ++++ tools/gtest-1.6.0/src/gtest_main.cc | 39 + .../test/gtest-death-test_ex_test.cc | 93 + .../gtest-1.6.0/test/gtest-death-test_test.cc | 1296 + tools/gtest-1.6.0/test/gtest-filepath_test.cc | 696 + .../gtest-1.6.0/test/gtest-linked_ptr_test.cc | 155 + tools/gtest-1.6.0/test/gtest-listener_test.cc | 313 + tools/gtest-1.6.0/test/gtest-message_test.cc | 166 + tools/gtest-1.6.0/test/gtest-options_test.cc | 212 + .../test/gtest-param-test2_test.cc | 65 + .../gtest-1.6.0/test/gtest-param-test_test.cc | 895 + .../gtest-1.6.0/test/gtest-param-test_test.h | 55 + tools/gtest-1.6.0/test/gtest-port_test.cc | 1206 + tools/gtest-1.6.0/test/gtest-printers_test.cc | 1307 ++ .../gtest-1.6.0/test/gtest-test-part_test.cc | 208 + tools/gtest-1.6.0/test/gtest-tuple_test.cc | 320 + .../test/gtest-typed-test2_test.cc | 45 + .../gtest-1.6.0/test/gtest-typed-test_test.cc | 360 + .../gtest-1.6.0/test/gtest-typed-test_test.h | 66 + .../test/gtest-unittest-api_test.cc | 341 + tools/gtest-1.6.0/test/gtest_all_test.cc | 47 + .../test/gtest_break_on_failure_unittest.py | 218 + .../test/gtest_break_on_failure_unittest_.cc | 88 + .../test/gtest_catch_exceptions_test.py | 220 + .../test/gtest_catch_exceptions_test_.cc | 308 + tools/gtest-1.6.0/test/gtest_color_test.py | 130 + tools/gtest-1.6.0/test/gtest_color_test_.cc | 71 + tools/gtest-1.6.0/test/gtest_env_var_test.py | 103 + tools/gtest-1.6.0/test/gtest_env_var_test_.cc | 126 + .../test/gtest_environment_test.cc | 191 + .../gtest-1.6.0/test/gtest_filter_unittest.py | 633 + .../test/gtest_filter_unittest_.cc | 140 + tools/gtest-1.6.0/test/gtest_help_test.py | 172 + tools/gtest-1.6.0/test/gtest_help_test_.cc | 46 + .../test/gtest_list_tests_unittest.py | 177 + .../test/gtest_list_tests_unittest_.cc | 85 + tools/gtest-1.6.0/test/gtest_main_unittest.cc | 45 + .../test/gtest_no_test_unittest.cc | 57 + tools/gtest-1.6.0/test/gtest_output_test.py | 335 + tools/gtest-1.6.0/test/gtest_output_test_.cc | 1020 + .../test/gtest_output_test_golden_lin.txt | 711 + .../test/gtest_pred_impl_unittest.cc | 2427 ++ tools/gtest-1.6.0/test/gtest_prod_test.cc | 57 + tools/gtest-1.6.0/test/gtest_repeat_test.cc | 253 + tools/gtest-1.6.0/test/gtest_shuffle_test.py | 325 + tools/gtest-1.6.0/test/gtest_shuffle_test_.cc | 104 + .../test/gtest_sole_header_test.cc | 57 + tools/gtest-1.6.0/test/gtest_stress_test.cc | 257 + tools/gtest-1.6.0/test/gtest_test_utils.py | 305 + .../test/gtest_throw_on_failure_ex_test.cc | 92 + .../test/gtest_throw_on_failure_test.py | 171 + .../test/gtest_throw_on_failure_test_.cc | 56 + .../test/gtest_uninitialized_test.py | 70 + .../test/gtest_uninitialized_test_.cc | 43 + tools/gtest-1.6.0/test/gtest_unittest.cc | 7337 ++++++ .../test/gtest_xml_outfile1_test_.cc | 49 + .../test/gtest_xml_outfile2_test_.cc | 49 + .../test/gtest_xml_outfiles_test.py | 132 + .../test/gtest_xml_output_unittest.py | 242 + .../test/gtest_xml_output_unittest_.cc | 174 + .../gtest-1.6.0/test/gtest_xml_test_utils.py | 179 + tools/gtest-1.6.0/test/production.cc | 36 + tools/gtest-1.6.0/test/production.h | 55 + .../xcode/Config/DebugProject.xcconfig | 30 + .../xcode/Config/FrameworkTarget.xcconfig | 17 + .../gtest-1.6.0/xcode/Config/General.xcconfig | 41 + .../xcode/Config/ReleaseProject.xcconfig | 32 + .../xcode/Config/StaticLibraryTarget.xcconfig | 18 + .../xcode/Config/TestTarget.xcconfig | 8 + tools/gtest-1.6.0/xcode/Resources/Info.plist | 30 + .../xcode/Samples/FrameworkSample/Info.plist | 28 + .../WidgetFramework.xcodeproj/project.pbxproj | 457 + .../xcode/Samples/FrameworkSample/runtests.sh | 62 + .../xcode/Samples/FrameworkSample/widget.cc | 63 + .../xcode/Samples/FrameworkSample/widget.h | 59 + .../Samples/FrameworkSample/widget_test.cc | 68 + tools/gtest-1.6.0/xcode/Scripts/runtests.sh | 65 + .../xcode/Scripts/versiongenerate.py | 100 + .../xcode/gtest.xcodeproj/project.pbxproj | 1084 + 1291 files changed, 425650 insertions(+), 12 deletions(-) create mode 100644 .gitignore create mode 100644 .lvimrc create mode 100644 CMakeLists.txt create mode 100644 COMPILE create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 INSTALL create mode 100755 configure create mode 100755 doc/MacReadme.txt create mode 100644 doc/org.synergy-foss.org.synergyc.plist create mode 100644 doc/org.synergy-foss.org.synergys.plist create mode 100644 doc/synergy.conf.example create mode 100644 doc/synergy.conf.example-advanced create mode 100644 doc/synergy.conf.example-basic create mode 100644 doc/synergyc.man create mode 100644 doc/synergys.man create mode 100644 hm.cmd create mode 100644 hm.py create mode 100755 hm.sh create mode 100644 res/DefineIfExist.nsh create mode 100644 res/Installer.nsi.in create mode 100644 res/License.rtf create mode 100644 res/License.tex create mode 100644 res/Readme.txt create mode 100644 res/config.h.in create mode 100644 res/doxygen.cfg.in create mode 100644 res/synergy.desktop create mode 100644 res/synergy.ico create mode 100644 src/CMakeLists.txt create mode 100644 src/cmd/CMakeLists.txt create mode 100644 src/cmd/synergyc/.gitignore create mode 100644 src/cmd/synergyc/CMSWindowsClientTaskBarReceiver.cpp create mode 100644 src/cmd/synergyc/CMSWindowsClientTaskBarReceiver.h create mode 100644 src/cmd/synergyc/CMakeLists.txt create mode 100644 src/cmd/synergyc/COSXClientTaskBarReceiver.cpp create mode 100644 src/cmd/synergyc/COSXClientTaskBarReceiver.h create mode 100644 src/cmd/synergyc/CXWindowsClientTaskBarReceiver.cpp create mode 100644 src/cmd/synergyc/CXWindowsClientTaskBarReceiver.h create mode 100644 src/cmd/synergyc/resource.h create mode 100644 src/cmd/synergyc/synergyc.cpp create mode 100644 src/cmd/synergyc/synergyc.ico create mode 100644 src/cmd/synergyc/synergyc.rc create mode 100644 src/cmd/synergyc/tb_error.ico create mode 100644 src/cmd/synergyc/tb_idle.ico create mode 100644 src/cmd/synergyc/tb_run.ico create mode 100644 src/cmd/synergyc/tb_wait.ico create mode 100644 src/cmd/synergyd/CMakeLists.txt create mode 100644 src/cmd/synergyd/synergyd.cpp create mode 100644 src/cmd/synergys/.gitignore create mode 100644 src/cmd/synergys/CMSWindowsServerTaskBarReceiver.cpp create mode 100644 src/cmd/synergys/CMSWindowsServerTaskBarReceiver.h create mode 100644 src/cmd/synergys/CMakeLists.txt create mode 100644 src/cmd/synergys/COSXServerTaskBarReceiver.cpp create mode 100644 src/cmd/synergys/COSXServerTaskBarReceiver.h create mode 100644 src/cmd/synergys/CXWindowsServerTaskBarReceiver.cpp create mode 100644 src/cmd/synergys/CXWindowsServerTaskBarReceiver.h create mode 100644 src/cmd/synergys/resource.h create mode 100644 src/cmd/synergys/synergys.cpp create mode 100644 src/cmd/synergys/synergys.ico create mode 100644 src/cmd/synergys/synergys.rc create mode 100644 src/cmd/synergys/tb_error.ico create mode 100644 src/cmd/synergys/tb_idle.ico create mode 100644 src/cmd/synergys/tb_run.ico create mode 100644 src/cmd/synergys/tb_wait.ico create mode 100644 src/gui/gui.pro create mode 100644 src/gui/gui.ts create mode 100644 src/gui/lang.cmd create mode 100644 src/gui/res/AboutDialogBase.ui create mode 100644 src/gui/res/ActionDialogBase.ui create mode 100644 src/gui/res/HotkeyDialogBase.ui create mode 100644 src/gui/res/MainWindowBase.ui create mode 100644 src/gui/res/ScreenSettingsDialogBase.ui create mode 100644 src/gui/res/ServerConfigDialogBase.ui create mode 100644 src/gui/res/SettingsDialogBase.ui create mode 100644 src/gui/res/SetupWizardBase.ui create mode 100644 src/gui/res/Synergy.qrc create mode 100644 src/gui/res/icons/16x16/synergy-connected.png create mode 100644 src/gui/res/icons/16x16/synergy-connected.psd create mode 100644 src/gui/res/icons/16x16/synergy-disconnected.png create mode 100644 src/gui/res/icons/16x16/synergy-disconnected.psd create mode 100644 src/gui/res/icons/16x16/warning.png create mode 100644 src/gui/res/icons/256x256/synergy.ico create mode 100644 src/gui/res/icons/64x64/user-trash.png create mode 100644 src/gui/res/icons/64x64/video-display.png create mode 100644 src/gui/res/lang/nl_NL.qm create mode 100644 src/gui/res/lang/nl_NL.ts create mode 100644 src/gui/res/mac/QSynergy.icns create mode 100644 src/gui/res/mac/Synergy.icns create mode 100644 src/gui/res/mac/Synergy.plist create mode 100644 src/gui/res/win/Synergy.rc create mode 100644 src/gui/src/AboutDialog.cpp create mode 100644 src/gui/src/AboutDialog.h create mode 100644 src/gui/src/Action.cpp create mode 100644 src/gui/src/Action.h create mode 100644 src/gui/src/ActionDialog.cpp create mode 100644 src/gui/src/ActionDialog.h create mode 100644 src/gui/src/AppConfig.cpp create mode 100644 src/gui/src/AppConfig.h create mode 100644 src/gui/src/BaseConfig.cpp create mode 100644 src/gui/src/BaseConfig.h create mode 100644 src/gui/src/Hotkey.cpp create mode 100644 src/gui/src/Hotkey.h create mode 100644 src/gui/src/HotkeyDialog.cpp create mode 100644 src/gui/src/HotkeyDialog.h create mode 100644 src/gui/src/IpcLogReader.cpp create mode 100644 src/gui/src/IpcLogReader.h create mode 100644 src/gui/src/KeySequence.cpp create mode 100644 src/gui/src/KeySequence.h create mode 100644 src/gui/src/KeySequenceWidget.cpp create mode 100644 src/gui/src/KeySequenceWidget.h create mode 100644 src/gui/src/MainWindow.cpp create mode 100644 src/gui/src/MainWindow.h create mode 100644 src/gui/src/NewScreenWidget.cpp create mode 100644 src/gui/src/NewScreenWidget.h create mode 100644 src/gui/src/QSynergyApplication.cpp create mode 100644 src/gui/src/QSynergyApplication.h create mode 100644 src/gui/src/Screen.cpp create mode 100644 src/gui/src/Screen.h create mode 100644 src/gui/src/ScreenSettingsDialog.cpp create mode 100644 src/gui/src/ScreenSettingsDialog.h create mode 100644 src/gui/src/ScreenSetupModel.cpp create mode 100644 src/gui/src/ScreenSetupModel.h create mode 100644 src/gui/src/ScreenSetupView.cpp create mode 100644 src/gui/src/ScreenSetupView.h create mode 100644 src/gui/src/ServerConfig.cpp create mode 100644 src/gui/src/ServerConfig.h create mode 100644 src/gui/src/ServerConfigDialog.cpp create mode 100644 src/gui/src/ServerConfigDialog.h create mode 100644 src/gui/src/SettingsDialog.cpp create mode 100644 src/gui/src/SettingsDialog.h create mode 100644 src/gui/src/SetupWizard.cpp create mode 100644 src/gui/src/SetupWizard.h create mode 100644 src/gui/src/TrashScreenWidget.cpp create mode 100644 src/gui/src/TrashScreenWidget.h create mode 100644 src/gui/src/UpdateCheck.cpp create mode 100644 src/gui/src/VersionChecker.cpp create mode 100644 src/gui/src/VersionChecker.h create mode 100644 src/gui/src/main.cpp create mode 100644 src/lib/CMakeLists.txt create mode 100644 src/lib/arch/CArch.cpp create mode 100644 src/lib/arch/CArch.h create mode 100644 src/lib/arch/CArchConsoleStd.cpp create mode 100644 src/lib/arch/CArchConsoleStd.h create mode 100644 src/lib/arch/CArchConsoleUnix.cpp create mode 100644 src/lib/arch/CArchConsoleUnix.h create mode 100644 src/lib/arch/CArchConsoleWindows.cpp create mode 100644 src/lib/arch/CArchConsoleWindows.h create mode 100644 src/lib/arch/CArchDaemonNone.cpp create mode 100644 src/lib/arch/CArchDaemonNone.h create mode 100644 src/lib/arch/CArchDaemonUnix.cpp create mode 100644 src/lib/arch/CArchDaemonUnix.h create mode 100644 src/lib/arch/CArchDaemonWindows.cpp create mode 100644 src/lib/arch/CArchDaemonWindows.h create mode 100644 src/lib/arch/CArchFileUnix.cpp create mode 100644 src/lib/arch/CArchFileUnix.h create mode 100644 src/lib/arch/CArchFileWindows.cpp create mode 100644 src/lib/arch/CArchFileWindows.h create mode 100644 src/lib/arch/CArchIpcLogUnix.cpp create mode 100644 src/lib/arch/CArchIpcLogUnix.h create mode 100644 src/lib/arch/CArchIpcLogWindows.cpp create mode 100644 src/lib/arch/CArchIpcLogWindows.h create mode 100644 src/lib/arch/CArchLogUnix.cpp create mode 100644 src/lib/arch/CArchLogUnix.h create mode 100644 src/lib/arch/CArchLogWindows.cpp create mode 100644 src/lib/arch/CArchLogWindows.h create mode 100644 src/lib/arch/CArchMiscWindows.cpp create mode 100644 src/lib/arch/CArchMiscWindows.h create mode 100644 src/lib/arch/CArchMultithreadPosix.cpp create mode 100644 src/lib/arch/CArchMultithreadPosix.h create mode 100644 src/lib/arch/CArchMultithreadWindows.cpp create mode 100644 src/lib/arch/CArchMultithreadWindows.h create mode 100644 src/lib/arch/CArchNetworkBSD.cpp create mode 100644 src/lib/arch/CArchNetworkBSD.h create mode 100644 src/lib/arch/CArchNetworkWinsock.cpp create mode 100644 src/lib/arch/CArchNetworkWinsock.h create mode 100644 src/lib/arch/CArchPluginUnix.cpp create mode 100644 src/lib/arch/CArchPluginUnix.h create mode 100644 src/lib/arch/CArchPluginWindows.cpp create mode 100644 src/lib/arch/CArchPluginWindows.h create mode 100644 src/lib/arch/CArchSleepUnix.cpp create mode 100644 src/lib/arch/CArchSleepUnix.h create mode 100644 src/lib/arch/CArchSleepWindows.cpp create mode 100644 src/lib/arch/CArchSleepWindows.h create mode 100644 src/lib/arch/CArchStringUnix.cpp create mode 100644 src/lib/arch/CArchStringUnix.h create mode 100644 src/lib/arch/CArchStringWindows.cpp create mode 100644 src/lib/arch/CArchStringWindows.h create mode 100644 src/lib/arch/CArchSystemUnix.cpp create mode 100644 src/lib/arch/CArchSystemUnix.h create mode 100644 src/lib/arch/CArchSystemWindows.cpp create mode 100644 src/lib/arch/CArchSystemWindows.h create mode 100644 src/lib/arch/CArchTaskBarWindows.cpp create mode 100644 src/lib/arch/CArchTaskBarWindows.h create mode 100644 src/lib/arch/CArchTaskBarXWindows.cpp create mode 100644 src/lib/arch/CArchTaskBarXWindows.h create mode 100644 src/lib/arch/CArchTimeUnix.cpp create mode 100644 src/lib/arch/CArchTimeUnix.h create mode 100644 src/lib/arch/CArchTimeWindows.cpp create mode 100644 src/lib/arch/CArchTimeWindows.h create mode 100644 src/lib/arch/CMakeLists.txt create mode 100644 src/lib/arch/CMultibyte.h create mode 100644 src/lib/arch/IArchConsole.h create mode 100644 src/lib/arch/IArchDaemon.h create mode 100644 src/lib/arch/IArchFile.h create mode 100644 src/lib/arch/IArchLog.h create mode 100644 src/lib/arch/IArchMultithread.h create mode 100644 src/lib/arch/IArchNetwork.h create mode 100644 src/lib/arch/IArchPlugin.h create mode 100644 src/lib/arch/IArchSleep.h create mode 100644 src/lib/arch/IArchString.cpp create mode 100644 src/lib/arch/IArchString.h create mode 100644 src/lib/arch/IArchSystem.h create mode 100644 src/lib/arch/IArchTaskBar.h create mode 100644 src/lib/arch/IArchTaskBarReceiver.h create mode 100644 src/lib/arch/IArchTime.h create mode 100644 src/lib/arch/XArch.cpp create mode 100644 src/lib/arch/XArch.h create mode 100644 src/lib/arch/XArchUnix.cpp create mode 100644 src/lib/arch/XArchUnix.h create mode 100644 src/lib/arch/XArchWindows.cpp create mode 100644 src/lib/arch/XArchWindows.h create mode 100644 src/lib/arch/vsnprintf.h create mode 100644 src/lib/base/CEvent.cpp create mode 100644 src/lib/base/CEvent.h create mode 100644 src/lib/base/CEventQueue.cpp create mode 100644 src/lib/base/CEventQueue.h create mode 100644 src/lib/base/CFunctionEventJob.cpp create mode 100644 src/lib/base/CFunctionEventJob.h create mode 100644 src/lib/base/CFunctionJob.cpp create mode 100644 src/lib/base/CFunctionJob.h create mode 100644 src/lib/base/CLog.cpp create mode 100644 src/lib/base/CLog.h create mode 100644 src/lib/base/CMakeLists.txt create mode 100644 src/lib/base/CPriorityQueue.h create mode 100644 src/lib/base/CSimpleEventQueueBuffer.cpp create mode 100644 src/lib/base/CSimpleEventQueueBuffer.h create mode 100644 src/lib/base/CStopwatch.cpp create mode 100644 src/lib/base/CStopwatch.h create mode 100644 src/lib/base/CString.h create mode 100644 src/lib/base/CStringUtil.cpp create mode 100644 src/lib/base/CStringUtil.h create mode 100644 src/lib/base/CUnicode.cpp create mode 100644 src/lib/base/CUnicode.h create mode 100644 src/lib/base/ELevel.h create mode 100644 src/lib/base/IEventJob.h create mode 100644 src/lib/base/IEventQueue.cpp create mode 100644 src/lib/base/IEventQueue.h create mode 100644 src/lib/base/IEventQueueBuffer.h create mode 100644 src/lib/base/IJob.h create mode 100644 src/lib/base/ILogOutputter.h create mode 100644 src/lib/base/LogOutputters.cpp create mode 100644 src/lib/base/LogOutputters.h create mode 100644 src/lib/base/TMethodEventJob.h create mode 100644 src/lib/base/TMethodJob.h create mode 100644 src/lib/base/XBase.cpp create mode 100644 src/lib/base/XBase.h create mode 100644 src/lib/client/CClient.cpp create mode 100644 src/lib/client/CClient.h create mode 100644 src/lib/client/CMakeLists.txt create mode 100644 src/lib/client/CServerProxy.cpp create mode 100644 src/lib/client/CServerProxy.h create mode 100644 src/lib/common/BasicTypes.h create mode 100644 src/lib/common/CMakeLists.txt create mode 100644 src/lib/common/IInterface.h create mode 100644 src/lib/common/MacOSXPrecomp.h create mode 100644 src/lib/common/Version.cpp create mode 100644 src/lib/common/Version.h create mode 100644 src/lib/common/common.h create mode 100644 src/lib/common/stdbitset.h create mode 100644 src/lib/common/stddeque.h create mode 100644 src/lib/common/stdfstream.h create mode 100644 src/lib/common/stdistream.h create mode 100644 src/lib/common/stdlist.h create mode 100644 src/lib/common/stdmap.h create mode 100644 src/lib/common/stdostream.h create mode 100644 src/lib/common/stdpost.h create mode 100644 src/lib/common/stdpre.h create mode 100644 src/lib/common/stdset.h create mode 100644 src/lib/common/stdsstream.h create mode 100644 src/lib/common/stdstring.h create mode 100644 src/lib/common/stdvector.h create mode 100644 src/lib/io/CMakeLists.txt create mode 100644 src/lib/io/CStreamBuffer.cpp create mode 100644 src/lib/io/CStreamBuffer.h create mode 100644 src/lib/io/CStreamFilter.cpp create mode 100644 src/lib/io/CStreamFilter.h create mode 100644 src/lib/io/IStream.cpp create mode 100644 src/lib/io/IStream.h create mode 100644 src/lib/io/IStreamFilterFactory.h create mode 100644 src/lib/io/XIO.cpp create mode 100644 src/lib/io/XIO.h create mode 100644 src/lib/mt/CCondVar.cpp create mode 100644 src/lib/mt/CCondVar.h create mode 100644 src/lib/mt/CLock.cpp create mode 100644 src/lib/mt/CLock.h create mode 100644 src/lib/mt/CMakeLists.txt create mode 100644 src/lib/mt/CMutex.cpp create mode 100644 src/lib/mt/CMutex.h create mode 100644 src/lib/mt/CThread.cpp create mode 100644 src/lib/mt/CThread.h create mode 100644 src/lib/mt/XMT.cpp create mode 100644 src/lib/mt/XMT.h create mode 100644 src/lib/mt/XThread.h create mode 100644 src/lib/net/CMakeLists.txt create mode 100644 src/lib/net/CNetworkAddress.cpp create mode 100644 src/lib/net/CNetworkAddress.h create mode 100644 src/lib/net/CSocketMultiplexer.cpp create mode 100644 src/lib/net/CSocketMultiplexer.h create mode 100644 src/lib/net/CTCPListenSocket.cpp create mode 100644 src/lib/net/CTCPListenSocket.h create mode 100644 src/lib/net/CTCPSocket.cpp create mode 100644 src/lib/net/CTCPSocket.h create mode 100644 src/lib/net/CTCPSocketFactory.cpp create mode 100644 src/lib/net/CTCPSocketFactory.h create mode 100644 src/lib/net/IDataSocket.cpp create mode 100644 src/lib/net/IDataSocket.h create mode 100644 src/lib/net/IListenSocket.cpp create mode 100644 src/lib/net/IListenSocket.h create mode 100644 src/lib/net/ISocket.cpp create mode 100644 src/lib/net/ISocket.h create mode 100644 src/lib/net/ISocketFactory.h create mode 100644 src/lib/net/ISocketMultiplexerJob.h create mode 100644 src/lib/net/TSocketMultiplexerMethodJob.h create mode 100644 src/lib/net/XSocket.cpp create mode 100644 src/lib/net/XSocket.h create mode 100644 src/lib/platform/CMSWindowsClipboard.cpp create mode 100644 src/lib/platform/CMSWindowsClipboard.h create mode 100644 src/lib/platform/CMSWindowsClipboardAnyTextConverter.cpp create mode 100644 src/lib/platform/CMSWindowsClipboardAnyTextConverter.h create mode 100644 src/lib/platform/CMSWindowsClipboardBitmapConverter.cpp create mode 100644 src/lib/platform/CMSWindowsClipboardBitmapConverter.h create mode 100644 src/lib/platform/CMSWindowsClipboardFacade.cpp create mode 100644 src/lib/platform/CMSWindowsClipboardFacade.h create mode 100644 src/lib/platform/CMSWindowsClipboardHTMLConverter.cpp create mode 100644 src/lib/platform/CMSWindowsClipboardHTMLConverter.h create mode 100644 src/lib/platform/CMSWindowsClipboardTextConverter.cpp create mode 100644 src/lib/platform/CMSWindowsClipboardTextConverter.h create mode 100644 src/lib/platform/CMSWindowsClipboardUTF16Converter.cpp create mode 100644 src/lib/platform/CMSWindowsClipboardUTF16Converter.h create mode 100644 src/lib/platform/CMSWindowsDebugOutputter.cpp create mode 100644 src/lib/platform/CMSWindowsDebugOutputter.h create mode 100644 src/lib/platform/CMSWindowsDesks.cpp create mode 100644 src/lib/platform/CMSWindowsDesks.h create mode 100644 src/lib/platform/CMSWindowsEventQueueBuffer.cpp create mode 100644 src/lib/platform/CMSWindowsEventQueueBuffer.h create mode 100644 src/lib/platform/CMSWindowsHookLibraryLoader.cpp create mode 100644 src/lib/platform/CMSWindowsHookLibraryLoader.h create mode 100644 src/lib/platform/CMSWindowsKeyState.cpp create mode 100644 src/lib/platform/CMSWindowsKeyState.h create mode 100644 src/lib/platform/CMSWindowsRelauncher.cpp create mode 100644 src/lib/platform/CMSWindowsRelauncher.h create mode 100644 src/lib/platform/CMSWindowsScreen.cpp create mode 100644 src/lib/platform/CMSWindowsScreen.h create mode 100644 src/lib/platform/CMSWindowsScreenSaver.cpp create mode 100644 src/lib/platform/CMSWindowsScreenSaver.h create mode 100644 src/lib/platform/CMSWindowsUtil.cpp create mode 100644 src/lib/platform/CMSWindowsUtil.h create mode 100644 src/lib/platform/CMSWindowsXInput.cpp create mode 100644 src/lib/platform/CMSWindowsXInput.h create mode 100644 src/lib/platform/CMakeLists.txt create mode 100644 src/lib/platform/COSXClipboard.cpp create mode 100644 src/lib/platform/COSXClipboard.h create mode 100644 src/lib/platform/COSXClipboardAnyTextConverter.cpp create mode 100644 src/lib/platform/COSXClipboardAnyTextConverter.h create mode 100644 src/lib/platform/COSXClipboardTextConverter.cpp create mode 100644 src/lib/platform/COSXClipboardTextConverter.h create mode 100644 src/lib/platform/COSXClipboardUTF16Converter.cpp create mode 100644 src/lib/platform/COSXClipboardUTF16Converter.h create mode 100644 src/lib/platform/COSXEventQueueBuffer.cpp create mode 100644 src/lib/platform/COSXEventQueueBuffer.h create mode 100644 src/lib/platform/COSXKeyState.cpp create mode 100644 src/lib/platform/COSXKeyState.h create mode 100644 src/lib/platform/COSXScreen.cpp create mode 100644 src/lib/platform/COSXScreen.h create mode 100644 src/lib/platform/COSXScreenSaver.cpp create mode 100644 src/lib/platform/COSXScreenSaver.h create mode 100644 src/lib/platform/COSXScreenSaverUtil.h create mode 100644 src/lib/platform/COSXScreenSaverUtil.m create mode 100644 src/lib/platform/CSynergyHook.cpp create mode 100644 src/lib/platform/CSynergyHook.h create mode 100644 src/lib/platform/CXWindowsClipboard.cpp create mode 100644 src/lib/platform/CXWindowsClipboard.h create mode 100644 src/lib/platform/CXWindowsClipboardAnyBitmapConverter.cpp create mode 100644 src/lib/platform/CXWindowsClipboardAnyBitmapConverter.h create mode 100644 src/lib/platform/CXWindowsClipboardBMPConverter.cpp create mode 100644 src/lib/platform/CXWindowsClipboardBMPConverter.h create mode 100644 src/lib/platform/CXWindowsClipboardHTMLConverter.cpp create mode 100644 src/lib/platform/CXWindowsClipboardHTMLConverter.h create mode 100644 src/lib/platform/CXWindowsClipboardTextConverter.cpp create mode 100644 src/lib/platform/CXWindowsClipboardTextConverter.h create mode 100644 src/lib/platform/CXWindowsClipboardUCS2Converter.cpp create mode 100644 src/lib/platform/CXWindowsClipboardUCS2Converter.h create mode 100644 src/lib/platform/CXWindowsClipboardUTF8Converter.cpp create mode 100644 src/lib/platform/CXWindowsClipboardUTF8Converter.h create mode 100644 src/lib/platform/CXWindowsEventQueueBuffer.cpp create mode 100644 src/lib/platform/CXWindowsEventQueueBuffer.h create mode 100644 src/lib/platform/CXWindowsKeyState.cpp create mode 100644 src/lib/platform/CXWindowsKeyState.h create mode 100644 src/lib/platform/CXWindowsScreen.cpp create mode 100644 src/lib/platform/CXWindowsScreen.h create mode 100644 src/lib/platform/CXWindowsScreenSaver.cpp create mode 100644 src/lib/platform/CXWindowsScreenSaver.h create mode 100644 src/lib/platform/CXWindowsUtil.cpp create mode 100644 src/lib/platform/CXWindowsUtil.h create mode 100644 src/lib/platform/HookDLL.cpp create mode 100644 src/lib/platform/HookDLL.h create mode 100644 src/lib/platform/IMSWindowsClipboardFacade.h create mode 100644 src/lib/platform/OSXScreenSaverControl.h create mode 100644 src/lib/platform/XInput13.h create mode 100644 src/lib/platform/XInputHook.cpp create mode 100644 src/lib/platform/XInputHook.h create mode 100644 src/lib/platform/XInputProxy13.cpp create mode 100644 src/lib/platform/XInputProxy13.h create mode 100644 src/lib/server/CBaseClientProxy.cpp create mode 100644 src/lib/server/CBaseClientProxy.h create mode 100644 src/lib/server/CClientListener.cpp create mode 100644 src/lib/server/CClientListener.h create mode 100644 src/lib/server/CClientProxy.cpp create mode 100644 src/lib/server/CClientProxy.h create mode 100644 src/lib/server/CClientProxy1_0.cpp create mode 100644 src/lib/server/CClientProxy1_0.h create mode 100644 src/lib/server/CClientProxy1_1.cpp create mode 100644 src/lib/server/CClientProxy1_1.h create mode 100644 src/lib/server/CClientProxy1_2.cpp create mode 100644 src/lib/server/CClientProxy1_2.h create mode 100644 src/lib/server/CClientProxy1_3.cpp create mode 100644 src/lib/server/CClientProxy1_3.h create mode 100644 src/lib/server/CClientProxy1_4.cpp create mode 100644 src/lib/server/CClientProxy1_4.h create mode 100644 src/lib/server/CClientProxyUnknown.cpp create mode 100644 src/lib/server/CClientProxyUnknown.h create mode 100644 src/lib/server/CConfig.cpp create mode 100644 src/lib/server/CConfig.h create mode 100644 src/lib/server/CInputFilter.cpp create mode 100644 src/lib/server/CInputFilter.h create mode 100644 src/lib/server/CMakeLists.txt create mode 100644 src/lib/server/CPrimaryClient.cpp create mode 100644 src/lib/server/CPrimaryClient.h create mode 100644 src/lib/server/CServer.cpp create mode 100644 src/lib/server/CServer.h create mode 100644 src/lib/synergy/CApp.cpp create mode 100644 src/lib/synergy/CApp.h create mode 100644 src/lib/synergy/CAppUtil.cpp create mode 100644 src/lib/synergy/CAppUtil.h create mode 100644 src/lib/synergy/CAppUtilUnix.cpp create mode 100644 src/lib/synergy/CAppUtilUnix.h create mode 100644 src/lib/synergy/CAppUtilWindows.cpp create mode 100644 src/lib/synergy/CAppUtilWindows.h create mode 100644 src/lib/synergy/CArgsBase.cpp create mode 100644 src/lib/synergy/CArgsBase.h create mode 100644 src/lib/synergy/CClientApp.cpp create mode 100644 src/lib/synergy/CClientApp.h create mode 100644 src/lib/synergy/CClientTaskBarReceiver.cpp create mode 100644 src/lib/synergy/CClientTaskBarReceiver.h create mode 100644 src/lib/synergy/CClipboard.cpp create mode 100644 src/lib/synergy/CClipboard.h create mode 100644 src/lib/synergy/CDaemonApp.cpp create mode 100644 src/lib/synergy/CDaemonApp.h create mode 100644 src/lib/synergy/CEventGameDevice.cpp create mode 100644 src/lib/synergy/CEventGameDevice.h create mode 100644 src/lib/synergy/CGameDevice.cpp create mode 100644 src/lib/synergy/CGameDevice.h create mode 100644 src/lib/synergy/CKeyMap.cpp create mode 100644 src/lib/synergy/CKeyMap.h create mode 100644 src/lib/synergy/CKeyState.cpp create mode 100644 src/lib/synergy/CKeyState.h create mode 100644 src/lib/synergy/CMakeLists.txt create mode 100644 src/lib/synergy/CPacketStreamFilter.cpp create mode 100644 src/lib/synergy/CPacketStreamFilter.h create mode 100644 src/lib/synergy/CPlatformScreen.cpp create mode 100644 src/lib/synergy/CPlatformScreen.h create mode 100644 src/lib/synergy/CProtocolUtil.cpp create mode 100644 src/lib/synergy/CProtocolUtil.h create mode 100644 src/lib/synergy/CScreen.cpp create mode 100644 src/lib/synergy/CScreen.h create mode 100644 src/lib/synergy/CServerApp.cpp create mode 100644 src/lib/synergy/CServerApp.h create mode 100644 src/lib/synergy/CServerTaskBarReceiver.cpp create mode 100644 src/lib/synergy/CServerTaskBarReceiver.h create mode 100644 src/lib/synergy/CVncClient.cpp create mode 100644 src/lib/synergy/CVncClient.h create mode 100644 src/lib/synergy/ClipboardTypes.h create mode 100644 src/lib/synergy/GameDeviceTypes.h create mode 100644 src/lib/synergy/Global.h create mode 100644 src/lib/synergy/IApp.h create mode 100644 src/lib/synergy/IAppUtil.h create mode 100644 src/lib/synergy/IClient.h create mode 100644 src/lib/synergy/IClipboard.cpp create mode 100644 src/lib/synergy/IClipboard.h create mode 100644 src/lib/synergy/IKeyState.cpp create mode 100644 src/lib/synergy/IKeyState.h create mode 100644 src/lib/synergy/INode.h create mode 100644 src/lib/synergy/IPlatformScreen.h create mode 100644 src/lib/synergy/IPrimaryScreen.cpp create mode 100644 src/lib/synergy/IPrimaryScreen.h create mode 100644 src/lib/synergy/IScreen.cpp create mode 100644 src/lib/synergy/IScreen.h create mode 100644 src/lib/synergy/IScreenSaver.h create mode 100644 src/lib/synergy/ISecondaryScreen.cpp create mode 100644 src/lib/synergy/ISecondaryScreen.h create mode 100644 src/lib/synergy/KeyTypes.cpp create mode 100644 src/lib/synergy/KeyTypes.h create mode 100644 src/lib/synergy/MouseTypes.h create mode 100644 src/lib/synergy/OptionTypes.h create mode 100644 src/lib/synergy/ProtocolTypes.cpp create mode 100644 src/lib/synergy/ProtocolTypes.h create mode 100644 src/lib/synergy/XScreen.cpp create mode 100644 src/lib/synergy/XScreen.h create mode 100644 src/lib/synergy/XSynergy.cpp create mode 100644 src/lib/synergy/XSynergy.h create mode 100644 src/plugin/CMakeLists.txt create mode 100644 src/plugin/winmmjoy/CMakeLists.txt create mode 100644 src/plugin/winmmjoy/winmmjoy.cpp create mode 100644 src/plugin/winmmjoy/winmmjoy.h create mode 100644 src/test/CMakeLists.txt create mode 100644 src/test/guitests/guitests.pro create mode 100644 src/test/guitests/src/VersionCheckerTests.cpp create mode 100644 src/test/guitests/src/VersionCheckerTests.h create mode 100644 src/test/guitests/src/main.cpp create mode 100644 src/test/integtests/CMakeLists.txt create mode 100644 src/test/integtests/Main.cpp create mode 100644 src/test/integtests/platform/CMSWindowsClipboardTests.cpp create mode 100644 src/test/integtests/platform/CMSWindowsKeyStateTests.cpp create mode 100644 src/test/integtests/platform/COSXClipboardTests.cpp create mode 100644 src/test/integtests/platform/COSXKeyStateTests.cpp create mode 100644 src/test/integtests/platform/CXWindowsClipboardTests.cpp create mode 100644 src/test/integtests/platform/CXWindowsKeyStateTests.cpp create mode 100644 src/test/integtests/platform/CXWindowsScreenSaverTests.cpp create mode 100644 src/test/integtests/platform/CXWindowsScreenTests.cpp create mode 100644 src/test/unittests/CMakeLists.txt create mode 100644 src/test/unittests/Main.cpp create mode 100644 src/test/unittests/client/CMockClient.h create mode 100644 src/test/unittests/client/CServerProxyTests.cpp create mode 100644 src/test/unittests/io/CMockStream.h create mode 100644 src/test/unittests/synergy/CClipboardTests.cpp create mode 100644 src/test/unittests/synergy/CKeyStateTests.cpp create mode 100644 src/test/unittests/synergy/CKeyStateTests.h create mode 100644 src/test/unittests/synergy/CMockEventQueue.h create mode 100644 src/test/unittests/synergy/CMockKeyMap.h create mode 100644 src/vnc/CMakeLists.txt create mode 100644 src/vnc/LICENCE.txt create mode 100644 src/vnc/common/CMakeLists.txt create mode 100644 src/vnc/common/Makefile.in create mode 100644 src/vnc/common/Xregion/CMakeLists.txt create mode 100644 src/vnc/common/Xregion/Makefile.in create mode 100644 src/vnc/common/Xregion/Region.c create mode 100644 src/vnc/common/Xregion/Xregion.h create mode 100644 src/vnc/common/Xregion/region.h create mode 100644 src/vnc/common/boilerplate.mk create mode 100644 src/vnc/common/configure create mode 100644 src/vnc/common/configure.in create mode 100644 src/vnc/common/depend.mk create mode 100644 src/vnc/common/javabin/index.vnc create mode 100644 src/vnc/common/javabin/logo150x150.gif create mode 100644 src/vnc/common/javabin/vncviewer.jar create mode 100644 src/vnc/common/network/CMakeLists.txt create mode 100644 src/vnc/common/network/Makefile.in create mode 100644 src/vnc/common/network/Socket.h create mode 100644 src/vnc/common/network/TcpSocket.cxx create mode 100644 src/vnc/common/network/TcpSocket.h create mode 100644 src/vnc/common/rdr/CMakeLists.txt create mode 100644 src/vnc/common/rdr/Exception.cxx create mode 100644 src/vnc/common/rdr/Exception.h create mode 100644 src/vnc/common/rdr/FdInStream.cxx create mode 100644 src/vnc/common/rdr/FdInStream.h create mode 100644 src/vnc/common/rdr/FdOutStream.cxx create mode 100644 src/vnc/common/rdr/FdOutStream.h create mode 100644 src/vnc/common/rdr/FixedMemOutStream.h create mode 100644 src/vnc/common/rdr/HexInStream.cxx create mode 100644 src/vnc/common/rdr/HexInStream.h create mode 100644 src/vnc/common/rdr/HexOutStream.cxx create mode 100644 src/vnc/common/rdr/HexOutStream.h create mode 100644 src/vnc/common/rdr/InStream.cxx create mode 100644 src/vnc/common/rdr/InStream.h create mode 100644 src/vnc/common/rdr/Makefile.in create mode 100644 src/vnc/common/rdr/MemInStream.h create mode 100644 src/vnc/common/rdr/MemOutStream.h create mode 100644 src/vnc/common/rdr/OutStream.h create mode 100644 src/vnc/common/rdr/RandomStream.cxx create mode 100644 src/vnc/common/rdr/RandomStream.h create mode 100644 src/vnc/common/rdr/SubstitutingInStream.h create mode 100644 src/vnc/common/rdr/ZlibInStream.cxx create mode 100644 src/vnc/common/rdr/ZlibInStream.h create mode 100644 src/vnc/common/rdr/ZlibOutStream.cxx create mode 100644 src/vnc/common/rdr/ZlibOutStream.h create mode 100644 src/vnc/common/rdr/types.h create mode 100644 src/vnc/common/rfb/Blacklist.cxx create mode 100644 src/vnc/common/rfb/Blacklist.h create mode 100644 src/vnc/common/rfb/CConnection.cxx create mode 100644 src/vnc/common/rfb/CConnection.h create mode 100644 src/vnc/common/rfb/CMakeLists.txt create mode 100644 src/vnc/common/rfb/CMsgHandler.cxx create mode 100644 src/vnc/common/rfb/CMsgHandler.h create mode 100644 src/vnc/common/rfb/CMsgReader.cxx create mode 100644 src/vnc/common/rfb/CMsgReader.h create mode 100644 src/vnc/common/rfb/CMsgReaderV3.cxx create mode 100644 src/vnc/common/rfb/CMsgReaderV3.h create mode 100644 src/vnc/common/rfb/CMsgWriter.cxx create mode 100644 src/vnc/common/rfb/CMsgWriter.h create mode 100644 src/vnc/common/rfb/CMsgWriterV3.cxx create mode 100644 src/vnc/common/rfb/CMsgWriterV3.h create mode 100644 src/vnc/common/rfb/CSecurity.h create mode 100644 src/vnc/common/rfb/CSecurityNone.h create mode 100644 src/vnc/common/rfb/CSecurityVncAuth.cxx create mode 100644 src/vnc/common/rfb/CSecurityVncAuth.h create mode 100644 src/vnc/common/rfb/ColourCube.h create mode 100644 src/vnc/common/rfb/ColourMap.h create mode 100644 src/vnc/common/rfb/ComparingUpdateTracker.cxx create mode 100644 src/vnc/common/rfb/ComparingUpdateTracker.h create mode 100644 src/vnc/common/rfb/Configuration.cxx create mode 100644 src/vnc/common/rfb/Configuration.h create mode 100644 src/vnc/common/rfb/ConnParams.cxx create mode 100644 src/vnc/common/rfb/ConnParams.h create mode 100644 src/vnc/common/rfb/Cursor.cxx create mode 100644 src/vnc/common/rfb/Cursor.h create mode 100644 src/vnc/common/rfb/Decoder.cxx create mode 100644 src/vnc/common/rfb/Decoder.h create mode 100644 src/vnc/common/rfb/Encoder.cxx create mode 100644 src/vnc/common/rfb/Encoder.h create mode 100644 src/vnc/common/rfb/Exception.h create mode 100644 src/vnc/common/rfb/HTTPServer.cxx create mode 100644 src/vnc/common/rfb/HTTPServer.h create mode 100644 src/vnc/common/rfb/HextileDecoder.cxx create mode 100644 src/vnc/common/rfb/HextileDecoder.h create mode 100644 src/vnc/common/rfb/HextileEncoder.cxx create mode 100644 src/vnc/common/rfb/HextileEncoder.h create mode 100644 src/vnc/common/rfb/Hostname.h create mode 100644 src/vnc/common/rfb/ImageGetter.h create mode 100644 src/vnc/common/rfb/InputHandler.h create mode 100644 src/vnc/common/rfb/KeyRemapper.cxx create mode 100644 src/vnc/common/rfb/KeyRemapper.h create mode 100644 src/vnc/common/rfb/LogWriter.cxx create mode 100644 src/vnc/common/rfb/LogWriter.h create mode 100644 src/vnc/common/rfb/Logger.cxx create mode 100644 src/vnc/common/rfb/Logger.h create mode 100644 src/vnc/common/rfb/Logger_file.cxx create mode 100644 src/vnc/common/rfb/Logger_file.h create mode 100644 src/vnc/common/rfb/Logger_stdio.cxx create mode 100644 src/vnc/common/rfb/Logger_stdio.h create mode 100644 src/vnc/common/rfb/Makefile.in create mode 100644 src/vnc/common/rfb/Password.cxx create mode 100644 src/vnc/common/rfb/Password.h create mode 100644 src/vnc/common/rfb/Pixel.h create mode 100644 src/vnc/common/rfb/PixelBuffer.cxx create mode 100644 src/vnc/common/rfb/PixelBuffer.h create mode 100644 src/vnc/common/rfb/PixelFormat.cxx create mode 100644 src/vnc/common/rfb/PixelFormat.h create mode 100644 src/vnc/common/rfb/RREDecoder.cxx create mode 100644 src/vnc/common/rfb/RREDecoder.h create mode 100644 src/vnc/common/rfb/RREEncoder.cxx create mode 100644 src/vnc/common/rfb/RREEncoder.h create mode 100644 src/vnc/common/rfb/RawDecoder.cxx create mode 100644 src/vnc/common/rfb/RawDecoder.h create mode 100644 src/vnc/common/rfb/RawEncoder.cxx create mode 100644 src/vnc/common/rfb/RawEncoder.h create mode 100644 src/vnc/common/rfb/Rect.h create mode 100644 src/vnc/common/rfb/Region.cxx create mode 100644 src/vnc/common/rfb/Region.h create mode 100644 src/vnc/common/rfb/SConnection.cxx create mode 100644 src/vnc/common/rfb/SConnection.h create mode 100644 src/vnc/common/rfb/SDesktop.h create mode 100644 src/vnc/common/rfb/SMsgHandler.cxx create mode 100644 src/vnc/common/rfb/SMsgHandler.h create mode 100644 src/vnc/common/rfb/SMsgReader.cxx create mode 100644 src/vnc/common/rfb/SMsgReader.h create mode 100644 src/vnc/common/rfb/SMsgReaderV3.cxx create mode 100644 src/vnc/common/rfb/SMsgReaderV3.h create mode 100644 src/vnc/common/rfb/SMsgWriter.cxx create mode 100644 src/vnc/common/rfb/SMsgWriter.h create mode 100644 src/vnc/common/rfb/SMsgWriterV3.cxx create mode 100644 src/vnc/common/rfb/SMsgWriterV3.h create mode 100644 src/vnc/common/rfb/SSecurity.h create mode 100644 src/vnc/common/rfb/SSecurityFactoryStandard.cxx create mode 100644 src/vnc/common/rfb/SSecurityFactoryStandard.h create mode 100644 src/vnc/common/rfb/SSecurityNone.h create mode 100644 src/vnc/common/rfb/SSecurityVncAuth.cxx create mode 100644 src/vnc/common/rfb/SSecurityVncAuth.h create mode 100644 src/vnc/common/rfb/ServerCore.cxx create mode 100644 src/vnc/common/rfb/ServerCore.h create mode 100644 src/vnc/common/rfb/Threading.h create mode 100644 src/vnc/common/rfb/Timer.cxx create mode 100644 src/vnc/common/rfb/Timer.h create mode 100644 src/vnc/common/rfb/TransImageGetter.cxx create mode 100644 src/vnc/common/rfb/TransImageGetter.h create mode 100644 src/vnc/common/rfb/TrueColourMap.h create mode 100644 src/vnc/common/rfb/UpdateTracker.cxx create mode 100644 src/vnc/common/rfb/UpdateTracker.h create mode 100644 src/vnc/common/rfb/UserPasswdGetter.h create mode 100644 src/vnc/common/rfb/VNCSConnectionST.cxx create mode 100644 src/vnc/common/rfb/VNCSConnectionST.h create mode 100644 src/vnc/common/rfb/VNCServer.h create mode 100644 src/vnc/common/rfb/VNCServerST.cxx create mode 100644 src/vnc/common/rfb/VNCServerST.h create mode 100644 src/vnc/common/rfb/ZRLEDecoder.cxx create mode 100644 src/vnc/common/rfb/ZRLEDecoder.h create mode 100644 src/vnc/common/rfb/ZRLEEncoder.cxx create mode 100644 src/vnc/common/rfb/ZRLEEncoder.h create mode 100644 src/vnc/common/rfb/d3des.c create mode 100644 src/vnc/common/rfb/d3des.h create mode 100644 src/vnc/common/rfb/encodings.cxx create mode 100644 src/vnc/common/rfb/encodings.h create mode 100644 src/vnc/common/rfb/hextileConstants.h create mode 100644 src/vnc/common/rfb/hextileDecode.h create mode 100644 src/vnc/common/rfb/hextileEncode.h create mode 100644 src/vnc/common/rfb/keysymdef.h create mode 100644 src/vnc/common/rfb/msgTypes.h create mode 100644 src/vnc/common/rfb/rreDecode.h create mode 100644 src/vnc/common/rfb/rreEncode.h create mode 100644 src/vnc/common/rfb/secTypes.cxx create mode 100644 src/vnc/common/rfb/secTypes.h create mode 100644 src/vnc/common/rfb/transInitTempl.h create mode 100644 src/vnc/common/rfb/transTempl.h create mode 100644 src/vnc/common/rfb/util.cxx create mode 100644 src/vnc/common/rfb/util.h create mode 100644 src/vnc/common/rfb/zrleDecode.h create mode 100644 src/vnc/common/rfb/zrleEncode.h create mode 100644 src/vnc/common/zlib/CMakeLists.txt create mode 100644 src/vnc/common/zlib/ChangeLog create mode 100644 src/vnc/common/zlib/FAQ create mode 100644 src/vnc/common/zlib/INDEX create mode 100644 src/vnc/common/zlib/Make_vms.com create mode 100644 src/vnc/common/zlib/Makefile.in create mode 100644 src/vnc/common/zlib/Makefile.riscos create mode 100644 src/vnc/common/zlib/README create mode 100644 src/vnc/common/zlib/adler32.c create mode 100644 src/vnc/common/zlib/algorithm.txt create mode 100644 src/vnc/common/zlib/compress.c create mode 100644 src/vnc/common/zlib/configure create mode 100644 src/vnc/common/zlib/crc32.c create mode 100644 src/vnc/common/zlib/deflate.c create mode 100644 src/vnc/common/zlib/deflate.h create mode 100644 src/vnc/common/zlib/descrip.mms create mode 100644 src/vnc/common/zlib/example.c create mode 100644 src/vnc/common/zlib/gzio.c create mode 100644 src/vnc/common/zlib/infblock.c create mode 100644 src/vnc/common/zlib/infblock.h create mode 100644 src/vnc/common/zlib/infcodes.c create mode 100644 src/vnc/common/zlib/infcodes.h create mode 100644 src/vnc/common/zlib/inffast.c create mode 100644 src/vnc/common/zlib/inffast.h create mode 100644 src/vnc/common/zlib/inffixed.h create mode 100644 src/vnc/common/zlib/inflate.c create mode 100644 src/vnc/common/zlib/inftrees.c create mode 100644 src/vnc/common/zlib/inftrees.h create mode 100644 src/vnc/common/zlib/infutil.c create mode 100644 src/vnc/common/zlib/infutil.h create mode 100644 src/vnc/common/zlib/maketree.c create mode 100644 src/vnc/common/zlib/minigzip.c create mode 100644 src/vnc/common/zlib/trees.c create mode 100644 src/vnc/common/zlib/trees.h create mode 100644 src/vnc/common/zlib/uncompr.c create mode 100644 src/vnc/common/zlib/zconf.h create mode 100644 src/vnc/common/zlib/zlib.3 create mode 100644 src/vnc/common/zlib/zlib.h create mode 100644 src/vnc/common/zlib/zlib.html create mode 100644 src/vnc/common/zlib/zutil.c create mode 100644 src/vnc/common/zlib/zutil.h create mode 100644 src/vnc/unix/Makefile.in create mode 100644 src/vnc/unix/README create mode 100644 src/vnc/unix/README.hpux_aCC create mode 100644 src/vnc/unix/README.hpux_gcc create mode 100644 src/vnc/unix/boilerplate.mk create mode 100644 src/vnc/unix/cleanDeps create mode 100644 src/vnc/unix/common.mk create mode 100644 src/vnc/unix/configure create mode 100644 src/vnc/unix/configure.in create mode 100644 src/vnc/unix/depend.mk create mode 100644 src/vnc/unix/hpux_aCC.patch create mode 100644 src/vnc/unix/hpux_gcc.patch create mode 100644 src/vnc/unix/tx/Makefile.in create mode 100644 src/vnc/unix/tx/TXButton.h create mode 100644 src/vnc/unix/tx/TXCheckbox.h create mode 100644 src/vnc/unix/tx/TXDialog.h create mode 100644 src/vnc/unix/tx/TXEntry.h create mode 100644 src/vnc/unix/tx/TXImage.cxx create mode 100644 src/vnc/unix/tx/TXImage.h create mode 100644 src/vnc/unix/tx/TXLabel.h create mode 100644 src/vnc/unix/tx/TXMenu.cxx create mode 100644 src/vnc/unix/tx/TXMenu.h create mode 100644 src/vnc/unix/tx/TXMsgBox.h create mode 100644 src/vnc/unix/tx/TXScrollbar.cxx create mode 100644 src/vnc/unix/tx/TXScrollbar.h create mode 100644 src/vnc/unix/tx/TXViewport.cxx create mode 100644 src/vnc/unix/tx/TXViewport.h create mode 100644 src/vnc/unix/tx/TXWindow.cxx create mode 100644 src/vnc/unix/tx/TXWindow.h create mode 100644 src/vnc/unix/vncconfig/Makefile.in create mode 100644 src/vnc/unix/vncconfig/QueryConnectDialog.cxx create mode 100644 src/vnc/unix/vncconfig/QueryConnectDialog.h create mode 100644 src/vnc/unix/vncconfig/buildtime.c create mode 100644 src/vnc/unix/vncconfig/vncExt.c create mode 100644 src/vnc/unix/vncconfig/vncExt.h create mode 100644 src/vnc/unix/vncconfig/vncconfig.cxx create mode 100644 src/vnc/unix/vncconfig/vncconfig.man create mode 100644 src/vnc/unix/vncinstall create mode 100644 src/vnc/unix/vncmkdepend/Makefile create mode 100644 src/vnc/unix/vncmkdepend/README create mode 100644 src/vnc/unix/vncmkdepend/cppsetup.c create mode 100644 src/vnc/unix/vncmkdepend/def.h create mode 100644 src/vnc/unix/vncmkdepend/ifparser.c create mode 100644 src/vnc/unix/vncmkdepend/ifparser.h create mode 100644 src/vnc/unix/vncmkdepend/include.c create mode 100644 src/vnc/unix/vncmkdepend/main.c create mode 100644 src/vnc/unix/vncmkdepend/parse.c create mode 100644 src/vnc/unix/vncmkdepend/pr.c create mode 100644 src/vnc/unix/vncpasswd/Makefile.in create mode 100644 src/vnc/unix/vncpasswd/vncpasswd.cxx create mode 100644 src/vnc/unix/vncpasswd/vncpasswd.man create mode 100644 src/vnc/unix/vncserver create mode 100644 src/vnc/unix/vncserver.man create mode 100644 src/vnc/unix/vncviewer/AboutDialog.h create mode 100644 src/vnc/unix/vncviewer/CConn.cxx create mode 100644 src/vnc/unix/vncviewer/CConn.h create mode 100644 src/vnc/unix/vncviewer/DesktopWindow.cxx create mode 100644 src/vnc/unix/vncviewer/DesktopWindow.h create mode 100644 src/vnc/unix/vncviewer/InfoDialog.h create mode 100644 src/vnc/unix/vncviewer/Makefile.in create mode 100644 src/vnc/unix/vncviewer/OptionsDialog.h create mode 100644 src/vnc/unix/vncviewer/PasswdDialog.h create mode 100644 src/vnc/unix/vncviewer/ServerDialog.h create mode 100644 src/vnc/unix/vncviewer/buildtime.c create mode 100644 src/vnc/unix/vncviewer/parameters.h create mode 100644 src/vnc/unix/vncviewer/vncviewer.cxx create mode 100644 src/vnc/unix/vncviewer/vncviewer.man create mode 100644 src/vnc/unix/x0vncserver/Image.cxx create mode 100644 src/vnc/unix/x0vncserver/Image.h create mode 100644 src/vnc/unix/x0vncserver/Makefile.in create mode 100644 src/vnc/unix/x0vncserver/buildtime.c create mode 100644 src/vnc/unix/x0vncserver/x0vncserver.cxx create mode 100644 src/vnc/unix/x0vncserver/x0vncserver.man create mode 100644 src/vnc/unix/xc.patch create mode 100644 src/vnc/unix/xc/config/cf/host.def create mode 100644 src/vnc/unix/xc/config/cf/vnc.def create mode 100644 src/vnc/unix/xc/programs/Xserver/Xvnc.man create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/Imakefile create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/RegionHelper.h create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/XserverDesktop.cc create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/XserverDesktop.h create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/Xvnc/Imakefile create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/Xvnc/buildtime.c create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/Xvnc/xvnc.cc create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/module/Imakefile create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/vncExtInit.cc create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/vncExtInit.h create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/vncHooks.cc create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/vncHooks.h create mode 100644 src/vnc/unix/xc/programs/Xserver/vnc/xf86vncModule.cc create mode 100644 src/vnc/win/CMakeLists.txt create mode 100644 src/vnc/win/logmessages/messages.h create mode 100644 src/vnc/win/logmessages/messages.mc create mode 100644 src/vnc/win/logmessages/messages.rc create mode 100644 src/vnc/win/rfb_win32/AboutDialog.cxx create mode 100644 src/vnc/win/rfb_win32/AboutDialog.h create mode 100644 src/vnc/win/rfb_win32/BitmapInfo.h create mode 100644 src/vnc/win/rfb_win32/CKeyboard.cxx create mode 100644 src/vnc/win/rfb_win32/CKeyboard.h create mode 100644 src/vnc/win/rfb_win32/CMakeLists.txt create mode 100644 src/vnc/win/rfb_win32/CPointer.cxx create mode 100644 src/vnc/win/rfb_win32/CPointer.h create mode 100644 src/vnc/win/rfb_win32/CleanDesktop.cxx create mode 100644 src/vnc/win/rfb_win32/CleanDesktop.h create mode 100644 src/vnc/win/rfb_win32/Clipboard.cxx create mode 100644 src/vnc/win/rfb_win32/Clipboard.h create mode 100644 src/vnc/win/rfb_win32/CompatibleBitmap.h create mode 100644 src/vnc/win/rfb_win32/ComputerName.h create mode 100644 src/vnc/win/rfb_win32/CurrentUser.cxx create mode 100644 src/vnc/win/rfb_win32/CurrentUser.h create mode 100644 src/vnc/win/rfb_win32/DIBSectionBuffer.cxx create mode 100644 src/vnc/win/rfb_win32/DIBSectionBuffer.h create mode 100644 src/vnc/win/rfb_win32/DeviceContext.cxx create mode 100644 src/vnc/win/rfb_win32/DeviceContext.h create mode 100644 src/vnc/win/rfb_win32/DeviceFrameBuffer.cxx create mode 100644 src/vnc/win/rfb_win32/DeviceFrameBuffer.h create mode 100644 src/vnc/win/rfb_win32/Dialog.cxx create mode 100644 src/vnc/win/rfb_win32/Dialog.h create mode 100644 src/vnc/win/rfb_win32/DynamicFn.cxx create mode 100644 src/vnc/win/rfb_win32/DynamicFn.h create mode 100644 src/vnc/win/rfb_win32/EventManager.cxx create mode 100644 src/vnc/win/rfb_win32/EventManager.h create mode 100644 src/vnc/win/rfb_win32/Handle.h create mode 100644 src/vnc/win/rfb_win32/IconInfo.h create mode 100644 src/vnc/win/rfb_win32/IntervalTimer.h create mode 100644 src/vnc/win/rfb_win32/LaunchProcess.cxx create mode 100644 src/vnc/win/rfb_win32/LaunchProcess.h create mode 100644 src/vnc/win/rfb_win32/LocalMem.h create mode 100644 src/vnc/win/rfb_win32/LogicalPalette.h create mode 100644 src/vnc/win/rfb_win32/LowLevelKeyEvents.cxx create mode 100644 src/vnc/win/rfb_win32/LowLevelKeyEvents.h create mode 100644 src/vnc/win/rfb_win32/ModuleFileName.h create mode 100644 src/vnc/win/rfb_win32/MonitorInfo.cxx create mode 100644 src/vnc/win/rfb_win32/MonitorInfo.h create mode 100644 src/vnc/win/rfb_win32/MsgBox.h create mode 100644 src/vnc/win/rfb_win32/MsgWindow.cxx create mode 100644 src/vnc/win/rfb_win32/MsgWindow.h create mode 100644 src/vnc/win/rfb_win32/OSVersion.cxx create mode 100644 src/vnc/win/rfb_win32/OSVersion.h create mode 100644 src/vnc/win/rfb_win32/RegConfig.cxx create mode 100644 src/vnc/win/rfb_win32/RegConfig.h create mode 100644 src/vnc/win/rfb_win32/Registry.cxx create mode 100644 src/vnc/win/rfb_win32/Registry.h create mode 100644 src/vnc/win/rfb_win32/SDisplay.cxx create mode 100644 src/vnc/win/rfb_win32/SDisplay.h create mode 100644 src/vnc/win/rfb_win32/SDisplayCoreDriver.h create mode 100644 src/vnc/win/rfb_win32/SDisplayCorePolling.cxx create mode 100644 src/vnc/win/rfb_win32/SDisplayCorePolling.h create mode 100644 src/vnc/win/rfb_win32/SDisplayCoreWMHooks.cxx create mode 100644 src/vnc/win/rfb_win32/SDisplayCoreWMHooks.h create mode 100644 src/vnc/win/rfb_win32/SInput.cxx create mode 100644 src/vnc/win/rfb_win32/SInput.h create mode 100644 src/vnc/win/rfb_win32/Security.cxx create mode 100644 src/vnc/win/rfb_win32/Security.h create mode 100644 src/vnc/win/rfb_win32/Service.cxx create mode 100644 src/vnc/win/rfb_win32/Service.h create mode 100644 src/vnc/win/rfb_win32/SocketManager.cxx create mode 100644 src/vnc/win/rfb_win32/SocketManager.h create mode 100644 src/vnc/win/rfb_win32/TCharArray.cxx create mode 100644 src/vnc/win/rfb_win32/TCharArray.h create mode 100644 src/vnc/win/rfb_win32/Threading.cxx create mode 100644 src/vnc/win/rfb_win32/Threading.h create mode 100644 src/vnc/win/rfb_win32/TrayIcon.h create mode 100644 src/vnc/win/rfb_win32/TsSessions.cxx create mode 100644 src/vnc/win/rfb_win32/TsSessions.h create mode 100644 src/vnc/win/rfb_win32/WMCursor.cxx create mode 100644 src/vnc/win/rfb_win32/WMCursor.h create mode 100644 src/vnc/win/rfb_win32/WMHooks.cxx create mode 100644 src/vnc/win/rfb_win32/WMHooks.h create mode 100644 src/vnc/win/rfb_win32/WMNotifier.cxx create mode 100644 src/vnc/win/rfb_win32/WMNotifier.h create mode 100644 src/vnc/win/rfb_win32/WMPoller.cxx create mode 100644 src/vnc/win/rfb_win32/WMPoller.h create mode 100644 src/vnc/win/rfb_win32/WMShatter.cxx create mode 100644 src/vnc/win/rfb_win32/WMShatter.h create mode 100644 src/vnc/win/rfb_win32/WMWindowCopyRect.cxx create mode 100644 src/vnc/win/rfb_win32/WMWindowCopyRect.h create mode 100644 src/vnc/win/rfb_win32/Win32Util.cxx create mode 100644 src/vnc/win/rfb_win32/Win32Util.h create mode 100644 src/vnc/win/rfb_win32/keymap.h create mode 100644 src/vnc/win/vnc.suo create mode 100644 src/vnc/win/vncconfig/Authentication.h create mode 100644 src/vnc/win/vncconfig/Connections.h create mode 100644 src/vnc/win/vncconfig/Desktop.h create mode 100644 src/vnc/win/vncconfig/Hooking.h create mode 100644 src/vnc/win/vncconfig/Inputs.h create mode 100644 src/vnc/win/vncconfig/Legacy.cxx create mode 100644 src/vnc/win/vncconfig/Legacy.h create mode 100644 src/vnc/win/vncconfig/PasswordDialog.cxx create mode 100644 src/vnc/win/vncconfig/PasswordDialog.h create mode 100644 src/vnc/win/vncconfig/Sharing.h create mode 100644 src/vnc/win/vncconfig/resource.h create mode 100644 src/vnc/win/vncconfig/vncconfig.cxx create mode 100644 src/vnc/win/vncconfig/vncconfig.exe.manifest create mode 100644 src/vnc/win/vncconfig/vncconfig.ico create mode 100644 src/vnc/win/vncconfig/vncconfig.rc create mode 100644 src/vnc/win/vncviewer/CConn.cxx create mode 100644 src/vnc/win/vncviewer/CConn.h create mode 100644 src/vnc/win/vncviewer/CConnOptions.cxx create mode 100644 src/vnc/win/vncviewer/CConnOptions.h create mode 100644 src/vnc/win/vncviewer/CConnThread.cxx create mode 100644 src/vnc/win/vncviewer/CConnThread.h create mode 100644 src/vnc/win/vncviewer/CMakeLists.txt create mode 100644 src/vnc/win/vncviewer/ConnectingDialog.cxx create mode 100644 src/vnc/win/vncviewer/ConnectingDialog.h create mode 100644 src/vnc/win/vncviewer/ConnectionDialog.cxx create mode 100644 src/vnc/win/vncviewer/ConnectionDialog.h create mode 100644 src/vnc/win/vncviewer/DesktopWindow.cxx create mode 100644 src/vnc/win/vncviewer/DesktopWindow.h create mode 100644 src/vnc/win/vncviewer/InfoDialog.cxx create mode 100644 src/vnc/win/vncviewer/InfoDialog.h create mode 100644 src/vnc/win/vncviewer/ListenServer.h create mode 100644 src/vnc/win/vncviewer/ListenTrayIcon.h create mode 100644 src/vnc/win/vncviewer/MRU.h create mode 100644 src/vnc/win/vncviewer/OptionsDialog.cxx create mode 100644 src/vnc/win/vncviewer/OptionsDialog.h create mode 100644 src/vnc/win/vncviewer/UserPasswdDialog.cxx create mode 100644 src/vnc/win/vncviewer/UserPasswdDialog.h create mode 100644 src/vnc/win/vncviewer/buildTime.cxx create mode 100644 src/vnc/win/vncviewer/cursor1.cur create mode 100644 src/vnc/win/vncviewer/resource.h create mode 100644 src/vnc/win/vncviewer/vncviewer.bmp create mode 100644 src/vnc/win/vncviewer/vncviewer.cxx create mode 100644 src/vnc/win/vncviewer/vncviewer.exe.manifest create mode 100644 src/vnc/win/vncviewer/vncviewer.h create mode 100644 src/vnc/win/vncviewer/vncviewer.ico create mode 100644 src/vnc/win/vncviewer/vncviewer.rc create mode 100644 src/vnc/win/winvnc/AddNewClientDialog.h create mode 100644 src/vnc/win/winvnc/CMakeLists.txt create mode 100644 src/vnc/win/winvnc/JavaViewer.cxx create mode 100644 src/vnc/win/winvnc/JavaViewer.h create mode 100644 src/vnc/win/winvnc/ManagedListener.cxx create mode 100644 src/vnc/win/winvnc/ManagedListener.h create mode 100644 src/vnc/win/winvnc/QueryConnectDialog.cxx create mode 100644 src/vnc/win/winvnc/QueryConnectDialog.h create mode 100644 src/vnc/win/winvnc/STrayIcon.cxx create mode 100644 src/vnc/win/winvnc/STrayIcon.h create mode 100644 src/vnc/win/winvnc/VNCServerService.cxx create mode 100644 src/vnc/win/winvnc/VNCServerService.h create mode 100644 src/vnc/win/winvnc/VNCServerWin32.cxx create mode 100644 src/vnc/win/winvnc/VNCServerWin32.h create mode 100644 src/vnc/win/winvnc/buildTime.cxx create mode 100644 src/vnc/win/winvnc/connected.ico create mode 100644 src/vnc/win/winvnc/resource.h create mode 100644 src/vnc/win/winvnc/winvnc.bmp create mode 100644 src/vnc/win/winvnc/winvnc.cxx create mode 100644 src/vnc/win/winvnc/winvnc.h create mode 100644 src/vnc/win/winvnc/winvnc.ico create mode 100644 src/vnc/win/winvnc/winvnc.rc create mode 100644 src/vnc/win/winvnc/winvnc4.exe.manifest create mode 100644 src/vnc/win/wm_hooks/CMakeLists.txt create mode 100644 src/vnc/win/wm_hooks/resource.h create mode 100644 src/vnc/win/wm_hooks/wm_hooks.aps create mode 100644 src/vnc/win/wm_hooks/wm_hooks.cxx create mode 100644 src/vnc/win/wm_hooks/wm_hooks.def create mode 100644 src/vnc/win/wm_hooks/wm_hooks.h create mode 100644 src/vnc/win/wm_hooks/wm_hooks.rc create mode 100644 tools/build/README.txt create mode 100644 tools/build/__init__.py create mode 100644 tools/build/ftputil.py create mode 100644 tools/build/generators.py create mode 100644 tools/build/toolchain.py create mode 100644 tools/gmock-1.6.0/CHANGES create mode 100644 tools/gmock-1.6.0/CMakeLists.txt create mode 100644 tools/gmock-1.6.0/CONTRIBUTORS create mode 100644 tools/gmock-1.6.0/COPYING create mode 100644 tools/gmock-1.6.0/Makefile.am create mode 100644 tools/gmock-1.6.0/Makefile.in create mode 100644 tools/gmock-1.6.0/README create mode 100644 tools/gmock-1.6.0/aclocal.m4 create mode 100644 tools/gmock-1.6.0/build-aux/config.guess create mode 100644 tools/gmock-1.6.0/build-aux/config.h.in create mode 100644 tools/gmock-1.6.0/build-aux/config.sub create mode 100644 tools/gmock-1.6.0/build-aux/depcomp create mode 100644 tools/gmock-1.6.0/build-aux/install-sh create mode 100644 tools/gmock-1.6.0/build-aux/ltmain.sh create mode 100644 tools/gmock-1.6.0/build-aux/missing create mode 100644 tools/gmock-1.6.0/configure create mode 100644 tools/gmock-1.6.0/configure.ac create mode 100644 tools/gmock-1.6.0/fused-src/gmock-gtest-all.cc create mode 100644 tools/gmock-1.6.0/fused-src/gmock/gmock.h create mode 100644 tools/gmock-1.6.0/fused-src/gmock_main.cc create mode 100644 tools/gmock-1.6.0/fused-src/gtest/gtest.h create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-actions.h create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-cardinalities.h create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-generated-actions.h create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-generated-actions.h.pump create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-generated-function-mockers.h create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-generated-function-mockers.h.pump create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-generated-matchers.h create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-generated-matchers.h.pump create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-generated-nice-strict.h create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-generated-nice-strict.h.pump create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-matchers.h create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-more-actions.h create mode 100644 tools/gmock-1.6.0/include/gmock/gmock-spec-builders.h create mode 100644 tools/gmock-1.6.0/include/gmock/gmock.h create mode 100644 tools/gmock-1.6.0/include/gmock/internal/gmock-generated-internal-utils.h create mode 100644 tools/gmock-1.6.0/include/gmock/internal/gmock-generated-internal-utils.h.pump create mode 100644 tools/gmock-1.6.0/include/gmock/internal/gmock-internal-utils.h create mode 100644 tools/gmock-1.6.0/include/gmock/internal/gmock-port.h create mode 100644 tools/gmock-1.6.0/make/Makefile create mode 100644 tools/gmock-1.6.0/msvc/2005/gmock.sln create mode 100644 tools/gmock-1.6.0/msvc/2005/gmock.vcproj create mode 100644 tools/gmock-1.6.0/msvc/2005/gmock_config.vsprops create mode 100644 tools/gmock-1.6.0/msvc/2005/gmock_main.vcproj create mode 100644 tools/gmock-1.6.0/msvc/2005/gmock_test.vcproj create mode 100644 tools/gmock-1.6.0/msvc/2010/gmock.sln create mode 100644 tools/gmock-1.6.0/msvc/2010/gmock.vcxproj create mode 100644 tools/gmock-1.6.0/msvc/2010/gmock_config.props create mode 100644 tools/gmock-1.6.0/msvc/2010/gmock_main.vcxproj create mode 100644 tools/gmock-1.6.0/msvc/2010/gmock_test.vcxproj create mode 100644 tools/gmock-1.6.0/scripts/fuse_gmock_files.py create mode 100644 tools/gmock-1.6.0/scripts/generator/COPYING create mode 100644 tools/gmock-1.6.0/scripts/generator/README create mode 100644 tools/gmock-1.6.0/scripts/generator/README.cppclean create mode 100644 tools/gmock-1.6.0/scripts/generator/cpp/__init__.py create mode 100644 tools/gmock-1.6.0/scripts/generator/cpp/ast.py create mode 100644 tools/gmock-1.6.0/scripts/generator/cpp/gmock_class.py create mode 100644 tools/gmock-1.6.0/scripts/generator/cpp/keywords.py create mode 100644 tools/gmock-1.6.0/scripts/generator/cpp/tokenize.py create mode 100644 tools/gmock-1.6.0/scripts/generator/cpp/utils.py create mode 100644 tools/gmock-1.6.0/scripts/generator/gmock_gen.py create mode 100644 tools/gmock-1.6.0/scripts/gmock-config.in create mode 100644 tools/gmock-1.6.0/src/gmock-all.cc create mode 100644 tools/gmock-1.6.0/src/gmock-cardinalities.cc create mode 100644 tools/gmock-1.6.0/src/gmock-internal-utils.cc create mode 100644 tools/gmock-1.6.0/src/gmock-matchers.cc create mode 100644 tools/gmock-1.6.0/src/gmock-spec-builders.cc create mode 100644 tools/gmock-1.6.0/src/gmock.cc create mode 100644 tools/gmock-1.6.0/src/gmock_main.cc create mode 100644 tools/gmock-1.6.0/test/gmock-actions_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock-cardinalities_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock-generated-actions_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock-generated-function-mockers_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock-generated-internal-utils_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock-generated-matchers_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock-internal-utils_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock-matchers_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock-more-actions_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock-nice-strict_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock-port_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock-spec-builders_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock_all_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock_leak_test.py create mode 100644 tools/gmock-1.6.0/test/gmock_leak_test_.cc create mode 100644 tools/gmock-1.6.0/test/gmock_link2_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock_link_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock_link_test.h create mode 100644 tools/gmock-1.6.0/test/gmock_output_test.py create mode 100644 tools/gmock-1.6.0/test/gmock_output_test_.cc create mode 100644 tools/gmock-1.6.0/test/gmock_output_test_golden.txt create mode 100644 tools/gmock-1.6.0/test/gmock_test.cc create mode 100644 tools/gmock-1.6.0/test/gmock_test_utils.py create mode 100644 tools/gtest-1.6.0/CHANGES create mode 100644 tools/gtest-1.6.0/CMakeLists.txt create mode 100644 tools/gtest-1.6.0/CONTRIBUTORS create mode 100644 tools/gtest-1.6.0/COPYING create mode 100644 tools/gtest-1.6.0/Makefile.am create mode 100644 tools/gtest-1.6.0/Makefile.in create mode 100644 tools/gtest-1.6.0/README create mode 100644 tools/gtest-1.6.0/aclocal.m4 create mode 100644 tools/gtest-1.6.0/build-aux/config.guess create mode 100644 tools/gtest-1.6.0/build-aux/config.h.in create mode 100644 tools/gtest-1.6.0/build-aux/config.sub create mode 100644 tools/gtest-1.6.0/build-aux/depcomp create mode 100644 tools/gtest-1.6.0/build-aux/install-sh create mode 100644 tools/gtest-1.6.0/build-aux/ltmain.sh create mode 100644 tools/gtest-1.6.0/build-aux/missing create mode 100644 tools/gtest-1.6.0/cmake/internal_utils.cmake create mode 100644 tools/gtest-1.6.0/codegear/gtest.cbproj create mode 100644 tools/gtest-1.6.0/codegear/gtest.groupproj create mode 100644 tools/gtest-1.6.0/codegear/gtest_all.cc create mode 100644 tools/gtest-1.6.0/codegear/gtest_link.cc create mode 100644 tools/gtest-1.6.0/codegear/gtest_main.cbproj create mode 100644 tools/gtest-1.6.0/codegear/gtest_unittest.cbproj create mode 100644 tools/gtest-1.6.0/configure create mode 100644 tools/gtest-1.6.0/configure.ac create mode 100644 tools/gtest-1.6.0/fused-src/gtest/gtest-all.cc create mode 100644 tools/gtest-1.6.0/fused-src/gtest/gtest.h create mode 100644 tools/gtest-1.6.0/fused-src/gtest/gtest_main.cc create mode 100644 tools/gtest-1.6.0/include/gtest/gtest-death-test.h create mode 100644 tools/gtest-1.6.0/include/gtest/gtest-message.h create mode 100644 tools/gtest-1.6.0/include/gtest/gtest-param-test.h create mode 100644 tools/gtest-1.6.0/include/gtest/gtest-param-test.h.pump create mode 100644 tools/gtest-1.6.0/include/gtest/gtest-printers.h create mode 100644 tools/gtest-1.6.0/include/gtest/gtest-spi.h create mode 100644 tools/gtest-1.6.0/include/gtest/gtest-test-part.h create mode 100644 tools/gtest-1.6.0/include/gtest/gtest-typed-test.h create mode 100644 tools/gtest-1.6.0/include/gtest/gtest.h create mode 100644 tools/gtest-1.6.0/include/gtest/gtest_pred_impl.h create mode 100644 tools/gtest-1.6.0/include/gtest/gtest_prod.h create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-death-test-internal.h create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-filepath.h create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-internal.h create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-linked_ptr.h create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-param-util-generated.h create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-param-util-generated.h.pump create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-param-util.h create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-port.h create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-string.h create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-tuple.h create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-tuple.h.pump create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-type-util.h create mode 100644 tools/gtest-1.6.0/include/gtest/internal/gtest-type-util.h.pump create mode 100644 tools/gtest-1.6.0/m4/acx_pthread.m4 create mode 100644 tools/gtest-1.6.0/m4/gtest.m4 create mode 100644 tools/gtest-1.6.0/m4/libtool.m4 create mode 100644 tools/gtest-1.6.0/m4/ltoptions.m4 create mode 100644 tools/gtest-1.6.0/m4/ltsugar.m4 create mode 100644 tools/gtest-1.6.0/m4/ltversion.m4 create mode 100644 tools/gtest-1.6.0/m4/lt~obsolete.m4 create mode 100644 tools/gtest-1.6.0/make/Makefile create mode 100644 tools/gtest-1.6.0/msvc/gtest-md.sln create mode 100644 tools/gtest-1.6.0/msvc/gtest-md.vcproj create mode 100644 tools/gtest-1.6.0/msvc/gtest.sln create mode 100644 tools/gtest-1.6.0/msvc/gtest.vcproj create mode 100644 tools/gtest-1.6.0/msvc/gtest_main-md.vcproj create mode 100644 tools/gtest-1.6.0/msvc/gtest_main.vcproj create mode 100644 tools/gtest-1.6.0/msvc/gtest_prod_test-md.vcproj create mode 100644 tools/gtest-1.6.0/msvc/gtest_prod_test.vcproj create mode 100644 tools/gtest-1.6.0/msvc/gtest_unittest-md.vcproj create mode 100644 tools/gtest-1.6.0/msvc/gtest_unittest.vcproj create mode 100644 tools/gtest-1.6.0/samples/prime_tables.h create mode 100644 tools/gtest-1.6.0/samples/sample1.cc create mode 100644 tools/gtest-1.6.0/samples/sample1.h create mode 100644 tools/gtest-1.6.0/samples/sample10_unittest.cc create mode 100644 tools/gtest-1.6.0/samples/sample1_unittest.cc create mode 100644 tools/gtest-1.6.0/samples/sample2.cc create mode 100644 tools/gtest-1.6.0/samples/sample2.h create mode 100644 tools/gtest-1.6.0/samples/sample2_unittest.cc create mode 100644 tools/gtest-1.6.0/samples/sample3-inl.h create mode 100644 tools/gtest-1.6.0/samples/sample3_unittest.cc create mode 100644 tools/gtest-1.6.0/samples/sample4.cc create mode 100644 tools/gtest-1.6.0/samples/sample4.h create mode 100644 tools/gtest-1.6.0/samples/sample4_unittest.cc create mode 100644 tools/gtest-1.6.0/samples/sample5_unittest.cc create mode 100644 tools/gtest-1.6.0/samples/sample6_unittest.cc create mode 100644 tools/gtest-1.6.0/samples/sample7_unittest.cc create mode 100644 tools/gtest-1.6.0/samples/sample8_unittest.cc create mode 100644 tools/gtest-1.6.0/samples/sample9_unittest.cc create mode 100644 tools/gtest-1.6.0/scripts/fuse_gtest_files.py create mode 100644 tools/gtest-1.6.0/scripts/gen_gtest_pred_impl.py create mode 100644 tools/gtest-1.6.0/scripts/gtest-config.in create mode 100644 tools/gtest-1.6.0/scripts/pump.py create mode 100644 tools/gtest-1.6.0/scripts/test/Makefile create mode 100644 tools/gtest-1.6.0/src/gtest-all.cc create mode 100644 tools/gtest-1.6.0/src/gtest-death-test.cc create mode 100644 tools/gtest-1.6.0/src/gtest-filepath.cc create mode 100644 tools/gtest-1.6.0/src/gtest-internal-inl.h create mode 100644 tools/gtest-1.6.0/src/gtest-port.cc create mode 100644 tools/gtest-1.6.0/src/gtest-printers.cc create mode 100644 tools/gtest-1.6.0/src/gtest-test-part.cc create mode 100644 tools/gtest-1.6.0/src/gtest-typed-test.cc create mode 100644 tools/gtest-1.6.0/src/gtest.cc create mode 100644 tools/gtest-1.6.0/src/gtest_main.cc create mode 100644 tools/gtest-1.6.0/test/gtest-death-test_ex_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-death-test_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-filepath_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-linked_ptr_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-listener_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-message_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-options_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-param-test2_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-param-test_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-param-test_test.h create mode 100644 tools/gtest-1.6.0/test/gtest-port_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-printers_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-test-part_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-tuple_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-typed-test2_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-typed-test_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest-typed-test_test.h create mode 100644 tools/gtest-1.6.0/test/gtest-unittest-api_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest_all_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest_break_on_failure_unittest.py create mode 100644 tools/gtest-1.6.0/test/gtest_break_on_failure_unittest_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_catch_exceptions_test.py create mode 100644 tools/gtest-1.6.0/test/gtest_catch_exceptions_test_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_color_test.py create mode 100644 tools/gtest-1.6.0/test/gtest_color_test_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_env_var_test.py create mode 100644 tools/gtest-1.6.0/test/gtest_env_var_test_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_environment_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest_filter_unittest.py create mode 100644 tools/gtest-1.6.0/test/gtest_filter_unittest_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_help_test.py create mode 100644 tools/gtest-1.6.0/test/gtest_help_test_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_list_tests_unittest.py create mode 100644 tools/gtest-1.6.0/test/gtest_list_tests_unittest_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_main_unittest.cc create mode 100644 tools/gtest-1.6.0/test/gtest_no_test_unittest.cc create mode 100644 tools/gtest-1.6.0/test/gtest_output_test.py create mode 100644 tools/gtest-1.6.0/test/gtest_output_test_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_output_test_golden_lin.txt create mode 100644 tools/gtest-1.6.0/test/gtest_pred_impl_unittest.cc create mode 100644 tools/gtest-1.6.0/test/gtest_prod_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest_repeat_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest_shuffle_test.py create mode 100644 tools/gtest-1.6.0/test/gtest_shuffle_test_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_sole_header_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest_stress_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest_test_utils.py create mode 100644 tools/gtest-1.6.0/test/gtest_throw_on_failure_ex_test.cc create mode 100644 tools/gtest-1.6.0/test/gtest_throw_on_failure_test.py create mode 100644 tools/gtest-1.6.0/test/gtest_throw_on_failure_test_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_uninitialized_test.py create mode 100644 tools/gtest-1.6.0/test/gtest_uninitialized_test_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_unittest.cc create mode 100644 tools/gtest-1.6.0/test/gtest_xml_outfile1_test_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_xml_outfile2_test_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_xml_outfiles_test.py create mode 100644 tools/gtest-1.6.0/test/gtest_xml_output_unittest.py create mode 100644 tools/gtest-1.6.0/test/gtest_xml_output_unittest_.cc create mode 100644 tools/gtest-1.6.0/test/gtest_xml_test_utils.py create mode 100644 tools/gtest-1.6.0/test/production.cc create mode 100644 tools/gtest-1.6.0/test/production.h create mode 100644 tools/gtest-1.6.0/xcode/Config/DebugProject.xcconfig create mode 100644 tools/gtest-1.6.0/xcode/Config/FrameworkTarget.xcconfig create mode 100644 tools/gtest-1.6.0/xcode/Config/General.xcconfig create mode 100644 tools/gtest-1.6.0/xcode/Config/ReleaseProject.xcconfig create mode 100644 tools/gtest-1.6.0/xcode/Config/StaticLibraryTarget.xcconfig create mode 100644 tools/gtest-1.6.0/xcode/Config/TestTarget.xcconfig create mode 100644 tools/gtest-1.6.0/xcode/Resources/Info.plist create mode 100644 tools/gtest-1.6.0/xcode/Samples/FrameworkSample/Info.plist create mode 100644 tools/gtest-1.6.0/xcode/Samples/FrameworkSample/WidgetFramework.xcodeproj/project.pbxproj create mode 100644 tools/gtest-1.6.0/xcode/Samples/FrameworkSample/runtests.sh create mode 100644 tools/gtest-1.6.0/xcode/Samples/FrameworkSample/widget.cc create mode 100644 tools/gtest-1.6.0/xcode/Samples/FrameworkSample/widget.h create mode 100644 tools/gtest-1.6.0/xcode/Samples/FrameworkSample/widget_test.cc create mode 100644 tools/gtest-1.6.0/xcode/Scripts/runtests.sh create mode 100644 tools/gtest-1.6.0/xcode/Scripts/versiongenerate.py create mode 100644 tools/gtest-1.6.0/xcode/gtest.xcodeproj/project.pbxproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4ede8306 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +#Vim backup files +*~ +#Python compiled files +*.pyc + +#Git-svn created .gitignore +/Release +/Debug +/vc90.pdb +/synergy.ncb +/synergy.vcproj.ADOBENET.ssbarnea.user +/bin +/tool +/config.h +/tags + +#doxygen +/doc/doxygen +/doc/doxygen.cfg diff --git a/.lvimrc b/.lvimrc new file mode 100644 index 00000000..5fabcddf --- /dev/null +++ b/.lvimrc @@ -0,0 +1,13 @@ +"Instructions +"Download vim script 411 +"http://www.vim.org/scripts/script.php?script_id=441 +"Install localvimrc.vim to .vim/plugin +" +" Hint: You can disable it asking before sourcing a file by adding this to +" your .vimrc: let g:localvimrc_ask=0 + +set nosmarttab +set noexpandtab +set shiftwidth=8 +set softtabstop=0 +set tabstop=4 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..bf9876de --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,387 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +# Version number for Synergy +set(VERSION_MAJOR 1) +set(VERSION_MINOR 4) +set(VERSION_REV 9) +set(VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_REV}") + +# The check for 2.6 may be too strict (consider lowering). +cmake_minimum_required(VERSION 2.4.7) + +# CMake complains if we don't have this. +if (COMMAND cmake_policy) + cmake_policy(SET CMP0003 NEW) +endif() + +# We're escaping quotes in the Windows version number, because +# for some reason CMake won't do it at config version 2.4.7 +# It seems that this restores the newer behaviour where define +# args are not auto-escaped. +if (COMMAND cmake_policy) + cmake_policy(SET CMP0005 NEW) +endif() + +# First, declare project (important for prerequisite checks). +project(synergy C CXX) + +# put binaries in a different dir to make them easier to find. +set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) +set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib) + +# for unix, put debug files in a separate bin "debug" dir. +# release bin files should stay in the root of the bin dir. +if (CMAKE_GENERATOR STREQUAL "Unix Makefiles") + if (CMAKE_BUILD_TYPE STREQUAL Debug) + set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin/debug) + set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib/debug) + endif() +endif() + +# Set some easy to type variables. +set(root_dir ${CMAKE_SOURCE_DIR}) +set(cmake_dir ${root_dir}/res) +set(bin_dir ${root_dir}/bin) +set(doc_dir ${root_dir}/doc) +set(doc_dir ${root_dir}/doc) + +# Declare libs, so we can use list in linker later. There's probably +# a more elegant way of doing this; with SCons, when you check for the +# lib, it is automatically passed to the linker. +set(libs) + +# Depending on the platform, pass in the required defines. +if (UNIX) + + # For config.h, detect the libraries, functions, etc. + include(CheckIncludeFiles) + include(CheckLibraryExists) + include(CheckFunctionExists) + include(CheckTypeSize) + include(CheckIncludeFileCXX) + include(CheckSymbolExists) + include(CheckCSourceCompiles) + + check_include_file_cxx(istream HAVE_ISTREAM) + check_include_file_cxx(ostream HAVE_OSTREAM) + check_include_file_cxx(sstream HAVE_SSTREAM) + + check_include_files(inttypes.h HAVE_INTTYPES_H) + check_include_files(locale.h HAVE_LOCALE_H) + check_include_files(memory.h HAVE_MEMORY_H) + check_include_files(stdlib.h HAVE_STDLIB_H) + check_include_files(strings.h HAVE_STRINGS_H) + check_include_files(string.h HAVE_STRING_H) + check_include_files(sys/select.h HAVE_SYS_SELECT_H) + check_include_files(sys/socket.h HAVE_SYS_SOCKET_H) + check_include_files(sys/stat.h HAVE_SYS_STAT_H) + check_include_files(sys/time.h HAVE_SYS_TIME_H) + check_include_files(sys/utsname.h HAVE_SYS_UTSNAME_H) + check_include_files(unistd.h HAVE_UNISTD_H) + check_include_files(wchar.h HAVE_WCHAR_H) + + check_function_exists(getpwuid_r HAVE_GETPWUID_R) + check_function_exists(gmtime_r HAVE_GMTIME_R) + check_function_exists(nanosleep HAVE_NANOSLEEP) + check_function_exists(poll HAVE_POLL) + check_function_exists(sigwait HAVE_POSIX_SIGWAIT) + check_function_exists(strftime HAVE_STRFTIME) + check_function_exists(vsnprintf HAVE_VSNPRINTF) + check_function_exists(inet_aton HAVE_INET_ATON) + + # For some reason, the check_function_exists macro doesn't detect + # the inet_aton on some pure Unix platforms (e.g. sunos5). So we + # need to do a more detailed check and also include some extra libs. + if (NOT HAVE_INET_ATON) + + set(CMAKE_REQUIRED_LIBRARIES nsl) + check_c_source_compiles( + "#include \n int main() { inet_aton(0, 0); }" + HAVE_INET_ATON_ADV) + set(CMAKE_REQUIRED_LIBRARIES) + + if (HAVE_INET_ATON_ADV) + + # Override the previous fail. + set(HAVE_INET_ATON 1) + + # Assume that both nsl and socket will be needed, + # it seems safe to add socket on the back of nsl, + # since socket only ever needed when nsl is needed. + list(APPEND libs nsl socket) + + endif() + + endif() + + check_type_size(char SIZEOF_CHAR) + check_type_size(int SIZEOF_INT) + check_type_size(long SIZEOF_LONG) + check_type_size(short SIZEOF_SHORT) + + # pthread is used on both Linux and Mac + check_library_exists("pthread" pthread_create "" HAVE_PTHREAD) + if (HAVE_PTHREAD) + list(APPEND libs pthread) + else (HAVE_PTHREAD) + message(FATAL_ERROR "Missing library: pthread") + endif() + + if (APPLE) + exec_program(uname ARGS -v OUTPUT_VARIABLE DARWIN_VERSION) + string(REGEX MATCH "[0-9]+" DARWIN_VERSION ${DARWIN_VERSION}) + message(STATUS "DARWIN_VERSION=${DARWIN_VERSION}") + if (DARWIN_VERSION LESS 9) + # 10.4: universal (32-bit intel and power pc) + set(CMAKE_OSX_ARCHITECTURES "ppc;i386" + CACHE STRING "" FORCE) + else() + # 10.5+: 32-bit only -- missing funcs in 64-bit os libs + # such as GetGlobalMouse. + set(CMAKE_OSX_ARCHITECTURES "i386" + CACHE STRING "" FORCE) + endif() + + set(CMAKE_CXX_FLAGS "--sysroot ${CMAKE_OSX_SYSROOT} ${CMAKE_CXX_FLAGS}") + + find_library(lib_ScreenSaver ScreenSaver) + find_library(lib_IOKit IOKit) + find_library(lib_ApplicationServices ApplicationServices) + find_library(lib_Foundation Foundation) + find_library(lib_Carbon Carbon) + + list(APPEND libs + ${lib_ScreenSaver} + ${lib_IOKit} + ${lib_ApplicationServices} + ${lib_Foundation} + ${lib_Carbon} + ) + + else() + + # add include dir for bsd (posix uses /usr/include/) + set(CMAKE_INCLUDE_PATH "${CMAKE_INCLUDE_PATH}:/usr/local/include") + + set(XKBlib "X11/XKBlib.h") + check_include_files("${XKBlib};X11/extensions/Xrandr.h" HAVE_X11_EXTENSIONS_XRANDR_H) + check_include_files("${XKBlib};X11/extensions/dpms.h" HAVE_X11_EXTENSIONS_DPMS_H) + check_include_files("X11/extensions/Xinerama.h" HAVE_X11_EXTENSIONS_XINERAMA_H) + check_include_files("${XKBlib};X11/extensions/XKBstr.h" HAVE_X11_EXTENSIONS_XKBSTR_H) + check_include_files("X11/extensions/XKB.h" HAVE_XKB_EXTENSION) + check_include_files("X11/extensions/XTest.h" HAVE_X11_EXTENSIONS_XTEST_H) + check_include_files(${XKBlib} HAVE_X11_XKBLIB_H) + check_include_files("X11/extensions/XInput2.h" HAVE_XI2) + + if (HAVE_X11_EXTENSIONS_DPMS_H) + # Assume that function prototypes declared, when include exists. + set(HAVE_DPMS_PROTOTYPES 1) + endif() + + if (NOT HAVE_X11_XKBLIB_H) + message(FATAL_ERROR "Missing header: " ${XKBlib}) + endif() + + check_library_exists("SM;ICE" IceConnectionNumber "" HAVE_ICE) + check_library_exists("X11;Xext" DPMSQueryExtension "" HAVE_Xext) + check_library_exists("X11;Xext;Xtst" XTestQueryExtension "" HAVE_Xtst) + check_library_exists("Xinerama" XineramaQueryExtension "" HAVE_Xinerama) + check_library_exists("Xi" XISelectEvents "" HAVE_Xi) + check_library_exists("Xrandr" XRRQueryExtension "" HAVE_Xrandr) + + if (HAVE_ICE) + + # Assume we have SM if we have ICE. + set(HAVE_SM 1) + list(APPEND libs SM ICE) + + endif() + + if (HAVE_Xtst) + + # Xtxt depends on X11. + set(HAVE_X11) + list(APPEND libs X11 Xtst) + + else() + + message(FATAL_ERROR "Missing library: Xtst") + + endif() + + if (HAVE_Xext) + list(APPEND libs Xext) + endif() + + if (HAVE_Xinerama) + list(APPEND libs Xinerama) + else (HAVE_Xinerama) + if (HAVE_X11_EXTENSIONS_XINERAMA_H) + message(FATAL_ERROR "Missing library: Xinerama") + endif() + endif() + + if (HAVE_Xrandr) + list(APPEND libs Xrandr) + endif() + + endif() + + IF(HAVE_Xi) + LIST(APPEND libs Xi) + ENDIF() + + # For config.h, set some static values; it may be a good idea to make + # these values dynamic for non-standard UNIX compilers. + set(ACCEPT_TYPE_ARG3 socklen_t) + set(HAVE_CXX_BOOL 1) + set(HAVE_CXX_CASTS 1) + set(HAVE_CXX_EXCEPTIONS 1) + set(HAVE_CXX_MUTABLE 1) + set(HAVE_CXX_STDLIB 1) + set(HAVE_PTHREAD_SIGNAL 1) + set(SELECT_TYPE_ARG1 int) + set(SELECT_TYPE_ARG234 "(fd_set *)") + set(SELECT_TYPE_ARG5 "(struct timeval *)") + set(STDC_HEADERS 1) + set(TIME_WITH_SYS_TIME 1) + set(HAVE_SOCKLEN_T 1) + + # For config.h, save the results based on a template (config.h.in). + configure_file(res/config.h.in ${root_dir}/config.h) + + add_definitions(-DSYSAPI_UNIX=1 -DHAVE_CONFIG_H) + + if (APPLE) + add_definitions(-DWINAPI_CARBON=1 -D_THREAD_SAFE) + else (APPLE) + add_definitions(-DWINAPI_XWINDOWS=1) + endif() + +else (UNIX) + + list(APPEND libs Wtsapi32 Userenv) + + add_definitions( + /DWIN32 + /D_WINDOWS + /D_CRT_SECURE_NO_WARNINGS + /DVERSION=\"${VERSION}\" + ) + + if (MSVC_VERSION EQUAL 1600) + set(SLN_FILENAME "${CMAKE_CURRENT_BINARY_DIR}/synergy.sln") + if (EXISTS "${SLN_FILENAME}" ) + file(APPEND "${SLN_FILENAME}" "\n# This should be regenerated!\n") + endif() + endif() + +endif() + +if (GAME_DEVICE_SUPPORT) + add_definitions(-DGAME_DEVICE_SUPPORT) +endif() + +if (VNC_SUPPORT) + add_definitions(-DVNC_SUPPORT) +endif() + +add_subdirectory(src) + +if (WIN32) + # add /analyze in order to unconver potential bugs in the source code + # Details: http://msdn.microsoft.com/en-us/library/fwkeyyhe.aspx + # add /FR to generate browse information (ncb files) usefull for using IDE + + #define _BIND_TO_CURRENT_CRT_VERSION 1 + #define _BIND_TO_CURRENT_ATL_VERSION 1 + #define _BIND_TO_CURRENT_MFC_VERSION 1 + #define _BIND_TO_CURRENT_OPENMP_VERSION 1 + # next line replaced the previous 4 ones: + #define _BIND_TO_CURRENT_VCLIBS_VERSION 1; + + # compiler: /MP - use multi cores to compile + # added _SECURE_SCL=1 for finding bugs with iterators - http://msdn.microsoft.com/en-us/library/aa985965.aspx + + # common args between all vs builds + set(VS_ARGS "/FR /MP /D _BIND_TO_CURRENT_VCLIBS_VERSION=1 /D _SECURE_SCL=1 ${VS_ARGS_EXTRA}") + + # we may use `cmake -D VS_ARGS_EXTRA="/analyze"` for example to specify + # analyze mode (since we don't always want to use it; e.g. on non-team + # or non-x86 compiler editions where there's no support) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${VS_ARGS}") + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${VS_ARGS}") + + # this line removes "/D NDEBUG" from release, we want them in order to + # find bugs even on release builds. + set(CMAKE_CXX_FLAGS_RELEASE "/MD /O2 /Ob2") + +endif() + +if (CONF_CPACK) + + if (WIN32) + message(FATAL_ERROR "CPack support for Windows has been removed.") + endif() + + if (UNIX) + if (APPLE) + message(FATAL_ERROR "CPack support for Apple has been removed.") + else () + install(FILES bin/synergy + DESTINATION bin + PERMISSIONS + OWNER_READ OWNER_WRITE OWNER_EXECUTE + GROUP_READ GROUP_EXECUTE + WORLD_READ WORLD_EXECUTE) + + # install gnome menu item + install(FILES res/synergy.desktop + DESTINATION share/applications) + install(FILES res/synergy.ico + DESTINATION share/icons) + endif() + endif() + + # The default CPack behaviour is not to append the system processor + # type, which is undesirable in our case, since we want to support + # both 32-bit and 64-bit processors. + set(CPACK_SYSTEM_NAME ${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}) + + set(CPACK_PACKAGE_NAME "synergy") + set(CPACK_PACKAGE_VENDOR "The Synergy Project") + set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Synergy server and client") + set(CPACK_PACKAGE_VERSION_MAJOR ${VERSION_MAJOR}) + set(CPACK_PACKAGE_VERSION_MINOR ${VERSION_MINOR}) + set(CPACK_PACKAGE_VERSION_PATCH ${VERSION_REV}) + set(CPACK_PACKAGE_VERSION ${VERSION}) + set(CPACK_PACKAGE_CONTACT http://synergy-foss.org/) + set(CPACK_RESOURCE_FILE_LICENSE "${cmake_dir}/License.rtf") + set(CPACK_RESOURCE_FILE_README "${cmake_dir}/Readme.txt") + + # Must be last (since it relies of CPACK_ vars). + include(CPack) + +endif() + +if (CONF_DOXYGEN) + + set(VERSION, "${VERSION}") + + # For doxygen.cfg, save the results based on a template (doxygen.cfg.in). + configure_file(${cmake_dir}/doxygen.cfg.in ${doc_dir}/doxygen.cfg) + +endif() diff --git a/COMPILE b/COMPILE new file mode 100644 index 00000000..f5209e31 --- /dev/null +++ b/COMPILE @@ -0,0 +1 @@ +See: http://synergy-foss.org/pm/projects/synergy/wiki/Compiling diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..f8bbd88f --- /dev/null +++ b/COPYING @@ -0,0 +1,283 @@ +synergy -- mouse and keyboard sharing utility +Copyright (C) 2012 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..851815e1 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,56 @@ +1.4.8 +===== + +Bug #143: Cursor on Mac OS X goes to center when inactive +Bug #146: Screen Resize causes problems with moving off right-hand side of screen +Bug #3058: Modifier keys not working on Mac OS X server +Bug #3139: Double click too strict (click, move, click should not count) +Bug #3195: Service install can fail first time +Bug #3196: Wizard buttons not visible +Bug #3197: GUI doesn't take focus after install +Bug #3202: Hook DLL (synrgyhk.dll) is not released +Feature #3143: Setup wizard for first time users +Feature #3145: Check for updates +Feature #3174: Startup mode wizard page +Feature #3184: New service for process management + +1.4.7 +===== + +Bug #3132: GUI hides before successful connection +Bug #3133: Can't un-hide GUI on Mac +Feature #3054: Hide synergy[cs] dock icon (Mac OS X) +Feature #3135: Integrate log into main window +Task #3134: Move hotkey warnings to DEBUG + +1.4.6 +===== + +Bug #155: Build error on FreeBSD (missing sentinel in function call) +Bug #571: Synergy SegFaults with "Unknown Quartz Event type: 0x1d" +Bug #617: xrandr rotation on client confines cursor in wrong area +Bug #642: `synergyc --help` segfaults on sparc64 architecture +Bug #652: Stack overflow in getIDForKey +Bug #1071: Can't copy from the Firefox address bar on Linux +Bug #1662: Copying text from remote computer crashes java programs. +Bug #1731: YouTube can cause server to freeze randomly +Bug #2752: Use SAS for ctrl+alt+del on win7 +Bug #2763: Double-click broken on Mac OS +Bug #2817: Keypad Subtract has wrong keycode on OS X +Bug #2958: GNOME 3 mouse problem (gnome-shell) +Bug #2962: Clipboard not working on mac client +Bug #3063: Segfault in copy buffer +Bug #3066: Server segfault on clipboard paste +Bug #3089: Comma and Period translated wrong when using the NEO2-layout +Bug #3092: Wrong screen rotation detected +Bug #3105: There doesn't seem to be a system tray available. Quitting +Bug #3116: Memory Leak due to the XInput2 patches +Bug #3117: Dual monitors not detected properly anymore +Feature #3073: Re-introduce auto-start GUI (Windows) +Feature #3076: Re-introduce auto-start backend +Feature #3077: Re-introduce hidden on start +Feature #3091: Add option to remap altgr modifier +Feature #3119: Mac OS X secondary screen +Task #2905: Unit tests: Clipboard classes +Task #3072: Downgrade Linux build machines +Task #3090: CXWindowsKeyState integ test args wrong diff --git a/INSTALL b/INSTALL new file mode 100644 index 00000000..94805588 --- /dev/null +++ b/INSTALL @@ -0,0 +1 @@ +See: http://synergy-foss.org/pm/projects/synergy/wiki/Setup diff --git a/README b/README index d26ccf56..9b20263e 100644 --- a/README +++ b/README @@ -1,12 +1,17 @@ -Synergy -======= - -We now commit directly to release branches. The trunk was becoming a dead -weight (as it was always the same as the latest release branch), so we -deleted it. - -Please commit to (and build patches against) a release branch. -e.g. /branches/1.4 - -If you have any questions, please email the dev mailing list. - http://groups.google.com/group/synergy-plus-dev +See: http://synergy-foss.org/pm/projects/synergy/wiki/Readme + +Announcement | 2009-08-04 +========================= +We have recently switched to CMake, to replace Automake. +Plese read the Compiling wiki page for help with CMake: +http://synergy-foss.org/pm/projects/synergy/wiki/Compiling + +Linux/Mac +--------- +Instead of using the traditional GNU style `./configure; make` +commands, you will now need to use CMake. + +Windows +------- +Instead of using the VS2005 and VS2008 directories, you now need +to generate the Visual Studio project files using CMake. diff --git a/configure b/configure new file mode 100755 index 00000000..d2cd98a1 --- /dev/null +++ b/configure @@ -0,0 +1,2 @@ +cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release . + diff --git a/doc/MacReadme.txt b/doc/MacReadme.txt new file mode 100755 index 00000000..38dfe246 --- /dev/null +++ b/doc/MacReadme.txt @@ -0,0 +1,18 @@ +Mac OS X Readme +=============== + +To install on Mac OS X with the .zip distribution (first seen in 1.3.6) you must follow these steps: + + 1. Extract the zip file to any location (usually double click will do this) + 2. Open Terminal, and cd to the extracted directory (e.g. /Users/my-name/Downloads/extracted-dir/) + 3. Change to super user (use the su command) + 4. Copy the binaries to /usr/bin using: cp synergy* /usr/bin + +How to enable the root user in Mac OS X: + http://support.apple.com/kb/ht1528 + +Once the binaries have been copied to /usr/bin, you should follow the configuration guide: + http://synergy2.sourceforge.net/configuration.html + +If you have any problems, see the [[Support]] page: + http://synergy-foss.org/support diff --git a/doc/org.synergy-foss.org.synergyc.plist b/doc/org.synergy-foss.org.synergyc.plist new file mode 100644 index 00000000..f4501b0f --- /dev/null +++ b/doc/org.synergy-foss.org.synergyc.plist @@ -0,0 +1,20 @@ + + + + + + Label + org.synergy-foss.org.synergyc.plist + OnDemand + + ProgramArguments + + /usr/bin/synergyc + + 192.168.0.2 + + RunAtLoad + + + diff --git a/doc/org.synergy-foss.org.synergys.plist b/doc/org.synergy-foss.org.synergys.plist new file mode 100644 index 00000000..94cbb633 --- /dev/null +++ b/doc/org.synergy-foss.org.synergys.plist @@ -0,0 +1,22 @@ + + + + + + Label + org.synergy-foss.org.synergys.plist + OnDemand + + ProgramArguments + + /usr/bin/synergys + --no-daemon + --config + + /Users/snorp/.synergy.conf + + RunAtLoad + + + diff --git a/doc/synergy.conf.example b/doc/synergy.conf.example new file mode 100644 index 00000000..2586dfaf --- /dev/null +++ b/doc/synergy.conf.example @@ -0,0 +1,37 @@ +# sample synergy configuration file +# +# comments begin with the # character and continue to the end of +# line. comments may appear anywhere the syntax permits. + +section: screens + # three hosts named: moe, larry, and curly + moe: + larry: + curly: +end + +section: links + # larry is to the right of moe and curly is above moe + moe: + right = larry + up = curly + + # moe is to the left of larry and curly is above larry. + # note that curly is above both moe and larry and moe + # and larry have a symmetric connection (they're in + # opposite directions of each other). + larry: + left = moe + up = curly + + # larry is below curly. if you move up from moe and then + # down, you'll end up on larry. + curly: + down = larry +end + +section: aliases + # curly is also known as shemp + curly: + shemp +end diff --git a/doc/synergy.conf.example-advanced b/doc/synergy.conf.example-advanced new file mode 100644 index 00000000..771cf534 --- /dev/null +++ b/doc/synergy.conf.example-advanced @@ -0,0 +1,55 @@ +# sample synergy configuration file +# +# comments begin with the # character and continue to the end of +# line. comments may appear anywhere the syntax permits. + +# This example uses 3 computers. A laptop and two desktops (one a mac) +# They are arranged in the following configuration with Desktop1 acting as the server +# Desktop 2 has 3 screens arranged around desktop1 +# +# +--------+ +---------+ +# |Desktop2| |Desktop2 | +# | | | | +# +--------+ +---------+ +# +-------+ +--------+ +---------+ +# |Laptop | |Desktop1| |Desktop2 | +# | | | | | | +# +-------+ +--------+ +---------+ +# +# The laptop comes and goes but that doesn't really affect this configuration + +# The screens section is for the logical or short name of the computers +section: screens + # three computers that are logically named: desktop1, desktop2, and laptop + desktop1: + desktop2: + laptop: +end + +section: links + # larry is to the right of moe and curly is above moe + moe: + right = larry + up = curly + + # moe is to the left of larry and curly is above larry. + # note that curly is above both moe and larry and moe + # and larry have a symmetric connection (they're in + # opposite directions of each other). + larry: + left = moe + up = curly + + # larry is below curly. if you move up from moe and then + # down, you'll end up on larry. + curly: + down = larry +end + +# The aliases section is to map the full names of the computers to their logical names used in the screens section +# One way to find the actual name of a comptuer is to run hostname from a command window +section: aliases + # Laptop is actually known as John-Smiths-MacBook-3.local + desktop2: + John-Smiths-MacBook-3.local +end diff --git a/doc/synergy.conf.example-basic b/doc/synergy.conf.example-basic new file mode 100644 index 00000000..6f3c20d9 --- /dev/null +++ b/doc/synergy.conf.example-basic @@ -0,0 +1,39 @@ +# sample synergy configuration file +# +# comments begin with the # character and continue to the end of +# line. comments may appear anywhere the syntax permits. +# +-------+ +--------+ +---------+ +# |Laptop | |Desktop1| |iMac | +# | | | | | | +# +-------+ +--------+ +---------+ + +section: screens + # three hosts named: Laptop, Desktop1, and iMac + # These are the nice names of the hosts to make it easy to write the config file + # The aliases section below contain the "actual" names of the hosts (their hostnames) + Laptop: + Desktop1: + iMac: +end + +section: links + # iMac is to the right of Desktop1 + # Laptop is to the left of Desktop1 + Desktop1: + right = iMac + left = Laptop + + # Desktop1 is to the right of Laptop + Laptop: + right = Desktop1 + + # Desktop1 is to the left of iMac + iMac: + left = Desktop1 +end + +section: aliases + # The "real" name of iMac is John-Smiths-iMac-3.local. If we wanted we could remove this alias and instead use John-Smiths-iMac-3.local everywhere iMac is above. Hopefully it should be easy to see why using an alias is nicer + iMac: + John-Smiths-iMac-3.local +end diff --git a/doc/synergyc.man b/doc/synergyc.man new file mode 100644 index 00000000..de165a88 --- /dev/null +++ b/doc/synergyc.man @@ -0,0 +1,47 @@ +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.38.2. +.TH SYNERGYC "1" "June 2010" "synergyc 1.5.0, protocol version 1.3" "User Commands" +.SH NAME +synergyc \- manual page for synergyc 1.5.0, protocol version 1.3 +.SH SYNOPSIS +.B synergyc +[\fI--yscroll \fR] [\fI--daemon|--no-daemon\fR] [\fI--name \fR] [\fI--restart|--no-restart\fR] [\fI--debug \fR] \fI\fR +.SH DESCRIPTION +Connect to a synergy mouse/keyboard sharing server. +.TP +\fB\-d\fR, \fB\-\-debug\fR +filter out log messages with priority below level. +level may be: FATAL, ERROR, WARNING, NOTE, INFO, +DEBUG, DEBUGn (1\-5). +.TP +\fB\-n\fR, \fB\-\-name\fR use screen\-name instead the hostname to identify +this screen in the configuration. +.TP +\fB\-1\fR, \fB\-\-no\-restart\fR +do not try to restart on failure. +.PP +* \fB\-\-restart\fR restart the server automatically if it fails. +.TP +\fB\-l\fR \fB\-\-log\fR +write log messages to file. +.TP +\fB\-f\fR, \fB\-\-no\-daemon\fR +run in the foreground. +.PP +* \fB\-\-daemon\fR run as a daemon. +.TP +\fB\-\-yscroll\fR +defines the vertical scrolling delta, which is +.TP +\fB\-h\fR, \fB\-\-help\fR +display this help and exit. +.TP +\fB\-\-version\fR +display version information and exit. +.PP +* marks defaults. +.PP +The server address is of the form: [][:]. The hostname +must be the address or hostname of the server. The port overrides the +default port, 24800. +.SH COPYRIGHT +Copyright \(co 2010 Chris Schoeneman, Nick Bolton, Sorin Sbarnea diff --git a/doc/synergys.man b/doc/synergys.man new file mode 100644 index 00000000..1543ffdd --- /dev/null +++ b/doc/synergys.man @@ -0,0 +1,57 @@ +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.38.2. +.TH SYNERGYS "1" "June 2010" "synergys 1.5.0, protocol version 1.3" "User Commands" +.SH NAME +synergys \- manual page for synergys 1.5.0, protocol version 1.3 +.SH SYNOPSIS +.B synergys +[\fI--address
\fR] [\fI--config \fR] [\fI--daemon|--no-daemon\fR] [\fI--name \fR] [\fI--restart|--no-restart\fR] [\fI--debug \fR] +.SH DESCRIPTION +Start the synergy mouse/keyboard sharing server. +.TP +\fB\-a\fR, \fB\-\-address\fR
+listen for clients on the given address. +.TP +\fB\-c\fR, \fB\-\-config\fR +use the named configuration file instead. +.TP +\fB\-d\fR, \fB\-\-debug\fR +filter out log messages with priority below level. +level may be: FATAL, ERROR, WARNING, NOTE, INFO, +DEBUG, DEBUGn (1\-5). +.TP +\fB\-n\fR, \fB\-\-name\fR use screen\-name instead the hostname to identify +this screen in the configuration. +.TP +\fB\-1\fR, \fB\-\-no\-restart\fR +do not try to restart on failure. +.PP +* \fB\-\-restart\fR restart the server automatically if it fails. +.TP +\fB\-l\fR \fB\-\-log\fR +write log messages to file. +.TP +\fB\-f\fR, \fB\-\-no\-daemon\fR +run in the foreground. +.PP +* \fB\-\-daemon\fR run as a daemon. +.TP +\fB\-h\fR, \fB\-\-help\fR +display this help and exit. +.TP +\fB\-\-version\fR +display version information and exit. +.PP +* marks defaults. +.PP +The argument for \fB\-\-address\fR is of the form: [][:]. The +hostname must be the address or hostname of an interface on the system. +The default is to listen on all interfaces. The port overrides the +default port, 24800. +.PP +If no configuration file pathname is provided then the first of the +following to load successfully sets the configuration: +.IP +$HOME/.synergy.conf +/etc/synergy.conf +.SH COPYRIGHT +Copyright \(co 2010 Chris Schoeneman, Nick Bolton, Sorin Sbarnea diff --git a/hm.cmd b/hm.cmd new file mode 100644 index 00000000..60596e2a --- /dev/null +++ b/hm.cmd @@ -0,0 +1,3 @@ +@echo off +python hm.py %* +exit /b %errorlevel% \ No newline at end of file diff --git a/hm.py b/hm.py new file mode 100644 index 00000000..be126a63 --- /dev/null +++ b/hm.py @@ -0,0 +1,214 @@ +#! /usr/bin/env python + +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +# hm.py: 'Help Me', is a simple wrapper for all build tools. +# +# This script was created for the Synergy project. +# http://synergy-foss.org/ +# +# The idea behind this is to simplify the build system, +# however, it's not a dependancy of building Synergy. +# In other words, you don't need to use this script! +# +# If you don't wish to run this script, simply run: +# cmake . +# make +# This will create an in-source UNIX Makefile. + +import sys, os +sys.path.append('tools') + +# if old build src dir exists, move it, as this will now be used for build +# output. +if os.path.exists('build/toolchain.py'): + print "Removing legacy build dir." + os.rename('build', 'build.old') + +from build import toolchain +from getopt import gnu_getopt + +# minimum required version +requiredMajor = 2 +requiredMinor = 3 + +# options used by all commands +globalOptions = 'v' +globalOptionsLong = ['no-prompts', 'generator=', 'verbose', 'make-gui'] + +# list of valid commands as keys. the values are optarg strings, but most +# are None for now (this is mainly for extensibility) +cmd_opt_dict = { + 'about' : ['', []], + 'setup' : ['g:', []], + 'configure' : ['g:dr', ['debug', 'release', 'game-device', 'vnc', 'mac-sdk=']], + 'build' : ['dr', ['debug', 'release']], + 'clean' : ['dr', ['debug', 'release']], + 'update' : ['', []], + 'install' : ['', []], + 'doxygen' : ['', []], + 'dist' : ['', ['vcredist-dir=', 'qt-dir=']], + 'distftp' : ['', ['host=', 'user=', 'pass=', 'dir=']], + 'kill' : ['', []], + 'usage' : ['', []], + 'revision' : ['', []], + 'reformat' : ['', []], + 'open' : ['', []], + 'genlist' : ['', []], + 'reset' : ['', []], +} + +# aliases to valid commands +cmd_alias_dict = { + 'info' : 'about', + 'help' : 'usage', + 'package' : 'dist', + 'docs' : 'doxygen', + 'make' : 'build', + 'cmake' : 'configure', +} + +def complete_command(arg): + completions = [] + + for cmd, optarg in cmd_opt_dict.iteritems(): + # if command was matched fully, return only this, so that + # if `dist` is typed, it will return only `dist` and not + # `dist` and `distftp` for example. + if cmd == arg: + return [cmd,] + if cmd.startswith(arg): + completions.append(cmd) + + for alias, cmd in cmd_alias_dict.iteritems(): + # don't know if this will work just like above, but it's + # probably worth adding. + if alias == arg: + return [alias,] + if alias.startswith(arg): + completions.append(alias) + + return completions + +def start_cmd(argv): + + cmd_arg = '' + if len(argv) > 1: + cmd_arg = argv[1] + + # change common help args to help command + if cmd_arg in ('--help', '-h', '--usage', '-u', '/?'): + cmd_arg = 'usage' + + completions = complete_command(cmd_arg) + + if cmd_arg and len(completions) > 0: + + if len(completions) == 1: + + # get the only completion (since in this case we have 1) + cmd = completions[0] + + # build up the first part of the map (for illustrative purposes) + cmd_map = list() + if cmd_arg != cmd: + cmd_map.append(cmd_arg) + cmd_map.append(cmd) + + # map an alias to the command, and build up the map + if cmd in cmd_alias_dict.keys(): + alias = cmd + if cmd_arg == cmd: + cmd_map.append(alias) + cmd = cmd_alias_dict[cmd] + cmd_map.append(cmd) + + # show command map to avoid confusion + if len(cmd_map) != 0: + print 'Mapping command: %s' % ' -> '.join(cmd_map) + + run_cmd(cmd, argv[2:]) + + return 0 + + else: + print ( + 'Command `%s` too ambiguous, ' + 'could mean any of: %s' + ) % (cmd_arg, ', '.join(completions)) + else: + + if len(argv) == 1: + print 'No command specified, showing usage.\n' + else: + print 'Command not recognised: %s\n' % cmd_arg + + run_cmd('usage') + + # generic error code if not returned sooner + return 1 + +def run_cmd(cmd, argv = []): + + verbose = False + try: + options_pair = cmd_opt_dict[cmd] + + options = globalOptions + options_pair[0] + + options_long = [] + options_long.extend(globalOptionsLong) + options_long.extend(options_pair[1]) + + opts, args = gnu_getopt(argv, options, options_long) + + for o, a in opts: + if o in ('-v', '--verbose'): + verbose = True + + # pass args and optarg data to command handler, which figures out + # how to handle the arguments + handler = toolchain.CommandHandler(argv, opts, args, verbose) + + # use reflection to get the function pointer + cmd_func = getattr(handler, cmd) + + cmd_func() + except: + if not verbose: + # print friendly error for users + sys.stderr.write('Error: ' + sys.exc_info()[1].__str__() + '\n') + sys.exit(1) + else: + # if user wants to be verbose let python do it's thing + raise + +def main(argv): + + if sys.version_info < (requiredMajor, requiredMinor): + print ('Python version must be at least ' + + str(requiredMajor) + '.' + str(requiredMinor) + ', but is ' + + str(sys.version_info[0]) + '.' + str(sys.version_info[1])) + sys.exit(1) + + try: + start_cmd(argv) + except KeyboardInterrupt: + print '\n\nUser aborted, exiting.' + +# Start the program. +main(sys.argv) + diff --git a/hm.sh b/hm.sh new file mode 100755 index 00000000..eb7da8b4 --- /dev/null +++ b/hm.sh @@ -0,0 +1,3 @@ +#! /bin/bash + +python hm.py "$@" diff --git a/res/DefineIfExist.nsh b/res/DefineIfExist.nsh new file mode 100644 index 00000000..b23a8c88 --- /dev/null +++ b/res/DefineIfExist.nsh @@ -0,0 +1,16 @@ +!macro !defineifexist _VAR_NAME _FILE_NAME + !tempfile _TEMPFILE + !ifdef NSIS_WIN32_MAKENSIS + ; Windows - cmd.exe + !system 'if exist "${_FILE_NAME}" echo !define ${_VAR_NAME} > "${_TEMPFILE}"' + !else + ; Posix - sh + !system 'if [ -e "${_FILE_NAME}" ]; then echo "!define ${_VAR_NAME}" > "${_TEMPFILE}"; fi' + !endif + !include '${_TEMPFILE}' + !delfile '${_TEMPFILE}' + !undef _TEMPFILE +!macroend +!define !defineifexist "!insertmacro !defineifexist" + +${!defineifexist} gameDeviceSupport "${binDir}\Release\synxinhk.dll" diff --git a/res/Installer.nsi.in b/res/Installer.nsi.in new file mode 100644 index 00000000..59ee3294 --- /dev/null +++ b/res/Installer.nsi.in @@ -0,0 +1,213 @@ +; template variables +!define version ${in:version} +!define arch ${in:arch} +!define vcRedistDir ${in:vcRedistDir} +!define qtDir ${in:qtDir} +!define installDirVar ${in:installDirVar} + +; normal variables +!define product "Synergy" +!define productOld "Synergy+" +!define packageName "synergy" +!define packageNameOld "synergy-plus" +!define platform "Windows" +!define publisher "The Synergy Project" +!define publisherOld "The Synergy+ Project" +!define helpUrl "http://synergy-foss.org/support" +!define vcRedistFile "vcredist_${arch}.exe" +!define startMenuApp "synergy.exe" +!define binDir "..\bin" +!define uninstall "uninstall.exe" +!define icon "..\res\synergy.ico" +!define controlPanelReg "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" + +!define MUI_ICON ${icon} +!define MUI_UNICON ${icon} + +!include "MUI2.nsh" + +!addincludedir ..\res +!include "DefineIfExist.nsh" + +!insertmacro MUI_PAGE_LICENSE "..\\res\\License.rtf" +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES + +!insertmacro MUI_UNPAGE_WELCOME +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" + +Name ${product} +OutFile "..\bin\${packageName}-${version}-${platform}-${arch}.exe" +InstallDir "${installDirVar}\${product}" +InstallDirRegKey HKEY_LOCAL_MACHINE "SOFTWARE\${product}" "" + +; delete files we installed, and then dir if it's empty +!macro DeleteFiles dir + + Delete "${dir}\synergy.exe" + Delete "${dir}\synergyc.exe" + Delete "${dir}\synergys.exe" + Delete "${dir}\synergyd.exe" + Delete "${dir}\synergyd.log" + Delete "${dir}\launcher.exe" + Delete "${dir}\synrgyhk.dll" + Delete "${dir}\libgcc_s_dw2-1.dll" + Delete "${dir}\mingwm10.dll" + Delete "${dir}\QtCore4.dll" + Delete "${dir}\QtGui4.dll" + Delete "${dir}\QtNetwork4.dll" + Delete "${dir}\Uninstall.exe" + Delete "${dir}\uninstall.exe" + Delete "${dir}\synxinhk.dll" + Delete "${dir}\sxinpx13.dll" + RMDir "${dir}" + +!macroend + +Section + + SetShellVarContext all + SetOutPath "$INSTDIR" + + ; stops and removes all services (including legacy) + ExecWait "$INSTDIR\synergyd.exe /uninstall" + + ; force kill all synergy processes + nsExec::Exec "taskkill /f /im synergy.exe" + nsExec::Exec "taskkill /f /im qsynergy.exe" + nsExec::Exec "taskkill /f /im launcher.exe" + nsExec::Exec "taskkill /f /im synergys.exe" + nsExec::Exec "taskkill /f /im synergyc.exe" + nsExec::Exec "taskkill /f /im synergyd.exe" + + ; clean up legacy files that may exist (but leave user files) + !insertmacro DeleteFiles "$PROGRAMFILES32\${product}\bin" + !insertmacro DeleteFiles "$PROGRAMFILES64\${product}\bin" + !insertmacro DeleteFiles "$PROGRAMFILES32\${productOld}\bin" + !insertmacro DeleteFiles "$PROGRAMFILES64\${productOld}\bin" + !insertmacro DeleteFiles "$PROGRAMFILES32\${product}" + !insertmacro DeleteFiles "$PROGRAMFILES64\${product}" + !insertmacro DeleteFiles "$PROGRAMFILES32\${productOld}" + !insertmacro DeleteFiles "$PROGRAMFILES64\${productOld}" + + ; clean up legacy start menu entries + RMDir /R "$SMPROGRAMS\${product}" + RMDir /R "$SMPROGRAMS\${productOld}" + + ; always delete any existing uninstall info + DeleteRegKey HKLM "${controlPanelReg}\${product}" + DeleteRegKey HKLM "${controlPanelReg}\${productOld}" + DeleteRegKey HKLM "${controlPanelReg}\${publisher}" + DeleteRegKey HKLM "${controlPanelReg}\${publisherOld}" + DeleteRegKey HKLM "${controlPanelReg}\${packageNameOld}" + DeleteRegKey HKLM "SOFTWARE\${product}" + DeleteRegKey HKLM "SOFTWARE\${productOld}" + DeleteRegKey HKLM "SOFTWARE\${publisher}" + DeleteRegKey HKLM "SOFTWARE\${publisherOld}" + + ; create uninstaller (used for control panel icon) + WriteUninstaller "$INSTDIR\${uninstall}" + + ; add new uninstall info + WriteRegStr HKLM "${controlPanelReg}\${product}" "" $INSTDIR + WriteRegStr HKLM "${controlPanelReg}\${product}" "DisplayName" "${product}" + WriteRegStr HKLM "${controlPanelReg}\${product}" "DisplayVersion" "${version}" + WriteRegStr HKLM "${controlPanelReg}\${product}" "DisplayIcon" "$INSTDIR\uninstall.exe" + WriteRegStr HKLM "${controlPanelReg}\${product}" "Publisher" "${publisher}" + WriteRegStr HKLM "${controlPanelReg}\${product}" "UninstallString" "$INSTDIR\uninstall.exe" + WriteRegStr HKLM "${controlPanelReg}\${product}" "URLInfoAbout" "${helpUrl}" + +SectionEnd + +Section "Visual C++ Redistributable" vcredist + + ; this must run first, as some sections run + ; binaries that require a vcredist to be installed. + ; copy redist file, run it, then delete when done + File "${vcRedistDir}\${vcRedistFile}" + ExecWait "$INSTDIR\${vcRedistFile} /install /q /norestart" + Delete $INSTDIR\${vcRedistFile} + +SectionEnd + +Section "Server and Client" core + + ; client and server files + File "${binDir}\Release\synergys.exe" + File "${binDir}\Release\synergyc.exe" + File "${binDir}\Release\synrgyhk.dll" + File "${binDir}\Release\synergyd.exe" + + ; install and run the service + ExecWait "$INSTDIR\synergyd.exe /install" + +SectionEnd + +!ifdef gameDeviceSupport +Section "Game Device Support" gamedev + + ; files for xinput support + File "${binDir}\Release\synxinhk.dll" + File "${binDir}\Release\sxinpx13.dll" + +SectionEnd +!endif + +Section "Graphical User Interface" gui + + ; gui and qt libs + File "${binDir}\Release\synergy.exe" + File "${qtDir}\qt\bin\libgcc_s_dw2-1.dll" + File "${qtDir}\qt\bin\mingwm10.dll" + File "${qtDir}\qt\bin\QtGui4.dll" + File "${qtDir}\qt\bin\QtCore4.dll" + File "${qtDir}\qt\bin\QtNetwork4.dll" + + ; gui start menu shortcut + SetShellVarContext all + CreateShortCut "$SMPROGRAMS\${product}.lnk" "$INSTDIR\${startMenuApp}" + +SectionEnd + +Section Uninstall + + SetShellVarContext all + + ; stop and uninstall the service + ExecWait "$INSTDIR\synergyd.exe /uninstall" + + ; force kill all synergy processes + nsExec::Exec "taskkill /f /im synergy.exe" + nsExec::Exec "taskkill /f /im qsynergy.exe" + nsExec::Exec "taskkill /f /im launcher.exe" + nsExec::Exec "taskkill /f /im synergys.exe" + nsExec::Exec "taskkill /f /im synergyc.exe" + nsExec::Exec "taskkill /f /im synergyd.exe" + + ; delete start menu shortcut + Delete "$SMPROGRAMS\${product}.lnk" + + ; delete all registry keys + DeleteRegKey HKLM "SOFTWARE\${product}" + DeleteRegKey HKLM "${controlPanelReg}\${product}" + + ; note: edit macro to delete more files. + !insertmacro DeleteFiles $INSTDIR + Delete "$INSTDIR\${uninstall}" + + ; delete (only if empty, so we don't delete user files) + RMDir "$INSTDIR" + +SectionEnd + +Function .onInstSuccess + + ; start the GUI automatically. + Exec "$INSTDIR\synergy.exe" + + ; HACK: wait 5 secs for the GUI to take focus. + Sleep 5000 + +FunctionEnd \ No newline at end of file diff --git a/res/License.rtf b/res/License.rtf new file mode 100644 index 00000000..ea1602df --- /dev/null +++ b/res/License.rtf @@ -0,0 +1,102 @@ +{\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\froman\fcharset0 Times-Roman;} +{\colortbl;\red255\green255\blue255;} +{\info +{\title Original file was gpl-2.0.tex} +{\doccomm Created using latex2rtf 1.9.19a on Sun Jul 12 19:21:22 2009}}\paperw12280\paperh15900\margl2680\margr2700\margb1760\margt2540\vieww12280\viewh15900\viewkind1 +\deftab720 +\pard\pardeftab720\ri0\qj + +\f0\fs24 \cf0 \ +\pard\pardeftab720\ri0\qc + +\f1\fs30 \cf0 GNU GENERAL PUBLIC LICENSE +\f0\fs24 \ +\ +\ +\pard\pardeftab720\ri0\qc + +\f1 \cf0 Version 2, June 1991\ +\ +\ +Copyright \'a9 1989, 1991 Free Software Foundation, Inc.\ +\pard\pardeftab720\ri0\sb240\qc +\cf0 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA\ +\pard\pardeftab720\ri0\qc +\cf0 Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. \ +\pard\pardeftab720\ri0\qc + +\b\fs26 \cf0 Preamble +\b0\fs24 \ +\pard\pardeftab720\ri0\qj +\cf0 The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software\'97to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation\'92s software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too.\ +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.\ +To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.\ +For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.\ +We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.\ +Also, for each author\'92s protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors\'92 reputations.\ +Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone\'92s free use or not licensed at all.\ +The precise terms and conditions for copying, distribution and modification follow.\ +\pard\pardeftab720\ri0\qc + +\fs31 \cf0 Terms and Conditions For Copying, Distribution and Modification +\fs24 \ +\pard\pardeftab720\li600\fi-300\ri0\sb50\qj +\cf0 1. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The \'93Program\'94, below, refers to any such program or work, and a \'93work based on the Program\'94 means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term \'93modification\'94.) Each licensee is addressed as \'93you\'94.\ +\pard\pardeftab720\li600\ri0\qj +\cf0 Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.\ +\pard\pardeftab720\li600\fi-300\ri0\sb50\qj +\cf0 2. You may copy and distribute verbatim copies of the Program\'92s source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.\ +\pard\pardeftab720\li600\ri0\qj +\cf0 You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.\ +\pard\pardeftab720\li600\fi-300\ri0\sb50\qj +\cf0 3. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:\ +\pard\pardeftab720\li1200\fi-300\ri0\sb50\qj +\cf0 (a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.\ +(b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.\ +(c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)\ +\pard\pardeftab720\li600\ri0\sb100\qj +\cf0 These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.\ +\pard\pardeftab720\li600\fi-300\ri0\qj +\cf0 Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.\ +In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.\ +\pard\pardeftab720\li600\fi-300\ri0\sb50\qj +\cf0 4. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:\ +\pard\pardeftab720\li1200\fi-300\ri0\sb50\qj +\cf0 (a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,\ +(b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,\ +(c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)\ +\pard\pardeftab720\li600\ri0\sb100\qj +\cf0 The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.\ +\pard\pardeftab720\li600\fi-300\ri0\qj +\cf0 If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.\ +\pard\pardeftab720\li600\fi-300\ri0\sb50\qj +\cf0 5. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.\ +6. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.\ +7. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients\'92 exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.\ +8. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.\ +\pard\pardeftab720\li600\ri0\qj +\cf0 If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.\ +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.\ +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.\ +\pard\pardeftab720\li600\fi-300\ri0\sb50\qj +\cf0 9. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.\ +10. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.\ +\pard\pardeftab720\li600\ri0\qj +\cf0 Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and \'93any later version\'94, you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.\ +\pard\pardeftab720\li600\fi-300\ri0\sb50\qj +\cf0 11. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.\ +\pard\pardeftab720\li600\ri0\qc + +\fs31 \cf0 No Warranty +\fs24 \ +\pard\pardeftab720\li600\fi-300\ri0\sb50\qj +\cf0 12. Because the program is licensed free of charge, there is no warranty for the program, to the extent permitted by applicable law. Except when otherwise stated in writing the copyright holders and/or other parties provide the program \'93as is\'94 without warranty of any kind, either expressed or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The entire risk as to the quality and performance of the program is with you. Should the program prove defective, you assume the cost of all necessary servicing, repair or correction. +\f0 \ + +\f1 13. In no event unless required by applicable law or agreed to in writing will any copyright holder, or any other party who may modify and/or redistribute the program as permitted above, be liable to you for damages, including any general, special, incidental or consequential damages arising out of the use or inability to use the program (including but not limited to loss of data or data being rendered inaccurate or losses sustained by you or third parties or a failure of the program to operate with any other programs), even if such holder or other party has been advised of the possibility of such damages. +\f0 \ +\pard\pardeftab720\ri0\sb100\qc + +\f1\fs31 \cf0 End of Terms and Conditions +\fs24 } \ No newline at end of file diff --git a/res/License.tex b/res/License.tex new file mode 100644 index 00000000..560aa9c3 --- /dev/null +++ b/res/License.tex @@ -0,0 +1,422 @@ +\documentclass[11pt]{article} + +\title{GNU GENERAL PUBLIC LICENSE} +\date{Version 2, June 1991} + +\begin{document} +\maketitle + +\begin{center} +{\parindent 0in + +Copyright \copyright\ 1989, 1991 Free Software Foundation, Inc. + +\bigskip + +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +\bigskip + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. +} +\end{center} + +\begin{center} +{\bf\large Preamble} +\end{center} + + +The licenses for most software are designed to take away your freedom to +share and change it. By contrast, the GNU General Public License is +intended to guarantee your freedom to share and change free software---to +make sure the software is free for all its users. This General Public +License applies to most of the Free Software Foundation's software and to +any other program whose authors commit to using it. (Some other Free +Software Foundation software is covered by the GNU Library General Public +License instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. +Our General Public Licenses are designed to make sure that you have the +freedom to distribute copies of free software (and charge for this service +if you wish), that you receive source code or can get it if you want it, +that you can change the software or use pieces of it in new free programs; +and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These +restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And +you must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its +recipients to know that what they have is not the original, so that any +problems introduced by others will not reflect on the original authors' +reputations. + +Finally, any free program is threatened constantly by software patents. +We wish to avoid the danger that redistributors of a free program will +individually obtain patent licenses, in effect making the program +proprietary. To prevent this, we have made it clear that any patent must +be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and +modification follow. + +\begin{center} +{\Large \sc Terms and Conditions For Copying, Distribution and + Modification} +\end{center} + + +%\renewcommand{\theenumi}{\alpha{enumi}} +\begin{enumerate} + +\addtocounter{enumi}{-1} + +\item + +This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the +terms of this General Public License. The ``Program'', below, refers to +any such program or work, and a ``work based on the Program'' means either +the Program or any derivative work under copyright law: that is to say, a +work containing the Program or a portion of it, either verbatim or with +modifications and/or translated into another language. (Hereinafter, +translation is included without limitation in the term ``modification''.) +Each licensee is addressed as ``you''. + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + +\item You may copy and distribute verbatim copies of the Program's source + code as you receive it, in any medium, provided that you conspicuously + and appropriately publish on each copy an appropriate copyright notice + and disclaimer of warranty; keep intact all the notices that refer to + this License and to the absence of any warranty; and give any other + recipients of the Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +\item + +You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + +\begin{enumerate} + +\item + +You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +\item + +You must cause any work that you distribute or publish, that in +whole or in part contains or is derived from the Program or any +part thereof, to be licensed as a whole at no charge to all third +parties under the terms of this License. + +\item +If the modified program normally reads commands interactively +when run, you must cause it, when started running for such +interactive use in the most ordinary way, to print or display an +announcement including an appropriate copyright notice and a +notice that there is no warranty (or else, saying that you provide +a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this +License. (Exception: if the Program itself is interactive but +does not normally print such an announcement, your work based on +the Program is not required to print an announcement.) + +\end{enumerate} + + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + +\item +You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + +\begin{enumerate} + +\item + +Accompany it with the complete corresponding machine-readable +source code, which must be distributed under the terms of Sections +1 and 2 above on a medium customarily used for software interchange; or, + +\item + +Accompany it with a written offer, valid for at least three +years, to give any third party, for a charge no more than your +cost of physically performing source distribution, a complete +machine-readable copy of the corresponding source code, to be +distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +\item + +Accompany it with the information you received as to the offer +to distribute corresponding source code. (This alternative is +allowed only for noncommercial distribution and only if you +received the program in object code or executable form with such +an offer, in accord with Subsection b above.) + +\end{enumerate} + + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + +\item +You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + +\item +You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +\item +Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + +\item +If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + +\item +If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + +\item +The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and ``any +later version'', you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + +\item +If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + +\begin{center} +{\Large\sc +No Warranty +} +\end{center} + +\item +{\sc Because the program is licensed free of charge, there is no warranty +for the program, to the extent permitted by applicable law. Except when +otherwise stated in writing the copyright holders and/or other parties +provide the program ``as is'' without warranty of any kind, either expressed +or implied, including, but not limited to, the implied warranties of +merchantability and fitness for a particular purpose. The entire risk as +to the quality and performance of the program is with you. Should the +program prove defective, you assume the cost of all necessary servicing, +repair or correction.} + +\item +{\sc In no event unless required by applicable law or agreed to in writing +will any copyright holder, or any other party who may modify and/or +redistribute the program as permitted above, be liable to you for damages, +including any general, special, incidental or consequential damages arising +out of the use or inability to use the program (including but not limited +to loss of data or data being rendered inaccurate or losses sustained by +you or third parties or a failure of the program to operate with any other +programs), even if such holder or other party has been advised of the +possibility of such damages.} + +\end{enumerate} + + +\begin{center} +{\Large\sc End of Terms and Conditions} +\end{center} + + +\pagebreak[2] + +\section*{Appendix: How to Apply These Terms to Your New Programs} + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + + To do so, attach the following notices to the program. It is safest to + attach them to the start of each source file to most effectively convey + the exclusion of warranty; and each file should have at least the + ``copyright'' line and a pointer to where the full notice is found. + +\begin{quote} +one line to give the program's name and a brief idea of what it does. \\ +Copyright (C) yyyy name of author \\ + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +\end{quote} + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + +\begin{quote} +Gnomovision version 69, Copyright (C) yyyy name of author \\ +Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. \\ +This is free software, and you are welcome to redistribute it +under certain conditions; type `show c' for details. +\end{quote} + + +The hypothetical commands {\tt show w} and {\tt show c} should show the +appropriate parts of the General Public License. Of course, the commands +you use may be called something other than {\tt show w} and {\tt show c}; +they could even be mouse-clicks or menu items---whatever suits your +program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a ``copyright disclaimer'' for the program, if +necessary. Here is a sample; alter the names: + +\begin{quote} +Yoyodyne, Inc., hereby disclaims all copyright interest in the program \\ +`Gnomovision' (which makes passes at compilers) written by James Hacker. \\ + +signature of Ty Coon, 1 April 1989 \\ +Ty Coon, President of Vice +\end{quote} + + +This General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications +with the library. If this is what you want to do, use the GNU Library +General Public License instead of this License. + +\end{document} diff --git a/res/Readme.txt b/res/Readme.txt new file mode 100644 index 00000000..86545317 --- /dev/null +++ b/res/Readme.txt @@ -0,0 +1,12 @@ +Thank you for chosing Synergy! +http://synergy-foss.org/ + +Synergy allows you to share your keyboard and mouse between computers over a network. + +For FAQ, setup, and usage instructions, please visit our online Readme: +http://synergy-foss.org/pm/projects/synergy/wiki/Readme + +Have fun! + +Thanks, +The Synergy Team diff --git a/res/config.h.in b/res/config.h.in new file mode 100644 index 00000000..e0a57752 --- /dev/null +++ b/res/config.h.in @@ -0,0 +1,173 @@ +/* Define version here for Unix, but using /D for Windows. */ +#cmakedefine VERSION "${VERSION}" + +/* Define to the base type of arg 3 for `accept`. */ +#cmakedefine ACCEPT_TYPE_ARG3 ${ACCEPT_TYPE_ARG3} + +/* Define if your compiler has bool support. */ +#cmakedefine HAVE_CXX_BOOL ${HAVE_CXX_BOOL} + +/* Define if your compiler has C++ cast support. */ +#cmakedefine HAVE_CXX_CASTS ${HAVE_CXX_CASTS} + +/* Define if your compiler has exceptions support. */ +#cmakedefine HAVE_CXX_EXCEPTIONS ${HAVE_CXX_EXCEPTIONS} + +/* Define if your compiler has mutable support. */ +#cmakedefine HAVE_CXX_MUTABLE ${HAVE_CXX_MUTABLE} + +/* Define if your compiler has standard C++ library support. */ +#cmakedefine HAVE_CXX_STDLIB ${HAVE_CXX_STDLIB} + +/* Define if the header file declares function prototypes. */ +#cmakedefine HAVE_DPMS_PROTOTYPES ${HAVE_DPMS_PROTOTYPES} + +/* Define if you have a working `getpwuid_r` function. */ +#cmakedefine HAVE_GETPWUID_R ${HAVE_GETPWUID_R} + +/* Define to 1 if you have the `gmtime_r` function. */ +#cmakedefine HAVE_GMTIME_R ${HAVE_GMTIME_R} + +/* Define if you have the `inet_aton` function. */ +#cmakedefine HAVE_INET_ATON ${HAVE_INET_ATON} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_INTTYPES_H ${HAVE_INTTYPES_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_ISTREAM ${HAVE_ISTREAM} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_LOCALE_H ${HAVE_LOCALE_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_MEMORY_H ${HAVE_MEMORY_H} + +/* Define if you have the `nanosleep` function. */ +#cmakedefine HAVE_NANOSLEEP ${HAVE_NANOSLEEP} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_OSTREAM ${HAVE_OSTREAM} + +/* Define if you have the `poll` function. */ +#cmakedefine HAVE_POLL ${HAVE_POLL} + +/* Define if you have a POSIX `sigwait` function. */ +#cmakedefine HAVE_POSIX_SIGWAIT ${HAVE_POSIX_SIGWAIT} + +/* Define if you have POSIX threads libraries and header files. */ +#cmakedefine HAVE_PTHREAD ${HAVE_PTHREAD} + +/* Define if you have `pthread_sigmask` and `pthread_kill` functions. */ +#cmakedefine HAVE_PTHREAD_SIGNAL ${HAVE_PTHREAD_SIGNAL} + +/* Define if your compiler defines socklen_t. */ +#cmakedefine HAVE_SOCKLEN_T ${HAVE_SOCKLEN_T} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SSTREAM ${HAVE_SSTREAM} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_STDINT_H ${HAVE_STDINT_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_STDLIB_H ${HAVE_STDLIB_H} + +/* Define to 1 if you have the `strftime` function. */ +#cmakedefine HAVE_STRFTIME ${HAVE_STRFTIME} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_STRINGS_H ${HAVE_STRINGS_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_STRING_H ${HAVE_STRING_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_SELECT_H ${HAVE_SYS_SELECT_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_SOCKET_H ${HAVE_SYS_SOCKET_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_STAT_H ${HAVE_SYS_STAT_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_TIME_H ${HAVE_SYS_TIME_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_TYPES_H ${HAVE_SYS_TYPES_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_UTSNAME_H ${HAVE_SYS_UTSNAME_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_UNISTD_H ${HAVE_UNISTD_H} + +/* Define to 1 if you have the `vsnprintf` function. */ +#cmakedefine HAVE_VSNPRINTF ${HAVE_VSNPRINTF} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_WCHAR_H ${HAVE_WCHAR_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_X11_EXTENSIONS_XRANDR_H ${HAVE_X11_EXTENSIONS_XRANDR_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_X11_EXTENSIONS_DPMS_H ${HAVE_X11_EXTENSIONS_DPMS_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_X11_EXTENSIONS_XINERAMA_H ${HAVE_X11_EXTENSIONS_XINERAMA_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_X11_EXTENSIONS_XKBSTR_H ${HAVE_X11_EXTENSIONS_XKBSTR_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_X11_EXTENSIONS_XTEST_H ${HAVE_X11_EXTENSIONS_XTEST_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_X11_XKBLIB_H ${HAVE_X11_XKBLIB_H} + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_XI2 ${HAVE_XI2} + +/* Define this if the XKB extension is available. */ +#cmakedefine HAVE_XKB_EXTENSION ${HAVE_XKB_EXTENSION} + +/* Define to necessary symbol if this constant uses a non-standard name on your system. */ +#cmakedefine PTHREAD_CREATE_JOINABLE ${PTHREAD_CREATE_JOINABLE} + +/* Define to the type of arg 1 for `select`. */ +#cmakedefine SELECT_TYPE_ARG1 ${SELECT_TYPE_ARG1} + +/* Define to the type of args 2, 3 and 4 for `select`. */ +#cmakedefine SELECT_TYPE_ARG234 ${SELECT_TYPE_ARG234} + +/* Define to the type of arg 5 for `select`. */ +#cmakedefine SELECT_TYPE_ARG5 ${SELECT_TYPE_ARG5} + +/* The size of `char`, as computed by sizeof. */ +#cmakedefine SIZEOF_CHAR ${SIZEOF_CHAR} + +/* The size of `int`, as computed by sizeof. */ +#cmakedefine SIZEOF_INT ${SIZEOF_INT} + +/* The size of `long`, as computed by sizeof. */ +#cmakedefine SIZEOF_LONG ${SIZEOF_LONG} + +/* The size of `short`, as computed by sizeof. */ +#cmakedefine SIZEOF_SHORT ${SIZEOF_SHORT} + +/* Define to 1 if you have the ANSI C header files. */ +#cmakedefine STDC_HEADERS ${STDC_HEADERS} + +/* Define to 1 if you can safely include both and . */ +#cmakedefine TIME_WITH_SYS_TIME ${TIME_WITH_SYS_TIME} + +/* Define to 1 if your declares `struct tm`. */ +#cmakedefine TM_IN_SYS_TIME ${TM_IN_SYS_TIME} + +/* Define to 1 if the X Window System is missing or not being used. */ +#cmakedefine X_DISPLAY_MISSING ${X_DISPLAY_MISSING} + +/* Define to `unsigned int` if does not define. */ +#cmakedefine size_t ${size_t} diff --git a/res/doxygen.cfg.in b/res/doxygen.cfg.in new file mode 100644 index 00000000..0c4d7213 --- /dev/null +++ b/res/doxygen.cfg.in @@ -0,0 +1,1638 @@ +# Doxyfile 1.7.1 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project +# +# All text after a hash (#) is considered a comment and will be ignored +# The format is: +# TAG = value [value, ...] +# For lists items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (" ") + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# http://www.gnu.org/software/libiconv for the list of possible encodings. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded +# by quotes) that should identify the project. + +PROJECT_NAME = "Synergy" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. +# This could be handy for archiving the generated documentation or +# if some version control system is used. + +PROJECT_NUMBER = "${VERSION}" + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) +# base path where the generated documentation will be put. +# If a relative path is entered, it will be relative to the location +# where doxygen was started. If left blank the current directory will be used. + +OUTPUT_DIRECTORY = doc/doxygen + +# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create +# 4096 sub-directories (in 2 levels) under the output directory of each output +# format and will distribute the generated files over these directories. +# Enabling this option can be useful when feeding doxygen a huge amount of +# source files, where putting all generated files in the same directory would +# otherwise cause performance problems for the file system. + +CREATE_SUBDIRS = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# The default language is English, other supported languages are: +# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, +# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, +# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English +# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, +# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak, +# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will +# include brief member descriptions after the members that are listed in +# the file and class documentation (similar to JavaDoc). +# Set to NO to disable this. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend +# the brief description of a member or function before the detailed description. +# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator +# that is used to form the text in various listings. Each string +# in this list, if found as the leading text of the brief description, will be +# stripped from the text and the result after processing the whole list, is +# used as the annotated text. Otherwise, the brief description is used as-is. +# If left blank, the following values are used ("$name" is automatically +# replaced with the name of the entity): "The $name class" "The $name widget" +# "The $name file" "is" "provides" "specifies" "contains" +# "represents" "a" "an" "the" + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full +# path before files name in the file list and in the header files. If set +# to NO the shortest path that makes the file name unique will be used. + +FULL_PATH_NAMES = NO + +# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag +# can be used to strip a user-defined part of the path. Stripping is +# only done if one of the specified strings matches the left-hand part of +# the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the +# path to strip. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of +# the path mentioned in the documentation of a class, which tells +# the reader which header file to include in order to use a class. +# If left blank only the name of the header file containing the class +# definition is used. Otherwise one should specify the include paths that +# are normally passed to the compiler using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter +# (but less readable) file names. This can be useful is your file systems +# doesn't support long names like on DOS, Mac, or CD-ROM. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen +# will interpret the first line (until the first dot) of a JavaDoc-style +# comment as the brief description. If set to NO, the JavaDoc +# comments will behave just like regular Qt-style comments +# (thus requiring an explicit @brief command for a brief description.) + +JAVADOC_AUTOBRIEF = NO + +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will +# interpret the first line (until the first dot) of a Qt-style +# comment as the brief description. If set to NO, the comments +# will behave just like regular Qt-style comments (thus requiring +# an explicit \brief command for a brief description.) + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen +# treat a multi-line C++ special comment block (i.e. a block of //! or /// +# comments) as a brief description. This used to be the default behaviour. +# The new default is to treat a multi-line C++ comment block as a detailed +# description. Set this tag to YES if you prefer the old behaviour instead. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented +# member inherits the documentation from any documented member that it +# re-implements. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce +# a new page for each member. If set to NO, the documentation of a member will +# be part of the file/class/namespace that contains it. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. +# Doxygen uses this value to replace tabs by spaces in code fragments. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that acts +# as commands in the documentation. An alias has the form "name=value". +# For example adding "sideeffect=\par Side Effects:\n" will allow you to +# put the command \sideeffect (or @sideeffect) in the documentation, which +# will result in a user-defined paragraph with heading "Side Effects:". +# You can put \n's in the value part of an alias to insert newlines. + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C +# sources only. Doxygen will then generate output that is more tailored for C. +# For instance, some of the names that are used will be different. The list +# of all members will be omitted, etc. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java +# sources only. Doxygen will then generate output that is more tailored for +# Java. For instance, namespaces will be presented as packages, qualified +# scopes will look different, etc. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources only. Doxygen will then generate output that is more tailored for +# Fortran. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for +# VHDL. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given extension. +# Doxygen has a built-in mapping, but you can override or extend it using this +# tag. The format is ext=language, where ext is a file extension, and language +# is one of the parsers supported by doxygen: IDL, Java, Javascript, CSharp, C, +# C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, C++. For instance to make +# doxygen treat .inc files as Fortran files (default is PHP), and .f files as C +# (default is Fortran), use: inc=Fortran f=C. Note that for custom extensions +# you also need to set FILE_PATTERNS otherwise the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should +# set this tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. +# func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. +# Doxygen will parse them like normal C++ but will assume all classes use public +# instead of private inheritance when no explicit protection keyword is present. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate getter +# and setter methods for a property. Setting this option to YES (the default) +# will make doxygen to replace the get and set methods by a property in the +# documentation. This will only work if the methods are indeed getting or +# setting a simple type. If this is not the case, or you want to show the +# methods anyway, you should set this option to NO. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES (the default) to allow class member groups of +# the same type (for instance a group of public functions) to be put as a +# subgroup of that type (e.g. under the Public Functions section). Set it to +# NO to prevent subgrouping. Alternatively, this can be done per class using +# the \nosubgrouping command. + +SUBGROUPING = YES + +# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum +# is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically +# be useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. + +TYPEDEF_HIDES_STRUCT = NO + +# The SYMBOL_CACHE_SIZE determines the size of the internal cache use to +# determine which symbols to keep in memory and which to flush to disk. +# When the cache is full, less often used symbols will be written to disk. +# For small to medium size projects (<1000 input files) the default value is +# probably good enough. For larger projects a too small cache size can cause +# doxygen to be busy swapping symbols to and from disk most of the time +# causing a significant performance penality. +# If the system has enough physical memory increasing the cache will improve the +# performance by keeping more symbols in memory. Note that the value works on +# a logarithmic scale so increasing the size by one will rougly double the +# memory usage. The cache size is given by this formula: +# 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0, +# corresponding to a cache size of 2^16 = 65536 symbols + +SYMBOL_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. +# Private class members and static file members will be hidden unless +# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class +# will be included in the documentation. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_STATIC tag is set to YES all static members of a file +# will be included in the documentation. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) +# defined locally in source files will be included in the documentation. +# If set to NO only classes defined in header files are included. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. When set to YES local +# methods, which are defined in the implementation section but not in +# the interface are included in the documentation. +# If set to NO (the default) only methods in the interface are included. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base +# name of the file that contains the anonymous namespace. By default +# anonymous namespace are hidden. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members of documented classes, files or namespaces. +# If set to NO (the default) these members will be included in the +# various overviews, but no documentation section is generated. +# This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. +# If set to NO (the default) these classes will be included in the various +# overviews. This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all +# friend (class|struct|union) declarations. +# If set to NO (the default) these declarations will be included in the +# documentation. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. +# If set to NO (the default) these blocks will be appended to the +# function's detailed documentation block. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation +# that is typed after a \internal command is included. If the tag is set +# to NO (the default) then the documentation will be excluded. +# Set it to YES to include the internal documentation. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate +# file names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen +# will show members with their full class and namespace scopes in the +# documentation. If set to YES the scope will be hidden. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen +# will put a list of the files that are included by a file in the documentation +# of that file. + +SHOW_INCLUDE_FILES = YES + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen +# will list include files with double quotes in the documentation +# rather than with sharp brackets. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] +# is inserted in the documentation for inline members. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen +# will sort the (detailed) documentation of file and class members +# alphabetically by member name. If set to NO the members will appear in +# declaration order. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the +# brief documentation of file, namespace and class members alphabetically +# by member name. If set to NO (the default) the members will appear in +# declaration order. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen +# will sort the (brief and detailed) documentation of class members so that +# constructors and destructors are listed first. If set to NO (the default) +# the constructors will appear in the respective orders defined by +# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. +# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO +# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the +# hierarchy of group names into alphabetical order. If set to NO (the default) +# the group names will appear in their defined order. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be +# sorted by fully-qualified names, including namespaces. If set to +# NO (the default), the class list will be sorted only by class name, +# not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the +# alphabetical list. + +SORT_BY_SCOPE_NAME = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or +# disable (NO) the todo list. This list is created by putting \todo +# commands in the documentation. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or +# disable (NO) the test list. This list is created by putting \test +# commands in the documentation. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or +# disable (NO) the bug list. This list is created by putting \bug +# commands in the documentation. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or +# disable (NO) the deprecated list. This list is created by putting +# \deprecated commands in the documentation. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional +# documentation sections, marked by \if sectionname ... \endif. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines +# the initial value of a variable or define consists of for it to appear in +# the documentation. If the initializer consists of more lines than specified +# here it will be hidden. Use a value of 0 to hide initializers completely. +# The appearance of the initializer of individual variables and defines in the +# documentation can be controlled using \showinitializer or \hideinitializer +# command in the documentation regardless of this setting. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated +# at the bottom of the documentation of classes and structs. If set to YES the +# list will mention the files that were used to generate the documentation. + +SHOW_USED_FILES = YES + +# If the sources in your project are distributed over multiple directories +# then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy +# in the documentation. The default is NO. + +SHOW_DIRECTORIES = NO + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. +# This will remove the Files entry from the Quick Index and from the +# Folder Tree View (if specified). The default is YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the +# Namespaces page. +# This will remove the Namespaces entry from the Quick Index +# and from the Folder Tree View (if specified). The default is YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command , where is the value of +# the FILE_VERSION_FILTER tag, and is the name of an input file +# provided by doxygen. Whatever the program writes to standard output +# is used as the file version. See the manual for examples. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. The create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. +# You can optionally specify a file name after the option, if omitted +# DoxygenLayout.xml will be used as the name of the layout file. + +LAYOUT_FILE = + +#--------------------------------------------------------------------------- +# configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated +# by doxygen. Possible values are YES and NO. If left blank NO is used. + +QUIET = YES + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated by doxygen. Possible values are YES and NO. If left blank +# NO is used. + +WARNINGS = YES + +# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings +# for undocumented members. If EXTRACT_ALL is set to YES then this flag will +# automatically be disabled. + +WARN_IF_UNDOCUMENTED = NO + +# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some +# parameters in a documented function, or documenting parameters that +# don't exist or using markup commands wrongly. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be abled to get warnings for +# functions that are documented, but have no documentation for their parameters +# or return value. If set to NO (the default) doxygen will only warn about +# wrong or incomplete parameter documentation, but not about the absence of +# documentation. + +WARN_NO_PARAMDOC = NO + +# The WARN_FORMAT tag determines the format of the warning messages that +# doxygen can produce. The string should contain the $file, $line, and $text +# tags, which will be replaced by the file and line number from which the +# warning originated and the warning text. Optionally the format may contain +# $version, which will be replaced by the version of the file (if it could +# be obtained via FILE_VERSION_FILTER) + +WARN_FORMAT = + +# The WARN_LOGFILE tag can be used to specify a file to which warning +# and error messages should be written. If left blank the output is written +# to stderr. + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag can be used to specify the files and/or directories that contain +# documented source files. You may enter file names like "myfile.cpp" or +# directories like "/usr/src/myproject". Separate the files or directories +# with spaces. + +INPUT = . + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is +# also the default input encoding. Doxygen uses libiconv (or the iconv built +# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for +# the list of possible encodings. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank the following patterns are tested: +# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx +# *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.py *.f90 + +FILE_PATTERNS = *.cpp \ + *.h + +# The RECURSIVE tag can be used to turn specify whether or not subdirectories +# should be searched for input files as well. Possible values are YES and NO. +# If left blank NO is used. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used select whether or not files or +# directories that are symbolic links (a Unix filesystem feature) are excluded +# from the input. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. Note that the wildcards are matched +# against the file with absolute path, so to exclude all test directories +# for example use the pattern */test/* + +EXCLUDE_PATTERNS = */.svn/* \ + */.git/* \ + */tools/* \ + */src/vnc/* \ + */src/gui/* \ + */src/test/guitests/debug/moc_* \ + */src/test/guitests/release/moc_* \ + */src/qsynergy-build-desktop/* + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or +# directories that contain example code fragments that are included (see +# the \include command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank all files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude +# commands irrespective of the value of the RECURSIVE tag. +# Possible values are YES and NO. If left blank NO is used. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or +# directories that contain image that are included in the documentation (see +# the \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command , where +# is the value of the INPUT_FILTER tag, and is the name of an +# input file. Doxygen will then use the output that the filter program writes +# to standard output. +# If FILTER_PATTERNS is specified, this tag will be +# ignored. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. +# Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. +# The filters are a list of the form: +# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further +# info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER +# is applied to all files. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will be used to filter the input files when producing source +# files to browse (i.e. when SOURCE_BROWSER is set to YES). + +FILTER_SOURCE_FILES = NO + +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will +# be generated. Documented entities will be cross-referenced with these sources. +# Note: To get rid of all source code in the generated output, make sure also +# VERBATIM_HEADERS is set to NO. + +SOURCE_BROWSER = YES + +# Setting the INLINE_SOURCES tag to YES will include the body +# of functions and classes directly in the documentation. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct +# doxygen to hide any special comment blocks from generated source code +# fragments. Normal C and C++ comments will always remain visible. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES +# then for each documented function all documented +# functions referencing it will be listed. + +REFERENCED_BY_RELATION = YES + +# If the REFERENCES_RELATION tag is set to YES +# then for each documented function all documented entities +# called/used by that function will be listed. + +REFERENCES_RELATION = YES + +# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) +# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from +# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will +# link to the source code. +# Otherwise they will link to the documentation. + +REFERENCES_LINK_SOURCE = YES + +# If the USE_HTAGS tag is set to YES then the references to source code +# will point to the HTML generated by the htags(1) tool instead of doxygen +# built-in source browser. The htags tool is part of GNU's global source +# tagging system (see http://www.gnu.org/software/global/global.html). You +# will need version 4.8.6 or higher. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen +# will generate a verbatim copy of the header file for each class for +# which an include is specified. Set to NO to disable this. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index +# of all compounds will be generated. Enable this if the project +# contains a lot of classes, structs, unions or interfaces. + +ALPHABETICAL_INDEX = YES + +# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then +# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns +# in which this list will be split (can be a number in the range [1..20]) + +COLS_IN_ALPHA_INDEX = 3 + +# In case all classes in a project start with a common prefix, all +# classes will be put under the same header in the alphabetical index. +# The IGNORE_PREFIX tag can be used to specify one or more prefixes that +# should be ignored while generating the index headers. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES (the default) Doxygen will +# generate HTML output. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `html' will be used as the default path. + +HTML_OUTPUT = + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for +# each generated HTML page (for example: .htm,.php,.asp). If it is left blank +# doxygen will generate files with .html extension. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a personal HTML header for +# each generated HTML page. If it is left blank doxygen will generate a +# standard header. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a personal HTML footer for +# each generated HTML page. If it is left blank doxygen will generate a +# standard footer. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading +# style sheet that is used by each HTML page. It can be used to +# fine-tune the look of the HTML output. If the tag is left blank doxygen +# will generate a default style sheet. Note that doxygen will try to copy +# the style sheet file to the HTML output directory, so don't put your own +# stylesheet in the HTML output directory as well, or it will be erased! + +HTML_STYLESHEET = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. +# Doxygen will adjust the colors in the stylesheet and background images +# according to this color. Hue is specified as an angle on a colorwheel, +# see http://en.wikipedia.org/wiki/Hue for more information. +# For instance the value 0 represents red, 60 is yellow, 120 is green, +# 180 is cyan, 240 is blue, 300 purple, and 360 is red again. +# The allowed range is 0 to 359. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of +# the colors in the HTML output. For a value of 0 the output will use +# grayscales only. A value of 255 will produce the most vivid colors. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to +# the luminance component of the colors in the HTML output. Values below +# 100 gradually make the output lighter, whereas values above 100 make +# the output darker. The value divided by 100 is the actual gamma applied, +# so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2, +# and 100 does not change the gamma. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting +# this to NO can help when comparing the output of multiple runs. + +HTML_TIMESTAMP = YES + +# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, +# files or namespaces will be aligned in HTML using tables. If set to +# NO a bullet list will be used. + +HTML_ALIGN_MEMBERS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. For this to work a browser that supports +# JavaScript and DHTML is required (for instance Mozilla 1.0+, Firefox +# Netscape 6.0+, Internet explorer 5.0+, Konqueror, or Safari). + +HTML_DYNAMIC_SECTIONS = NO + +# If the GENERATE_DOCSET tag is set to YES, additional index files +# will be generated that can be used as input for Apple's Xcode 3 +# integrated development environment, introduced with OSX 10.5 (Leopard). +# To create a documentation set, doxygen will generate a Makefile in the +# HTML output directory. Running make will produce the docset in that +# directory and running "make install" will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find +# it at startup. +# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. + +GENERATE_DOCSET = NO + +# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the +# feed. A documentation feed provides an umbrella under which multiple +# documentation sets from a single provider (such as a company or product suite) +# can be grouped. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that +# should uniquely identify the documentation set bundle. This should be a +# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen +# will append .docset to the name. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES, additional index files +# will be generated that can be used as input for tools like the +# Microsoft HTML help workshop to generate a compiled HTML help file (.chm) +# of the generated HTML documentation. + +GENERATE_HTMLHELP = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can +# be used to specify the file name of the resulting .chm file. You +# can add a path in front of the file if the result should not be +# written to the html output directory. + +CHM_FILE = + +# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can +# be used to specify the location (absolute path including file name) of +# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run +# the HTML help compiler on the generated index.hhp. + +HHC_LOCATION = + +# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag +# controls if a separate .chi index file is generated (YES) or that +# it should be included in the master .chm file (NO). + +GENERATE_CHI = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING +# is used to encode HtmlHelp index (hhk), content (hhc) and project file +# content. + +CHM_INDEX_ENCODING = + +# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag +# controls whether a binary table of contents is generated (YES) or a +# normal table of contents (NO) in the .chm file. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members +# to the contents of the HTML help documentation and to the tree view. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated +# that can be used as input for Qt's qhelpgenerator to generate a +# Qt Compressed Help (.qch) of the generated HTML documentation. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can +# be used to specify the file name of the resulting .qch file. +# The path specified is relative to the HTML output folder. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#namespace + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#virtual-folders + +QHP_VIRTUAL_FOLDER = doc + +# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to +# add. For more information please see +# http://doc.trolltech.com/qthelpproject.html#custom-filters + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see +# +# Qt Help Project / Custom Filters. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's +# filter section matches. +# +# Qt Help Project / Filter Attributes. + +QHP_SECT_FILTER_ATTRS = + +# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can +# be used to specify the location of Qt's qhelpgenerator. +# If non-empty doxygen will try to run qhelpgenerator on the generated +# .qhp file. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files +# will be generated, which together with the HTML files, form an Eclipse help +# plugin. To install this plugin and make it available under the help contents +# menu in Eclipse, the contents of the directory containing the HTML and XML +# files needs to be copied into the plugins directory of eclipse. The name of +# the directory within the plugins directory should be the same as +# the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before +# the help appears. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have +# this name. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# The DISABLE_INDEX tag can be used to turn on/off the condensed index at +# top of each HTML page. The value NO (the default) enables the index and +# the value YES disables it. + +DISABLE_INDEX = NO + +# This tag can be used to set the number of enum values (range [1..20]) +# that doxygen will group on one line in the generated HTML documentation. + +ENUM_VALUES_PER_LINE = 4 + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. +# If the tag value is set to YES, a side panel will be generated +# containing a tree-like index structure (just like the one that +# is generated for HTML Help). For this to work a browser that supports +# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). +# Windows users are probably better off using the HTML help feature. + +GENERATE_TREEVIEW = NO + +# By enabling USE_INLINE_TREES, doxygen will generate the Groups, Directories, +# and Class Hierarchy pages using a tree view instead of an ordered list. + +USE_INLINE_TREES = NO + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be +# used to set the initial width (in pixels) of the frame in which the tree +# is shown. + +TREEVIEW_WIDTH = 250 + +# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open +# links to external symbols imported via tag files in a separate window. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of Latex formulas included +# as images in the HTML documentation. The default is 10. Note that +# when you change the font size after a successful doxygen run you need +# to manually remove any form_*.png images from the HTML output directory +# to force them to be regenerated. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are +# not supported properly for IE 6.0, but are supported on all modern browsers. +# Note that when changing this option you need to delete any form_*.png files +# in the HTML output before the changes have effect. + +FORMULA_TRANSPARENT = YES + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box +# for the HTML output. The underlying search engine uses javascript +# and DHTML and should work on any modern browser. Note that when using +# HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets +# (GENERATE_DOCSET) there is already a search function so this one should +# typically be disabled. For large projects the javascript based search engine +# can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. + +SEARCHENGINE = NO + +# When the SERVER_BASED_SEARCH tag is enabled the search engine will be +# implemented using a PHP enabled web server instead of at the web client +# using Javascript. Doxygen will generate the search PHP script and index +# file to put on the web server. The advantage of the server +# based approach is that it scales better to large projects and allows +# full text search. The disadvances is that it is more difficult to setup +# and does not have live searching capabilities. + +SERVER_BASED_SEARCH = NO + +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will +# generate Latex output. + +GENERATE_LATEX = NO + +# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `latex' will be used as the default path. + +LATEX_OUTPUT = + +# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be +# invoked. If left blank `latex' will be used as the default command name. +# Note that when enabling USE_PDFLATEX this option is only used for +# generating bitmaps for formulas in the HTML output, but not in the +# Makefile that is written to the output directory. + +LATEX_CMD_NAME = latex + +# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to +# generate index for LaTeX. If left blank `makeindex' will be used as the +# default command name. + +MAKEINDEX_CMD_NAME = makeindex + +# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact +# LaTeX documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_LATEX = NO + +# The PAPER_TYPE tag can be used to set the paper type that is used +# by the printer. Possible values are: a4, a4wide, letter, legal and +# executive. If left blank a4wide will be used. + +PAPER_TYPE = a4wide + +# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX +# packages that should be included in the LaTeX output. + +EXTRA_PACKAGES = + +# The LATEX_HEADER tag can be used to specify a personal LaTeX header for +# the generated latex document. The header should contain everything until +# the first chapter. If it is left blank doxygen will generate a +# standard header. Notice: only use this tag if you know what you are doing! + +LATEX_HEADER = + +# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated +# is prepared for conversion to pdf (using ps2pdf). The pdf file will +# contain links (just like the HTML output) instead of page references +# This makes the output suitable for online browsing using a pdf viewer. + +PDF_HYPERLINKS = NO + +# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of +# plain latex in the generated Makefile. Set this option to YES to get a +# higher quality PDF documentation. + +USE_PDFLATEX = NO + +# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. +# command to the generated LaTeX files. This will instruct LaTeX to keep +# running if errors occur, instead of asking the user for help. +# This option is also used when generating formulas in HTML. + +LATEX_BATCHMODE = NO + +# If LATEX_HIDE_INDICES is set to YES then doxygen will not +# include the index chapters (such as File Index, Compound Index, etc.) +# in the output. + +LATEX_HIDE_INDICES = NO + +# If LATEX_SOURCE_CODE is set to YES then doxygen will include +# source code with syntax highlighting in the LaTeX output. +# Note that which sources are shown also depends on other settings +# such as SOURCE_BROWSER. + +LATEX_SOURCE_CODE = NO + +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- + +# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output +# The RTF output is optimized for Word 97 and may not look very pretty with +# other RTF readers or editors. + +GENERATE_RTF = NO + +# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `rtf' will be used as the default path. + +RTF_OUTPUT = + +# If the COMPACT_RTF tag is set to YES Doxygen generates more compact +# RTF documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_RTF = NO + +# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated +# will contain hyperlink fields. The RTF file will +# contain links (just like the HTML output) instead of page references. +# This makes the output suitable for online browsing using WORD or other +# programs which support those fields. +# Note: wordpad (write) and others do not support links. + +RTF_HYPERLINKS = NO + +# Load stylesheet definitions from file. Syntax is similar to doxygen's +# config file, i.e. a series of assignments. You only have to provide +# replacements, missing definitions are set to their default value. + +RTF_STYLESHEET_FILE = + +# Set optional variables used in the generation of an rtf document. +# Syntax is similar to doxygen's config file. + +RTF_EXTENSIONS_FILE = + +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- + +# If the GENERATE_MAN tag is set to YES (the default) Doxygen will +# generate man pages + +GENERATE_MAN = NO + +# The MAN_OUTPUT tag is used to specify where the man pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `man' will be used as the default path. + +MAN_OUTPUT = + +# The MAN_EXTENSION tag determines the extension that is added to +# the generated man pages (default is the subroutine's section .3) + +MAN_EXTENSION = + +# If the MAN_LINKS tag is set to YES and Doxygen generates man output, +# then it will generate one additional man file for each entity +# documented in the real man page(s). These additional files +# only source the real man page, but without them the man command +# would be unable to find the correct page. The default is NO. + +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- + +# If the GENERATE_XML tag is set to YES Doxygen will +# generate an XML file that captures the structure of +# the code including all documentation. + +GENERATE_XML = NO + +# The XML_OUTPUT tag is used to specify where the XML pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `xml' will be used as the default path. + +XML_OUTPUT = xml + +# The XML_SCHEMA tag can be used to specify an XML schema, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_SCHEMA = + +# The XML_DTD tag can be used to specify an XML DTD, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_DTD = + +# If the XML_PROGRAMLISTING tag is set to YES Doxygen will +# dump the program listings (including syntax highlighting +# and cross-referencing information) to the XML output. Note that +# enabling this will significantly increase the size of the XML output. + +XML_PROGRAMLISTING = YES + +#--------------------------------------------------------------------------- +# configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will +# generate an AutoGen Definitions (see autogen.sf.net) file +# that captures the structure of the code including all +# documentation. Note that this feature is still experimental +# and incomplete at the moment. + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +# If the GENERATE_PERLMOD tag is set to YES Doxygen will +# generate a Perl module file that captures the structure of +# the code including all documentation. Note that this +# feature is still experimental and incomplete at the +# moment. + +GENERATE_PERLMOD = NO + +# If the PERLMOD_LATEX tag is set to YES Doxygen will generate +# the necessary Makefile rules, Perl scripts and LaTeX code to be able +# to generate PDF and DVI output from the Perl module output. + +PERLMOD_LATEX = NO + +# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be +# nicely formatted so it can be parsed by a human reader. +# This is useful +# if you want to understand what is going on. +# On the other hand, if this +# tag is set to NO the size of the Perl module output will be much smaller +# and Perl will parse it just the same. + +PERLMOD_PRETTY = YES + +# The names of the make variables in the generated doxyrules.make file +# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. +# This is useful so different doxyrules.make files included by the same +# Makefile don't overwrite each other's variables. + +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will +# evaluate all C-preprocessor directives found in the sources and include +# files. + +ENABLE_PREPROCESSING = YES + +# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro +# names in the source code. If set to NO (the default) only conditional +# compilation will be performed. Macro expansion can be done in a controlled +# way by setting EXPAND_ONLY_PREDEF to YES. + +MACRO_EXPANSION = YES + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES +# then the macro expansion is limited to the macros specified with the +# PREDEFINED and EXPAND_AS_DEFINED tags. + +EXPAND_ONLY_PREDEF = NO + +# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files +# in the INCLUDE_PATH (see below) will be search if a #include is found. + +SEARCH_INCLUDES = YES + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by +# the preprocessor. + +INCLUDE_PATH = + +# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard +# patterns (like *.h and *.hpp) to filter out the header-files in the +# directories. If left blank, the patterns specified with FILE_PATTERNS will +# be used. + +INCLUDE_FILE_PATTERNS = + +# The PREDEFINED tag can be used to specify one or more macro names that +# are defined before the preprocessor is started (similar to the -D option of +# gcc). The argument of the tag is a list of macros of the form: name +# or name=definition (no spaces). If the definition and the = are +# omitted =1 is assumed. To prevent a macro definition from being +# undefined via #undef or recursively expanded use the := operator +# instead of the = operator. + +PREDEFINED = + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then +# this tag can be used to specify a list of macro names that should be expanded. +# The macro definition that is found in the sources will be used. +# Use the PREDEFINED tag if you want to use a different macro definition. + +EXPAND_AS_DEFINED = + +# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then +# doxygen's preprocessor will remove all function-like macros that are alone +# on a line, have an all uppercase name, and do not end with a semicolon. Such +# function macros are typically used for boiler-plate code, and will confuse +# the parser if not removed. + +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration::additions related to external references +#--------------------------------------------------------------------------- + +# The TAGFILES option can be used to specify one or more tagfiles. +# Optionally an initial location of the external documentation +# can be added for each tagfile. The format of a tag file without +# this location is as follows: +# +# TAGFILES = file1 file2 ... +# Adding location for the tag files is done as follows: +# +# TAGFILES = file1=loc1 "file2 = loc2" ... +# where "loc1" and "loc2" can be relative or absolute paths or +# URLs. If a location is present for each tag, the installdox tool +# does not have to be run to correct the links. +# Note that each tag file must have a unique name +# (where the name does NOT include the path) +# If a tag file is not located in the directory in which doxygen +# is run, you must also specify the path to the tagfile here. + +TAGFILES = + +# When a file name is specified after GENERATE_TAGFILE, doxygen will create +# a tag file that is based on the input files it reads. + +GENERATE_TAGFILE = + +# If the ALLEXTERNALS tag is set to YES all external classes will be listed +# in the class index. If set to NO only the inherited external classes +# will be listed. + +ALLEXTERNALS = NO + +# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will +# be listed. + +EXTERNAL_GROUPS = YES + +# The PERL_PATH should be the absolute path and name of the perl script +# interpreter (i.e. the result of `which perl'). + +PERL_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will +# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base +# or super classes. Setting the tag to NO turns the diagrams off. Note that +# this option is superseded by the HAVE_DOT option below. This is only a +# fallback. It is recommended to install and use dot, since it yields more +# powerful graphs. + +CLASS_DIAGRAMS = NO + +# You can define message sequence charts within doxygen comments using the \msc +# command. Doxygen will then run the mscgen tool (see +# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the +# documentation. The MSCGEN_PATH tag allows you to specify the directory where +# the mscgen tool resides. If left empty the tool is assumed to be found in the +# default search path. + +MSCGEN_PATH = + +# If set to YES, the inheritance and collaboration graphs will hide +# inheritance and usage relations if the target is undocumented +# or is not a class. + +HIDE_UNDOC_RELATIONS = YES + +# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# available from the path. This tool is part of Graphviz, a graph visualization +# toolkit from AT&T and Lucent Bell Labs. The other options in this section +# have no effect if this option is set to NO (the default) + +HAVE_DOT = @HAVE_DOT@ + +# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is +# allowed to run in parallel. When set to 0 (the default) doxygen will +# base this on the number of processors available in the system. You can set it +# explicitly to a value larger than 0 to get control over the balance +# between CPU load and processing speed. + +DOT_NUM_THREADS = 0 + +# By default doxygen will write a font called FreeSans.ttf to the output +# directory and reference it in all dot files that doxygen generates. This +# font does not include all possible unicode characters however, so when you need +# these (or just want a differently looking font) you can specify the font name +# using DOT_FONTNAME. You need need to make sure dot is able to find the font, +# which can be done by putting it in a standard location or by setting the +# DOTFONTPATH environment variable or by setting DOT_FONTPATH to the directory +# containing the font. + +DOT_FONTNAME = FreeSans.ttf + +# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. +# The default size is 10pt. + +DOT_FONTSIZE = 10 + +# By default doxygen will tell dot to use the output directory to look for the +# FreeSans.ttf font (which doxygen will put there itself). If you specify a +# different font using DOT_FONTNAME you can set the path where dot +# can find it using this tag. + +DOT_FONTPATH = + +# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect inheritance relations. Setting this tag to YES will force the +# the CLASS_DIAGRAMS tag to NO. + +CLASS_GRAPH = YES + +# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect implementation dependencies (inheritance, containment, and +# class references variables) of the class with other documented classes. + +COLLABORATION_GRAPH = YES + +# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for groups, showing the direct groups dependencies + +GROUP_GRAPHS = YES + +# If the UML_LOOK tag is set to YES doxygen will generate inheritance and +# collaboration diagrams in a style similar to the OMG's Unified Modeling +# Language. + +UML_LOOK = NO + +# If set to YES, the inheritance and collaboration graphs will show the +# relations between templates and their instances. + +TEMPLATE_RELATIONS = YES + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT +# tags are set to YES then doxygen will generate a graph for each documented +# file showing the direct and indirect include dependencies of the file with +# other documented files. + +INCLUDE_GRAPH = YES + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and +# HAVE_DOT tags are set to YES then doxygen will generate a graph for each +# documented header file showing the documented files that directly or +# indirectly include this file. + +INCLUDED_BY_GRAPH = YES + +# If the CALL_GRAPH and HAVE_DOT options are set to YES then +# doxygen will generate a call dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable call graphs +# for selected functions only using the \callgraph command. + +CALL_GRAPH = NO + +# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then +# doxygen will generate a caller dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable caller +# graphs for selected functions only using the \callergraph command. + +CALLER_GRAPH = NO + +# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen +# will graphical hierarchy of all classes instead of a textual one. + +GRAPHICAL_HIERARCHY = YES + +# If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES +# then doxygen will show the dependencies a directory has on other directories +# in a graphical way. The dependency relations are determined by the #include +# relations between the files in the directories. + +DIRECTORY_GRAPH = YES + +# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images +# generated by dot. Possible values are png, jpg, or gif +# If left blank png will be used. + +DOT_IMAGE_FORMAT = png + +# The tag DOT_PATH can be used to specify the path where the dot tool can be +# found. If left blank, it is assumed the dot tool can be found in the path. + +DOT_PATH = + +# The DOTFILE_DIRS tag can be used to specify one or more directories that +# contain dot files that are included in the documentation (see the +# \dotfile command). + +DOTFILE_DIRS = + +# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of +# nodes that will be shown in the graph. If the number of nodes in a graph +# becomes larger than this value, doxygen will truncate the graph, which is +# visualized by representing a node as a red box. Note that doxygen if the +# number of direct children of the root node in a graph is already larger than +# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note +# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. + +DOT_GRAPH_MAX_NODES = 50 + +# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the +# graphs generated by dot. A depth value of 3 means that only nodes reachable +# from the root by following a path via at most 3 edges will be shown. Nodes +# that lay further from the root node will be omitted. Note that setting this +# option to 1 or 2 may greatly reduce the computation time needed for large +# code bases. Also note that the size of a graph can be further restricted by +# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. + +MAX_DOT_GRAPH_DEPTH = 0 + +# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent +# background. This is disabled by default, because dot on Windows does not +# seem to support this out of the box. Warning: Depending on the platform used, +# enabling this option may lead to badly anti-aliased labels on the edges of +# a graph (i.e. they become hard to read). + +DOT_TRANSPARENT = NO + +# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output +# files in one run (i.e. multiple -o and -T options on the command line). This +# makes dot run faster, but since only newer versions of dot (>1.8.10) +# support this, this feature is disabled by default. + +DOT_MULTI_TARGETS = NO + +# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will +# generate a legend page explaining the meaning of the various boxes and +# arrows in the dot generated graphs. + +GENERATE_LEGEND = YES + +# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will +# remove the intermediate dot files that are used to generate +# the various graphs. + +DOT_CLEANUP = YES diff --git a/res/synergy.desktop b/res/synergy.desktop new file mode 100644 index 00000000..743701e5 --- /dev/null +++ b/res/synergy.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Name=Synergy +Comment=Share your keyboard and mouse over a network +Exec=synergy +Icon=/usr/share/icons/synergy.ico +Type=Application +Categories=Utility diff --git a/res/synergy.ico b/res/synergy.ico new file mode 100644 index 0000000000000000000000000000000000000000..fc2e41468ec60a88e0da4194f288a18572f3f36f GIT binary patch literal 287934 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 literal 0 HcmV?d00001 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 00000000..32cf6f48 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,23 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +add_subdirectory(lib) +add_subdirectory(cmd) +add_subdirectory(test) +add_subdirectory(plugin) + +if (VNC_SUPPORT) + add_subdirectory(vnc) +endif() diff --git a/src/cmd/CMakeLists.txt b/src/cmd/CMakeLists.txt new file mode 100644 index 00000000..be81494b --- /dev/null +++ b/src/cmd/CMakeLists.txt @@ -0,0 +1,18 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +add_subdirectory(synergyc) +add_subdirectory(synergys) +add_subdirectory(synergyd) diff --git a/src/cmd/synergyc/.gitignore b/src/cmd/synergyc/.gitignore new file mode 100644 index 00000000..41a58c4c --- /dev/null +++ b/src/cmd/synergyc/.gitignore @@ -0,0 +1 @@ +/*.aps diff --git a/src/cmd/synergyc/CMSWindowsClientTaskBarReceiver.cpp b/src/cmd/synergyc/CMSWindowsClientTaskBarReceiver.cpp new file mode 100644 index 00000000..f825b585 --- /dev/null +++ b/src/cmd/synergyc/CMSWindowsClientTaskBarReceiver.cpp @@ -0,0 +1,373 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsClientTaskBarReceiver.h" +#include "CClient.h" +#include "CMSWindowsClipboard.h" +#include "LogOutputters.h" +#include "BasicTypes.h" +#include "CArch.h" +#include "CArchTaskBarWindows.h" +#include "CArchMiscWindows.h" +#include "resource.h" +#include "CMSWindowsScreen.h" + +// +// CMSWindowsClientTaskBarReceiver +// + +const UINT CMSWindowsClientTaskBarReceiver::s_stateToIconID[kMaxState] = +{ + IDI_TASKBAR_NOT_RUNNING, + IDI_TASKBAR_NOT_WORKING, + IDI_TASKBAR_NOT_CONNECTED, + IDI_TASKBAR_NOT_CONNECTED, + IDI_TASKBAR_CONNECTED +}; + +CMSWindowsClientTaskBarReceiver::CMSWindowsClientTaskBarReceiver( + HINSTANCE appInstance, const CBufferedLogOutputter* logBuffer) : + CClientTaskBarReceiver(), + m_appInstance(appInstance), + m_window(NULL), + m_logBuffer(logBuffer) +{ + for (UInt32 i = 0; i < kMaxState; ++i) { + m_icon[i] = loadIcon(s_stateToIconID[i]); + } + m_menu = LoadMenu(m_appInstance, MAKEINTRESOURCE(IDR_TASKBAR)); + + // don't create the window yet. we'll create it on demand. this + // has the side benefit of being created in the thread used for + // the task bar. that's good because it means the existence of + // the window won't prevent changing the main thread's desktop. + + // add ourself to the task bar + ARCH->addReceiver(this); +} + +CMSWindowsClientTaskBarReceiver::~CMSWindowsClientTaskBarReceiver() +{ + cleanup(); +} + +void +CMSWindowsClientTaskBarReceiver::cleanup() +{ + ARCH->removeReceiver(this); + for (UInt32 i = 0; i < kMaxState; ++i) { + deleteIcon(m_icon[i]); + } + DestroyMenu(m_menu); + destroyWindow(); +} + +void +CMSWindowsClientTaskBarReceiver::showStatus() +{ + // create the window + createWindow(); + + // lock self while getting status + lock(); + + // get the current status + std::string status = getToolTip(); + + // done getting status + unlock(); + + // update dialog + HWND child = GetDlgItem(m_window, IDC_TASKBAR_STATUS_STATUS); + SendMessage(child, WM_SETTEXT, 0, (LPARAM)status.c_str()); + + if (!IsWindowVisible(m_window)) { + // position it by the mouse + POINT cursorPos; + GetCursorPos(&cursorPos); + RECT windowRect; + GetWindowRect(m_window, &windowRect); + int x = cursorPos.x; + int y = cursorPos.y; + int fw = GetSystemMetrics(SM_CXDLGFRAME); + int fh = GetSystemMetrics(SM_CYDLGFRAME); + int ww = windowRect.right - windowRect.left; + int wh = windowRect.bottom - windowRect.top; + int sw = GetSystemMetrics(SM_CXFULLSCREEN); + int sh = GetSystemMetrics(SM_CYFULLSCREEN); + if (fw < 1) { + fw = 1; + } + if (fh < 1) { + fh = 1; + } + if (x + ww - fw > sw) { + x -= ww - fw; + } + else { + x -= fw; + } + if (x < 0) { + x = 0; + } + if (y + wh - fh > sh) { + y -= wh - fh; + } + else { + y -= fh; + } + if (y < 0) { + y = 0; + } + SetWindowPos(m_window, HWND_TOPMOST, x, y, ww, wh, + SWP_SHOWWINDOW); + } +} + +void +CMSWindowsClientTaskBarReceiver::runMenu(int x, int y) +{ + // do popup menu. we need a window to pass to TrackPopupMenu(). + // the SetForegroundWindow() and SendMessage() calls around + // TrackPopupMenu() are to get the menu to be dismissed when + // another window gets activated and are just one of those + // win32 weirdnesses. + createWindow(); + SetForegroundWindow(m_window); + HMENU menu = GetSubMenu(m_menu, 0); + SetMenuDefaultItem(menu, IDC_TASKBAR_STATUS, FALSE); + HMENU logLevelMenu = GetSubMenu(menu, 3); + CheckMenuRadioItem(logLevelMenu, 0, 6, + CLOG->getFilter() - kERROR, MF_BYPOSITION); + int n = TrackPopupMenu(menu, + TPM_NONOTIFY | + TPM_RETURNCMD | + TPM_LEFTBUTTON | + TPM_RIGHTBUTTON, + x, y, 0, m_window, NULL); + SendMessage(m_window, WM_NULL, 0, 0); + + // perform the requested operation + switch (n) { + case IDC_TASKBAR_STATUS: + showStatus(); + break; + + case IDC_TASKBAR_LOG: + copyLog(); + break; + + case IDC_TASKBAR_SHOW_LOG: + ARCH->showConsole(true); + break; + + case IDC_TASKBAR_LOG_LEVEL_ERROR: + CLOG->setFilter(kERROR); + break; + + case IDC_TASKBAR_LOG_LEVEL_WARNING: + CLOG->setFilter(kWARNING); + break; + + case IDC_TASKBAR_LOG_LEVEL_NOTE: + CLOG->setFilter(kNOTE); + break; + + case IDC_TASKBAR_LOG_LEVEL_INFO: + CLOG->setFilter(kINFO); + break; + + case IDC_TASKBAR_LOG_LEVEL_DEBUG: + CLOG->setFilter(kDEBUG); + break; + + case IDC_TASKBAR_LOG_LEVEL_DEBUG1: + CLOG->setFilter(kDEBUG1); + break; + + case IDC_TASKBAR_LOG_LEVEL_DEBUG2: + CLOG->setFilter(kDEBUG2); + break; + + case IDC_TASKBAR_QUIT: + quit(); + break; + } +} + +void +CMSWindowsClientTaskBarReceiver::primaryAction() +{ + showStatus(); +} + +const IArchTaskBarReceiver::Icon +CMSWindowsClientTaskBarReceiver::getIcon() const +{ + return reinterpret_cast(m_icon[getStatus()]); +} + +void +CMSWindowsClientTaskBarReceiver::copyLog() const +{ + if (m_logBuffer != NULL) { + // collect log buffer + CString data; + for (CBufferedLogOutputter::const_iterator index = m_logBuffer->begin(); + index != m_logBuffer->end(); ++index) { + data += *index; + data += "\n"; + } + + // copy log to clipboard + if (!data.empty()) { + CMSWindowsClipboard clipboard(m_window); + clipboard.open(0); + clipboard.emptyUnowned(); + clipboard.add(IClipboard::kText, data); + clipboard.close(); + } + } +} + +void +CMSWindowsClientTaskBarReceiver::onStatusChanged() +{ + if (IsWindowVisible(m_window)) { + showStatus(); + } +} + +HICON +CMSWindowsClientTaskBarReceiver::loadIcon(UINT id) +{ + HANDLE icon = LoadImage(m_appInstance, + MAKEINTRESOURCE(id), + IMAGE_ICON, + 0, 0, + LR_DEFAULTCOLOR); + return reinterpret_cast(icon); +} + +void +CMSWindowsClientTaskBarReceiver::deleteIcon(HICON icon) +{ + if (icon != NULL) { + DestroyIcon(icon); + } +} + +void +CMSWindowsClientTaskBarReceiver::createWindow() +{ + // ignore if already created + if (m_window != NULL) { + return; + } + + // get the status dialog + m_window = CreateDialogParam(m_appInstance, + MAKEINTRESOURCE(IDD_TASKBAR_STATUS), + NULL, + (DLGPROC)&CMSWindowsClientTaskBarReceiver::staticDlgProc, + reinterpret_cast( + reinterpret_cast(this))); + + // window should appear on top of everything, including (especially) + // the task bar. + LONG_PTR style = GetWindowLongPtr(m_window, GWL_EXSTYLE); + style |= WS_EX_TOOLWINDOW | WS_EX_TOPMOST; + SetWindowLongPtr(m_window, GWL_EXSTYLE, style); + + // tell the task bar about this dialog + CArchTaskBarWindows::addDialog(m_window); +} + +void +CMSWindowsClientTaskBarReceiver::destroyWindow() +{ + if (m_window != NULL) { + CArchTaskBarWindows::removeDialog(m_window); + DestroyWindow(m_window); + m_window = NULL; + } +} + +BOOL +CMSWindowsClientTaskBarReceiver::dlgProc(HWND hwnd, + UINT msg, WPARAM wParam, LPARAM) +{ + switch (msg) { + case WM_INITDIALOG: + // use default focus + return TRUE; + + case WM_ACTIVATE: + // hide when another window is activated + if (LOWORD(wParam) == WA_INACTIVE) { + ShowWindow(hwnd, SW_HIDE); + } + break; + } + return FALSE; +} + +BOOL CALLBACK +CMSWindowsClientTaskBarReceiver::staticDlgProc(HWND hwnd, + UINT msg, WPARAM wParam, LPARAM lParam) +{ + // if msg is WM_INITDIALOG, extract the CMSWindowsClientTaskBarReceiver* + // and put it in the extra window data then forward the call. + CMSWindowsClientTaskBarReceiver* self = NULL; + if (msg == WM_INITDIALOG) { + self = reinterpret_cast( + reinterpret_cast(lParam)); + SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR) lParam); + } + else { + // get the extra window data and forward the call + LONG_PTR data = GetWindowLongPtr(hwnd, GWLP_USERDATA); + if (data != 0) { + self = (CMSWindowsClientTaskBarReceiver*) data; + } + } + + // forward the message + if (self != NULL) { + return self->dlgProc(hwnd, msg, wParam, lParam); + } + else { + return (msg == WM_INITDIALOG) ? TRUE : FALSE; + } +} + +IArchTaskBarReceiver* +createTaskBarReceiver(const CBufferedLogOutputter* logBuffer) +{ + CArchMiscWindows::setIcons( + (HICON)LoadImage(CArchMiscWindows::instanceWin32(), + MAKEINTRESOURCE(IDI_SYNERGY), + IMAGE_ICON, + 32, 32, LR_SHARED), + (HICON)LoadImage(CArchMiscWindows::instanceWin32(), + MAKEINTRESOURCE(IDI_SYNERGY), + IMAGE_ICON, + 16, 16, LR_SHARED)); + + return new CMSWindowsClientTaskBarReceiver( + CMSWindowsScreen::getWindowInstance(), logBuffer); +} diff --git a/src/cmd/synergyc/CMSWindowsClientTaskBarReceiver.h b/src/cmd/synergyc/CMSWindowsClientTaskBarReceiver.h new file mode 100644 index 00000000..3619bb03 --- /dev/null +++ b/src/cmd/synergyc/CMSWindowsClientTaskBarReceiver.h @@ -0,0 +1,68 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSCLIENTTASKBARRECEIVER_H +#define CMSWINDOWSCLIENTTASKBARRECEIVER_H + +#define WIN32_LEAN_AND_MEAN + +#include "CClientTaskBarReceiver.h" +#include + +class CBufferedLogOutputter; + +//! Implementation of CClientTaskBarReceiver for Microsoft Windows +class CMSWindowsClientTaskBarReceiver : public CClientTaskBarReceiver { +public: + CMSWindowsClientTaskBarReceiver(HINSTANCE, const CBufferedLogOutputter*); + virtual ~CMSWindowsClientTaskBarReceiver(); + + // IArchTaskBarReceiver overrides + virtual void showStatus(); + virtual void runMenu(int x, int y); + virtual void primaryAction(); + virtual const Icon getIcon() const; + void cleanup(); + +protected: + void copyLog() const; + + // CClientTaskBarReceiver overrides + virtual void onStatusChanged(); + +private: + HICON loadIcon(UINT); + void deleteIcon(HICON); + void createWindow(); + void destroyWindow(); + + BOOL dlgProc(HWND hwnd, + UINT msg, WPARAM wParam, LPARAM lParam); + static BOOL CALLBACK + staticDlgProc(HWND hwnd, + UINT msg, WPARAM wParam, LPARAM lParam); + +private: + HINSTANCE m_appInstance; + HWND m_window; + HMENU m_menu; + HICON m_icon[kMaxState]; + const CBufferedLogOutputter* m_logBuffer; + static const UINT s_stateToIconID[]; +}; + +#endif diff --git a/src/cmd/synergyc/CMakeLists.txt b/src/cmd/synergyc/CMakeLists.txt new file mode 100644 index 00000000..9512b9f5 --- /dev/null +++ b/src/cmd/synergyc/CMakeLists.txt @@ -0,0 +1,70 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(src + synergyc.cpp +) + +if (WIN32) + list(APPEND src + CMSWindowsClientTaskBarReceiver.cpp + CMSWindowsClientTaskBarReceiver.h + resource.h + synergyc.ico + synergyc.rc + tb_error.ico + tb_idle.ico + tb_run.ico + tb_wait.ico + ) +elseif (APPLE) + list(APPEND src COSXClientTaskBarReceiver.cpp) +elseif (UNIX) + list(APPEND src CXWindowsClientTaskBarReceiver.cpp) +endif() + +set(inc + ../../lib/arch + ../../lib/base + ../../lib/client + ../../lib/common + ../../lib/io + ../../lib/mt + ../../lib/net + ../../lib/platform + ../../lib/synergy +) + +if (UNIX) + list(APPEND inc + ../../.. + ) +endif() + +if (VNC_SUPPORT) + list(APPEND libs vnc_server vnc_client) +endif() + +include_directories(${inc}) +add_executable(synergyc ${src}) +target_link_libraries(synergyc + arch base client common io mt net platform server synergy ${libs}) + +if (CONF_CPACK) + install(TARGETS + synergyc + COMPONENT core + DESTINATION bin) +endif() diff --git a/src/cmd/synergyc/COSXClientTaskBarReceiver.cpp b/src/cmd/synergyc/COSXClientTaskBarReceiver.cpp new file mode 100644 index 00000000..f0cfbca1 --- /dev/null +++ b/src/cmd/synergyc/COSXClientTaskBarReceiver.cpp @@ -0,0 +1,66 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "COSXClientTaskBarReceiver.h" +#include "CArch.h" + +// +// COSXClientTaskBarReceiver +// + +COSXClientTaskBarReceiver::COSXClientTaskBarReceiver( + const CBufferedLogOutputter*) +{ + // add ourself to the task bar + ARCH->addReceiver(this); +} + +COSXClientTaskBarReceiver::~COSXClientTaskBarReceiver() +{ + ARCH->removeReceiver(this); +} + +void +COSXClientTaskBarReceiver::showStatus() +{ + // do nothing +} + +void +COSXClientTaskBarReceiver::runMenu(int, int) +{ + // do nothing +} + +void +COSXClientTaskBarReceiver::primaryAction() +{ + // do nothing +} + +const IArchTaskBarReceiver::Icon +COSXClientTaskBarReceiver::getIcon() const +{ + return NULL; +} + +IArchTaskBarReceiver* +createTaskBarReceiver(const CBufferedLogOutputter* logBuffer) +{ + return new COSXClientTaskBarReceiver(logBuffer); +} + diff --git a/src/cmd/synergyc/COSXClientTaskBarReceiver.h b/src/cmd/synergyc/COSXClientTaskBarReceiver.h new file mode 100644 index 00000000..147a6d9e --- /dev/null +++ b/src/cmd/synergyc/COSXClientTaskBarReceiver.h @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef COSXCLIENTTASKBARRECEIVER_H +#define COSXCLIENTTASKBARRECEIVER_H + +#include "CClientTaskBarReceiver.h" + +class CBufferedLogOutputter; + +//! Implementation of CClientTaskBarReceiver for OS X +class COSXClientTaskBarReceiver : public CClientTaskBarReceiver { +public: + COSXClientTaskBarReceiver(const CBufferedLogOutputter*); + virtual ~COSXClientTaskBarReceiver(); + + // IArchTaskBarReceiver overrides + virtual void showStatus(); + virtual void runMenu(int x, int y); + virtual void primaryAction(); + virtual const Icon getIcon() const; +}; + +#endif diff --git a/src/cmd/synergyc/CXWindowsClientTaskBarReceiver.cpp b/src/cmd/synergyc/CXWindowsClientTaskBarReceiver.cpp new file mode 100644 index 00000000..64f28a18 --- /dev/null +++ b/src/cmd/synergyc/CXWindowsClientTaskBarReceiver.cpp @@ -0,0 +1,65 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsClientTaskBarReceiver.h" +#include "CArch.h" + +// +// CXWindowsClientTaskBarReceiver +// + +CXWindowsClientTaskBarReceiver::CXWindowsClientTaskBarReceiver( + const CBufferedLogOutputter*) +{ + // add ourself to the task bar + ARCH->addReceiver(this); +} + +CXWindowsClientTaskBarReceiver::~CXWindowsClientTaskBarReceiver() +{ + ARCH->removeReceiver(this); +} + +void +CXWindowsClientTaskBarReceiver::showStatus() +{ + // do nothing +} + +void +CXWindowsClientTaskBarReceiver::runMenu(int, int) +{ + // do nothing +} + +void +CXWindowsClientTaskBarReceiver::primaryAction() +{ + // do nothing +} + +const IArchTaskBarReceiver::Icon +CXWindowsClientTaskBarReceiver::getIcon() const +{ + return NULL; +} + +IArchTaskBarReceiver* +createTaskBarReceiver(const CBufferedLogOutputter* logBuffer) +{ + return new CXWindowsClientTaskBarReceiver(logBuffer); +} diff --git a/src/cmd/synergyc/CXWindowsClientTaskBarReceiver.h b/src/cmd/synergyc/CXWindowsClientTaskBarReceiver.h new file mode 100644 index 00000000..4cad4d0b --- /dev/null +++ b/src/cmd/synergyc/CXWindowsClientTaskBarReceiver.h @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSCLIENTTASKBARRECEIVER_H +#define CXWINDOWSCLIENTTASKBARRECEIVER_H + +#include "CClientTaskBarReceiver.h" + +class CBufferedLogOutputter; + +//! Implementation of CClientTaskBarReceiver for X Windows +class CXWindowsClientTaskBarReceiver : public CClientTaskBarReceiver { +public: + CXWindowsClientTaskBarReceiver(const CBufferedLogOutputter*); + virtual ~CXWindowsClientTaskBarReceiver(); + + // IArchTaskBarReceiver overrides + virtual void showStatus(); + virtual void runMenu(int x, int y); + virtual void primaryAction(); + virtual const Icon getIcon() const; +}; + +#endif diff --git a/src/cmd/synergyc/resource.h b/src/cmd/synergyc/resource.h new file mode 100644 index 00000000..eeee6e1e --- /dev/null +++ b/src/cmd/synergyc/resource.h @@ -0,0 +1,37 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Developer Studio generated include file. +// Used by synergyc.rc +// +#define IDS_FAILED 1 +#define IDS_INIT_FAILED 2 +#define IDS_UNCAUGHT_EXCEPTION 3 +#define IDI_SYNERGY 101 +#define IDI_TASKBAR_NOT_RUNNING 102 +#define IDI_TASKBAR_NOT_WORKING 103 +#define IDI_TASKBAR_NOT_CONNECTED 104 +#define IDI_TASKBAR_CONNECTED 105 +#define IDR_TASKBAR 107 +#define IDD_TASKBAR_STATUS 108 +#define IDC_TASKBAR_STATUS_STATUS 1000 +#define IDC_TASKBAR_QUIT 40001 +#define IDC_TASKBAR_STATUS 40002 +#define IDC_TASKBAR_LOG 40003 +#define IDC_TASKBAR_SHOW_LOG 40004 +#define IDC_TASKBAR_LOG_LEVEL_ERROR 40009 +#define IDC_TASKBAR_LOG_LEVEL_WARNING 40010 +#define IDC_TASKBAR_LOG_LEVEL_NOTE 40011 +#define IDC_TASKBAR_LOG_LEVEL_INFO 40012 +#define IDC_TASKBAR_LOG_LEVEL_DEBUG 40013 +#define IDC_TASKBAR_LOG_LEVEL_DEBUG1 40014 +#define IDC_TASKBAR_LOG_LEVEL_DEBUG2 40015 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 109 +#define _APS_NEXT_COMMAND_VALUE 40016 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/cmd/synergyc/synergyc.cpp b/src/cmd/synergyc/synergyc.cpp new file mode 100644 index 00000000..f7eb4b50 --- /dev/null +++ b/src/cmd/synergyc/synergyc.cpp @@ -0,0 +1,35 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClientApp.h" + +#if WINAPI_MSWINDOWS +#include "CMSWindowsClientTaskBarReceiver.h" +#elif WINAPI_XWINDOWS +#include "CXWindowsClientTaskBarReceiver.h" +#elif WINAPI_CARBON +#include "COSXClientTaskBarReceiver.h" +#else +#error Platform not supported. +#endif + +int +main(int argc, char** argv) +{ + CClientApp app(createTaskBarReceiver); + return app.run(argc, argv); +} diff --git a/src/cmd/synergyc/synergyc.ico b/src/cmd/synergyc/synergyc.ico new file mode 100644 index 0000000000000000000000000000000000000000..fc2e41468ec60a88e0da4194f288a18572f3f36f GIT binary patch literal 287934 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 literal 0 HcmV?d00001 diff --git a/src/cmd/synergyc/synergyc.rc b/src/cmd/synergyc/synergyc.rc new file mode 100644 index 00000000..7f2a5dc1 --- /dev/null +++ b/src/cmd/synergyc/synergyc.rc @@ -0,0 +1,141 @@ +//Microsoft Developer Studio generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include +#if !defined(IDC_STATIC) +#define IDC_STATIC (-1) +#endif + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE DISCARDABLE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE DISCARDABLE +BEGIN + "#include \r\n" + "\0" +END + +3 TEXTINCLUDE DISCARDABLE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_SYNERGY ICON DISCARDABLE "synergyc.ico" +IDI_TASKBAR_NOT_RUNNING ICON DISCARDABLE "tb_idle.ico" +IDI_TASKBAR_NOT_WORKING ICON DISCARDABLE "tb_error.ico" +IDI_TASKBAR_NOT_CONNECTED ICON DISCARDABLE "tb_wait.ico" +IDI_TASKBAR_CONNECTED ICON DISCARDABLE "tb_run.ico" + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_TASKBAR_STATUS DIALOG DISCARDABLE 0, 0, 145, 18 +STYLE DS_MODALFRAME | WS_POPUP +FONT 8, "MS Sans Serif" +BEGIN + EDITTEXT IDC_TASKBAR_STATUS_STATUS,3,3,139,12,ES_AUTOHSCROLL | + ES_READONLY | NOT WS_BORDER +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Menu +// + +IDR_TASKBAR MENU DISCARDABLE +BEGIN + POPUP "Synergy" + BEGIN + MENUITEM "Show Status", IDC_TASKBAR_STATUS + MENUITEM "Show Log", IDC_TASKBAR_SHOW_LOG + MENUITEM "Copy Log To Clipboard", IDC_TASKBAR_LOG + POPUP "Set Log Level" + BEGIN + MENUITEM "Error", IDC_TASKBAR_LOG_LEVEL_ERROR + + MENUITEM "Warning", IDC_TASKBAR_LOG_LEVEL_WARNING + + MENUITEM "Note", IDC_TASKBAR_LOG_LEVEL_NOTE + + MENUITEM "Info", IDC_TASKBAR_LOG_LEVEL_INFO + + MENUITEM "Debug", IDC_TASKBAR_LOG_LEVEL_DEBUG + + MENUITEM "Debug1", IDC_TASKBAR_LOG_LEVEL_DEBUG1 + + MENUITEM "Debug2", IDC_TASKBAR_LOG_LEVEL_DEBUG2 + + END + MENUITEM SEPARATOR + MENUITEM "Quit", IDC_TASKBAR_QUIT + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE DISCARDABLE +BEGIN + IDS_FAILED "Synergy is about to quit with errors or warnings. Please check the log then click OK." + IDS_INIT_FAILED "Synergy failed to initialize: %{1}" + IDS_UNCAUGHT_EXCEPTION "Uncaught exception: %{1}" +END + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/src/cmd/synergyc/tb_error.ico b/src/cmd/synergyc/tb_error.ico new file mode 100644 index 0000000000000000000000000000000000000000..746a87c9ec8ae70f24b4125afe9ca68defb2f6d1 GIT binary patch literal 318 zcmZusyA6Xd5PgscB3VLIX+veDOwBY@u9?8{1k4aIf%|Jb3Y{4t9eD?$OZQF)q4|7v^Hl|=e!@IsjRK@B2u}b(FJQe=z?=V5liH zWoPHjzBjX*nT1SN6a`+-89X}5txV(@w?b&ncnuP0lhP#!b);z;MJKxRrt5r?%Pbk_ z?N%_Tg0`uZoK7he#O%9wJeiXYR@<+l75<*=!?qP*0HwJ2|O6{7v9^DvFs zu!_Y~)D!l8cMcLvA>Yqua2!LMtJfQ~uh~C-l}Rwt@WUxQyu;vMf6!iXu5qpJ`0fd8 C{!x(t literal 0 HcmV?d00001 diff --git a/src/cmd/synergyc/tb_run.ico b/src/cmd/synergyc/tb_run.ico new file mode 100644 index 0000000000000000000000000000000000000000..88e160cbfcd029978599f49605ba1a3c7695027e GIT binary patch literal 318 zcmZvXJ#K_B5QRT>QHT}^QKbzPO1ZU9vz2R3fRNJr5IzCD8;(L}A7MN84cny1jNhAi z^COL+lJ|X&*-r&u76q#eLPafx?d1Px0X>%G9mGo6woTC*$N4x8%LKWVjJU*LBRA(t z*&)UlLNF;^_DhTq!s6VW^|KVov_j`difNtFX&-H(O$k3SDILcq=N9iD-8@T;1372! my*B6BBsAGS;Q0-Eqg$^!Uw{7 literal 0 HcmV?d00001 diff --git a/src/cmd/synergyc/tb_wait.ico b/src/cmd/synergyc/tb_wait.ico new file mode 100644 index 0000000000000000000000000000000000000000..257be0a1d1bc613eec8cc1838a1dfd25d4644844 GIT binary patch literal 318 zcmZvXF%H5o3`Ji7QN#e9SYe77nRA*>nK?mJi9LtNNy<&SB_ktS@h=k+vHidOZA%U` zW?k2zcWvM#wvckMXxJFSxZpn+z?@1UcuF zl1i)Vw8|M$8oa;3u2z*2ycgFRqu9Ap#396Zhplt1gb?~ejL|uFp_CFr025R~TS5=- dGfb`By0-J}?~kW-COE!+Lz;S;(X4i~`vJXUO(y^V literal 0 HcmV?d00001 diff --git a/src/cmd/synergyd/CMakeLists.txt b/src/cmd/synergyd/CMakeLists.txt new file mode 100644 index 00000000..dfdb9274 --- /dev/null +++ b/src/cmd/synergyd/CMakeLists.txt @@ -0,0 +1,57 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2012 Nick Bolton +# +# 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 COPYING 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 . + +set(src + synergyd.cpp +) + +set(inc + ../../lib/arch + ../../lib/base + ../../lib/common + ../../lib/io + ../../lib/mt + ../../lib/net + ../../lib/platform + ../../lib/synergy +) + +if (UNIX) + list(APPEND inc + ../../.. + ) +endif() + +include_directories(${inc}) + +if (WIN32) + add_executable(synergyd WIN32 ${src}) +else() + add_executable(synergyd ${src}) +endif() + +if (VNC_SUPPORT) + list(APPEND libs vnc_server vnc_client) +endif() + +target_link_libraries(synergyd + arch base common io mt net platform synergy ${libs}) + +if (CONF_CPACK) + install(TARGETS + synergyd + COMPONENT core + DESTINATION bin) +endif() diff --git a/src/cmd/synergyd/synergyd.cpp b/src/cmd/synergyd/synergyd.cpp new file mode 100644 index 00000000..2ab2083e --- /dev/null +++ b/src/cmd/synergyd/synergyd.cpp @@ -0,0 +1,42 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CDaemonApp.h" +#include + +#ifdef SYSAPI_UNIX + +int +main(int argc, char** argv) +{ + CDaemonApp app; + return app.run(argc, argv); +} + +#elif SYSAPI_WIN32 + +#define WIN32_LEAN_AND_MEAN +#include + +int WINAPI +WinMain(HINSTANCE, HINSTANCE, LPSTR, int) +{ + CDaemonApp app; + return app.run(__argc, __argv); +} + +#endif diff --git a/src/cmd/synergys/.gitignore b/src/cmd/synergys/.gitignore new file mode 100644 index 00000000..41a58c4c --- /dev/null +++ b/src/cmd/synergys/.gitignore @@ -0,0 +1 @@ +/*.aps diff --git a/src/cmd/synergys/CMSWindowsServerTaskBarReceiver.cpp b/src/cmd/synergys/CMSWindowsServerTaskBarReceiver.cpp new file mode 100644 index 00000000..5b2a5348 --- /dev/null +++ b/src/cmd/synergys/CMSWindowsServerTaskBarReceiver.cpp @@ -0,0 +1,404 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsServerTaskBarReceiver.h" +#include "CServer.h" +#include "CMSWindowsClipboard.h" +#include "IEventQueue.h" +#include "LogOutputters.h" +#include "BasicTypes.h" +#include "CArch.h" +#include "CArchTaskBarWindows.h" +#include "resource.h" +#include "CArchMiscWindows.h" +#include "CMSWindowsScreen.h" + +// +// CMSWindowsServerTaskBarReceiver +// + +const UINT CMSWindowsServerTaskBarReceiver::s_stateToIconID[kMaxState] = +{ + IDI_TASKBAR_NOT_RUNNING, + IDI_TASKBAR_NOT_WORKING, + IDI_TASKBAR_NOT_CONNECTED, + IDI_TASKBAR_CONNECTED +}; + +CMSWindowsServerTaskBarReceiver::CMSWindowsServerTaskBarReceiver( + HINSTANCE appInstance, const CBufferedLogOutputter* logBuffer) : + CServerTaskBarReceiver(), + m_appInstance(appInstance), + m_window(NULL), + m_logBuffer(logBuffer) +{ + for (UInt32 i = 0; i < kMaxState; ++i) { + m_icon[i] = loadIcon(s_stateToIconID[i]); + } + m_menu = LoadMenu(m_appInstance, MAKEINTRESOURCE(IDR_TASKBAR)); + + // don't create the window yet. we'll create it on demand. this + // has the side benefit of being created in the thread used for + // the task bar. that's good because it means the existence of + // the window won't prevent changing the main thread's desktop. + + // add ourself to the task bar + ARCH->addReceiver(this); +} + +void +CMSWindowsServerTaskBarReceiver::cleanup() +{ + ARCH->removeReceiver(this); + for (UInt32 i = 0; i < kMaxState; ++i) { + deleteIcon(m_icon[i]); + } + DestroyMenu(m_menu); + destroyWindow(); +} + +CMSWindowsServerTaskBarReceiver::~CMSWindowsServerTaskBarReceiver() +{ + cleanup(); +} + +void +CMSWindowsServerTaskBarReceiver::showStatus() +{ + // create the window + createWindow(); + + // lock self while getting status + lock(); + + // get the current status + std::string status = getToolTip(); + + // get the connect clients, if any + const CClients& clients = getClients(); + + // done getting status + unlock(); + + // update dialog + HWND child = GetDlgItem(m_window, IDC_TASKBAR_STATUS_STATUS); + SendMessage(child, WM_SETTEXT, 0, (LPARAM)status.c_str()); + child = GetDlgItem(m_window, IDC_TASKBAR_STATUS_CLIENTS); + SendMessage(child, LB_RESETCONTENT, 0, 0); + for (CClients::const_iterator index = clients.begin(); + index != clients.end(); ) { + const char* client = index->c_str(); + if (++index == clients.end()) { + SendMessage(child, LB_ADDSTRING, 0, (LPARAM)client); + } + else { + SendMessage(child, LB_INSERTSTRING, (WPARAM)-1, (LPARAM)client); + } + } + + if (!IsWindowVisible(m_window)) { + // position it by the mouse + POINT cursorPos; + GetCursorPos(&cursorPos); + RECT windowRect; + GetWindowRect(m_window, &windowRect); + int x = cursorPos.x; + int y = cursorPos.y; + int fw = GetSystemMetrics(SM_CXDLGFRAME); + int fh = GetSystemMetrics(SM_CYDLGFRAME); + int ww = windowRect.right - windowRect.left; + int wh = windowRect.bottom - windowRect.top; + int sw = GetSystemMetrics(SM_CXFULLSCREEN); + int sh = GetSystemMetrics(SM_CYFULLSCREEN); + if (fw < 1) { + fw = 1; + } + if (fh < 1) { + fh = 1; + } + if (x + ww - fw > sw) { + x -= ww - fw; + } + else { + x -= fw; + } + if (x < 0) { + x = 0; + } + if (y + wh - fh > sh) { + y -= wh - fh; + } + else { + y -= fh; + } + if (y < 0) { + y = 0; + } + SetWindowPos(m_window, HWND_TOPMOST, x, y, ww, wh, + SWP_SHOWWINDOW); + } +} + +void +CMSWindowsServerTaskBarReceiver::runMenu(int x, int y) +{ + // do popup menu. we need a window to pass to TrackPopupMenu(). + // the SetForegroundWindow() and SendMessage() calls around + // TrackPopupMenu() are to get the menu to be dismissed when + // another window gets activated and are just one of those + // win32 weirdnesses. + createWindow(); + SetForegroundWindow(m_window); + HMENU menu = GetSubMenu(m_menu, 0); + SetMenuDefaultItem(menu, IDC_TASKBAR_STATUS, FALSE); + HMENU logLevelMenu = GetSubMenu(menu, 3); + CheckMenuRadioItem(logLevelMenu, 0, 6, + CLOG->getFilter() - kERROR, MF_BYPOSITION); + int n = TrackPopupMenu(menu, + TPM_NONOTIFY | + TPM_RETURNCMD | + TPM_LEFTBUTTON | + TPM_RIGHTBUTTON, + x, y, 0, m_window, NULL); + SendMessage(m_window, WM_NULL, 0, 0); + + // perform the requested operation + switch (n) { + case IDC_TASKBAR_STATUS: + showStatus(); + break; + + case IDC_TASKBAR_LOG: + copyLog(); + break; + + case IDC_TASKBAR_SHOW_LOG: + ARCH->showConsole(true); + break; + + case IDC_RELOAD_CONFIG: + EVENTQUEUE->addEvent(CEvent(getReloadConfigEvent(), + IEventQueue::getSystemTarget())); + break; + + case IDC_FORCE_RECONNECT: + EVENTQUEUE->addEvent(CEvent(getForceReconnectEvent(), + IEventQueue::getSystemTarget())); + break; + + case ID_SYNERGY_RESETSERVER: + EVENTQUEUE->addEvent(CEvent(getResetServerEvent(), + IEventQueue::getSystemTarget())); + break; + + case IDC_TASKBAR_LOG_LEVEL_ERROR: + CLOG->setFilter(kERROR); + break; + + case IDC_TASKBAR_LOG_LEVEL_WARNING: + CLOG->setFilter(kWARNING); + break; + + case IDC_TASKBAR_LOG_LEVEL_NOTE: + CLOG->setFilter(kNOTE); + break; + + case IDC_TASKBAR_LOG_LEVEL_INFO: + CLOG->setFilter(kINFO); + break; + + case IDC_TASKBAR_LOG_LEVEL_DEBUG: + CLOG->setFilter(kDEBUG); + break; + + case IDC_TASKBAR_LOG_LEVEL_DEBUG1: + CLOG->setFilter(kDEBUG1); + break; + + case IDC_TASKBAR_LOG_LEVEL_DEBUG2: + CLOG->setFilter(kDEBUG2); + break; + + case IDC_TASKBAR_QUIT: + quit(); + break; + } +} + +void +CMSWindowsServerTaskBarReceiver::primaryAction() +{ + showStatus(); +} + +const IArchTaskBarReceiver::Icon +CMSWindowsServerTaskBarReceiver::getIcon() const +{ + return reinterpret_cast(m_icon[getStatus()]); +} + +void +CMSWindowsServerTaskBarReceiver::copyLog() const +{ + if (m_logBuffer != NULL) { + // collect log buffer + CString data; + for (CBufferedLogOutputter::const_iterator index = m_logBuffer->begin(); + index != m_logBuffer->end(); ++index) { + data += *index; + data += "\n"; + } + + // copy log to clipboard + if (!data.empty()) { + CMSWindowsClipboard clipboard(m_window); + clipboard.open(0); + clipboard.emptyUnowned(); + clipboard.add(IClipboard::kText, data); + clipboard.close(); + } + } +} + +void +CMSWindowsServerTaskBarReceiver::onStatusChanged() +{ + if (IsWindowVisible(m_window)) { + showStatus(); + } +} + +HICON +CMSWindowsServerTaskBarReceiver::loadIcon(UINT id) +{ + HANDLE icon = LoadImage(m_appInstance, + MAKEINTRESOURCE(id), + IMAGE_ICON, + 0, 0, + LR_DEFAULTCOLOR); + return reinterpret_cast(icon); +} + +void +CMSWindowsServerTaskBarReceiver::deleteIcon(HICON icon) +{ + if (icon != NULL) { + DestroyIcon(icon); + } +} + +void +CMSWindowsServerTaskBarReceiver::createWindow() +{ + // ignore if already created + if (m_window != NULL) { + return; + } + + // get the status dialog + m_window = CreateDialogParam(m_appInstance, + MAKEINTRESOURCE(IDD_TASKBAR_STATUS), + NULL, + (DLGPROC)&CMSWindowsServerTaskBarReceiver::staticDlgProc, + reinterpret_cast( + reinterpret_cast(this))); + + // window should appear on top of everything, including (especially) + // the task bar. + LONG_PTR style = GetWindowLongPtr(m_window, GWL_EXSTYLE); + style |= WS_EX_TOOLWINDOW | WS_EX_TOPMOST; + SetWindowLongPtr(m_window, GWL_EXSTYLE, style); + + // tell the task bar about this dialog + CArchTaskBarWindows::addDialog(m_window); +} + +void +CMSWindowsServerTaskBarReceiver::destroyWindow() +{ + if (m_window != NULL) { + CArchTaskBarWindows::removeDialog(m_window); + DestroyWindow(m_window); + m_window = NULL; + } +} + +BOOL +CMSWindowsServerTaskBarReceiver::dlgProc(HWND hwnd, + UINT msg, WPARAM wParam, LPARAM) +{ + switch (msg) { + case WM_INITDIALOG: + // use default focus + return TRUE; + + case WM_ACTIVATE: + // hide when another window is activated + if (LOWORD(wParam) == WA_INACTIVE) { + ShowWindow(hwnd, SW_HIDE); + } + break; + } + return FALSE; +} + +BOOL CALLBACK +CMSWindowsServerTaskBarReceiver::staticDlgProc(HWND hwnd, + UINT msg, WPARAM wParam, LPARAM lParam) +{ + // if msg is WM_INITDIALOG, extract the CMSWindowsServerTaskBarReceiver* + // and put it in the extra window data then forward the call. + CMSWindowsServerTaskBarReceiver* self = NULL; + if (msg == WM_INITDIALOG) { + self = reinterpret_cast( + reinterpret_cast(lParam)); + SetWindowLongPtr(hwnd, GWLP_USERDATA, lParam); + } + else { + // get the extra window data and forward the call + LONG data = (LONG)GetWindowLongPtr(hwnd, GWLP_USERDATA); + if (data != 0) { + self = reinterpret_cast( + reinterpret_cast(data)); + } + } + + // forward the message + if (self != NULL) { + return self->dlgProc(hwnd, msg, wParam, lParam); + } + else { + return (msg == WM_INITDIALOG) ? TRUE : FALSE; + } +} + +IArchTaskBarReceiver* +createTaskBarReceiver(const CBufferedLogOutputter* logBuffer) +{ + CArchMiscWindows::setIcons( + (HICON)LoadImage(CArchMiscWindows::instanceWin32(), + MAKEINTRESOURCE(IDI_SYNERGY), + IMAGE_ICON, + 32, 32, LR_SHARED), + (HICON)LoadImage(CArchMiscWindows::instanceWin32(), + MAKEINTRESOURCE(IDI_SYNERGY), + IMAGE_ICON, + 16, 16, LR_SHARED)); + + return new CMSWindowsServerTaskBarReceiver( + CMSWindowsScreen::getWindowInstance(), logBuffer); +} diff --git a/src/cmd/synergys/CMSWindowsServerTaskBarReceiver.h b/src/cmd/synergys/CMSWindowsServerTaskBarReceiver.h new file mode 100644 index 00000000..7d8637b2 --- /dev/null +++ b/src/cmd/synergys/CMSWindowsServerTaskBarReceiver.h @@ -0,0 +1,68 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSSERVERTASKBARRECEIVER_H +#define CMSWINDOWSSERVERTASKBARRECEIVER_H + +#define WIN32_LEAN_AND_MEAN + +#include "CServerTaskBarReceiver.h" +#include + +class CBufferedLogOutputter; + +//! Implementation of CServerTaskBarReceiver for Microsoft Windows +class CMSWindowsServerTaskBarReceiver : public CServerTaskBarReceiver { +public: + CMSWindowsServerTaskBarReceiver(HINSTANCE, const CBufferedLogOutputter*); + virtual ~CMSWindowsServerTaskBarReceiver(); + + // IArchTaskBarReceiver overrides + virtual void showStatus(); + virtual void runMenu(int x, int y); + virtual void primaryAction(); + virtual const Icon getIcon() const; + void cleanup(); + +protected: + void copyLog() const; + + // CServerTaskBarReceiver overrides + virtual void onStatusChanged(); + +private: + HICON loadIcon(UINT); + void deleteIcon(HICON); + void createWindow(); + void destroyWindow(); + + BOOL dlgProc(HWND hwnd, + UINT msg, WPARAM wParam, LPARAM lParam); + static BOOL CALLBACK + staticDlgProc(HWND hwnd, + UINT msg, WPARAM wParam, LPARAM lParam); + +private: + HINSTANCE m_appInstance; + HWND m_window; + HMENU m_menu; + HICON m_icon[kMaxState]; + const CBufferedLogOutputter* m_logBuffer; + static const UINT s_stateToIconID[]; +}; + +#endif diff --git a/src/cmd/synergys/CMakeLists.txt b/src/cmd/synergys/CMakeLists.txt new file mode 100644 index 00000000..a389c497 --- /dev/null +++ b/src/cmd/synergys/CMakeLists.txt @@ -0,0 +1,70 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(src + synergys.cpp +) + +if (WIN32) + list(APPEND src + CMSWindowsServerTaskBarReceiver.cpp + CMSWindowsServerTaskBarReceiver.h + resource.h + synergys.ico + synergys.rc + tb_error.ico + tb_idle.ico + tb_run.ico + tb_wait.ico + ) +elseif (APPLE) + list(APPEND src COSXServerTaskBarReceiver.cpp) +elseif (UNIX) + list(APPEND src CXWindowsServerTaskBarReceiver.cpp) +endif() + +set(inc + ../../lib/arch + ../../lib/base + ../../lib/common + ../../lib/io + ../../lib/mt + ../../lib/net + ../../lib/platform + ../../lib/synergy + ../../lib/server +) + +if (UNIX) + list(APPEND inc + ../../.. + ) +endif() + +if (VNC_SUPPORT) + list(APPEND libs vnc_server vnc_client) +endif() + +include_directories(${inc}) +add_executable(synergys ${src}) +target_link_libraries(synergys + arch base client common io mt net platform server synergy ${libs}) + +if (CONF_CPACK) + install(TARGETS + synergys + COMPONENT core + DESTINATION bin) +endif() diff --git a/src/cmd/synergys/COSXServerTaskBarReceiver.cpp b/src/cmd/synergys/COSXServerTaskBarReceiver.cpp new file mode 100644 index 00000000..17d2a39a --- /dev/null +++ b/src/cmd/synergys/COSXServerTaskBarReceiver.cpp @@ -0,0 +1,65 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "COSXServerTaskBarReceiver.h" +#include "CArch.h" + +// +// COSXServerTaskBarReceiver +// + +COSXServerTaskBarReceiver::COSXServerTaskBarReceiver( + const CBufferedLogOutputter*) +{ + // add ourself to the task bar + ARCH->addReceiver(this); +} + +COSXServerTaskBarReceiver::~COSXServerTaskBarReceiver() +{ + ARCH->removeReceiver(this); +} + +void +COSXServerTaskBarReceiver::showStatus() +{ + // do nothing +} + +void +COSXServerTaskBarReceiver::runMenu(int, int) +{ + // do nothing +} + +void +COSXServerTaskBarReceiver::primaryAction() +{ + // do nothing +} + +const IArchTaskBarReceiver::Icon +COSXServerTaskBarReceiver::getIcon() const +{ + return NULL; +} + +IArchTaskBarReceiver* +createTaskBarReceiver(const CBufferedLogOutputter* logBuffer) +{ + return new COSXServerTaskBarReceiver(logBuffer); +} diff --git a/src/cmd/synergys/COSXServerTaskBarReceiver.h b/src/cmd/synergys/COSXServerTaskBarReceiver.h new file mode 100644 index 00000000..967f5b5c --- /dev/null +++ b/src/cmd/synergys/COSXServerTaskBarReceiver.h @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef COSXSERVERTASKBARRECEIVER_H +#define COSXSERVERTASKBARRECEIVER_H + +#include "CServerTaskBarReceiver.h" + +class CBufferedLogOutputter; + +//! Implementation of CServerTaskBarReceiver for OS X +class COSXServerTaskBarReceiver : public CServerTaskBarReceiver { +public: + COSXServerTaskBarReceiver(const CBufferedLogOutputter*); + virtual ~COSXServerTaskBarReceiver(); + + // IArchTaskBarReceiver overrides + virtual void showStatus(); + virtual void runMenu(int x, int y); + virtual void primaryAction(); + virtual const Icon getIcon() const; +}; + +#endif diff --git a/src/cmd/synergys/CXWindowsServerTaskBarReceiver.cpp b/src/cmd/synergys/CXWindowsServerTaskBarReceiver.cpp new file mode 100644 index 00000000..60a81eff --- /dev/null +++ b/src/cmd/synergys/CXWindowsServerTaskBarReceiver.cpp @@ -0,0 +1,65 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsServerTaskBarReceiver.h" +#include "CArch.h" + +// +// CXWindowsServerTaskBarReceiver +// + +CXWindowsServerTaskBarReceiver::CXWindowsServerTaskBarReceiver( + const CBufferedLogOutputter*) +{ + // add ourself to the task bar + ARCH->addReceiver(this); +} + +CXWindowsServerTaskBarReceiver::~CXWindowsServerTaskBarReceiver() +{ + ARCH->removeReceiver(this); +} + +void +CXWindowsServerTaskBarReceiver::showStatus() +{ + // do nothing +} + +void +CXWindowsServerTaskBarReceiver::runMenu(int, int) +{ + // do nothing +} + +void +CXWindowsServerTaskBarReceiver::primaryAction() +{ + // do nothing +} + +const IArchTaskBarReceiver::Icon +CXWindowsServerTaskBarReceiver::getIcon() const +{ + return NULL; +} + +IArchTaskBarReceiver* +createTaskBarReceiver(const CBufferedLogOutputter* logBuffer) +{ + return new CXWindowsServerTaskBarReceiver(logBuffer); +} diff --git a/src/cmd/synergys/CXWindowsServerTaskBarReceiver.h b/src/cmd/synergys/CXWindowsServerTaskBarReceiver.h new file mode 100644 index 00000000..1220bed1 --- /dev/null +++ b/src/cmd/synergys/CXWindowsServerTaskBarReceiver.h @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSSERVERTASKBARRECEIVER_H +#define CXWINDOWSSERVERTASKBARRECEIVER_H + +#include "CServerTaskBarReceiver.h" + +class CBufferedLogOutputter; + +//! Implementation of CServerTaskBarReceiver for X Windows +class CXWindowsServerTaskBarReceiver : public CServerTaskBarReceiver { +public: + CXWindowsServerTaskBarReceiver(const CBufferedLogOutputter*); + virtual ~CXWindowsServerTaskBarReceiver(); + + // IArchTaskBarReceiver overrides + virtual void showStatus(); + virtual void runMenu(int x, int y); + virtual void primaryAction(); + virtual const Icon getIcon() const; +}; + +#endif diff --git a/src/cmd/synergys/resource.h b/src/cmd/synergys/resource.h new file mode 100644 index 00000000..1cc1e8b9 --- /dev/null +++ b/src/cmd/synergys/resource.h @@ -0,0 +1,42 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by synergys.rc +// +#define IDS_FAILED 1 +#define IDS_INIT_FAILED 2 +#define IDS_UNCAUGHT_EXCEPTION 3 +#define IDI_SYNERGY 101 +#define IDI_TASKBAR_NOT_RUNNING 102 +#define IDI_TASKBAR_NOT_WORKING 103 +#define IDI_TASKBAR_NOT_CONNECTED 104 +#define IDI_TASKBAR_CONNECTED 105 +#define IDR_TASKBAR 107 +#define IDD_TASKBAR_STATUS 108 +#define IDC_TASKBAR_STATUS_STATUS 1000 +#define IDC_TASKBAR_STATUS_CLIENTS 1001 +#define IDC_TASKBAR_QUIT 40003 +#define IDC_TASKBAR_STATUS 40004 +#define IDC_TASKBAR_LOG 40005 +#define IDC_RELOAD_CONFIG 40006 +#define IDC_FORCE_RECONNECT 40007 +#define IDC_TASKBAR_SHOW_LOG 40008 +#define IDC_TASKBAR_LOG_LEVEL_ERROR 40009 +#define IDC_TASKBAR_LOG_LEVEL_WARNING 40010 +#define IDC_TASKBAR_LOG_LEVEL_NOTE 40011 +#define IDC_TASKBAR_LOG_LEVEL_INFO 40012 +#define IDC_TASKBAR_LOG_LEVEL_DEBUG 40013 +#define IDC_TASKBAR_LOG_LEVEL_DEBUG1 40014 +#define IDC_TASKBAR_LOG_LEVEL_DEBUG2 40015 +#define ID_SYNERGY_RELOADSYSTEM 40016 +#define ID_SYNERGY_RESETSERVER 40017 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 109 +#define _APS_NEXT_COMMAND_VALUE 40018 +#define _APS_NEXT_CONTROL_VALUE 1003 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/cmd/synergys/synergys.cpp b/src/cmd/synergys/synergys.cpp new file mode 100644 index 00000000..b56c19f1 --- /dev/null +++ b/src/cmd/synergys/synergys.cpp @@ -0,0 +1,35 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CServerApp.h" + +#if WINAPI_MSWINDOWS +#include "CMSWindowsServerTaskBarReceiver.h" +#elif WINAPI_XWINDOWS +#include "CXWindowsServerTaskBarReceiver.h" +#elif WINAPI_CARBON +#include "COSXServerTaskBarReceiver.h" +#else +#error Platform not supported. +#endif + +int +main(int argc, char** argv) +{ + CServerApp app(createTaskBarReceiver); + return app.run(argc, argv); +} diff --git a/src/cmd/synergys/synergys.ico b/src/cmd/synergys/synergys.ico new file mode 100644 index 0000000000000000000000000000000000000000..fc2e41468ec60a88e0da4194f288a18572f3f36f GIT binary patch literal 287934 zcmeFa2XK^Uwk_)W>b!b&ea=1Ou`wA$HqJK20b`pS3?|u_Xbc8S2Ahl!2!SL}4k+iG zQ>(kxYDpcWR?a!+074=_| zJo5iOBL4l4M1=P`(bCjp-cVO(^@E_is!o!M>Uvbw zHln7k-dad~eZ5s=D2s53L5Nlrm*Xc&CFyx`#Egp(Jnu>)YDec2rhZyP5LxkfK&0Br*ano*p=V=^`wS9>ej2hj3uu zKJ4Cm*QPhouGb&fi?s*$V*6QheCBWw=l#9m5)lfI=m_{k-Ic(YC`6|wBSoV^qAD3t zaghj%4nk~PD57Hn5gG1-zyN2syIsKfbNqjf?!jjVckut(hP`_}reF97yXaT;AJ~Hv zCyz?;f}6WD0s?)JkQk55tPJ{sV${{wT8o%K_5-pXxREdS;I?VecsK*8RlnBW9MfFu>R0q%-y>kuQE4ZV0RX~v8U2jBF ze+^o$)}WU^d#$?&*IU!kUmuODb$;lsbwpR~C3H7jL~oNFuGG7szs?8OYeI3WJ_6Ue zqHv=x4uhRZ=xSSc) z8&O-!+@QC2(XZuN%72IKmwv7e+_`mgSbuA?d0Dmrd0ASdCC0(q#RW$W9mJN+oABX= zb@+(3bIR2LZW&7WRpi6Hp%jkoRj}!AN+Jtf%LS&B{FrFg%q9Pf6O zVR83enaAhm^p;>A-?xH(U@v{c#jEv<|65ToP>;r|)o9{#T|H&!YtN&Nr=q(u6kQb_ zXsfu0)`|;gt+GN}^?7tu*rTJ=1wF++xKieix-xgP*Zbp2ODwK7B}?|-UZp`}aXQMf zH87?nBPKKmrx?F{xNa>rZP|={hY#WGrStFz@`1>0Dk>`6YHF+PJG(kI{@m|QexGb- z526m-{`$*DMIG!Kd-V;}`}8z5f}_G=X@3zLKHG;kHm$|t;|H-L#0%TwgRwg+1xIT0 zab}4c|XP*0K(O!2DtqnWS(zr#E zw)$;otJ{Ij+TDE5Av9GVLvy7CT52uPQE!W`CP(x&dttCE5_hht(BGj#XM>vlEfk4S zml5LQ0E^?FW5-7uvG3DeIDh^;LPJB5lB_^kX?aw0Yn!>i8Qr?`g^Vqd@dUprJjIP~ zz8uzj?dqcXhC1YCWg$L19QLf?A3c2%n?Bu+9jA|DhwV8Wj|_xQZ85?bpLv72%ccVoClM`wwKDK-hodAkTm14_#La(bKL$ zcYO%Ds-4hTa|Ug-2hdW#9ZmI{C26Vs7|qq&&|0|z?G^iI_a{(Wd>rKkhf!U03bmz{ zs4KSRbIzzK@j^p+xMcrV+H#m%RM2-6A|Wai7c5TU6nhs34jjPFojY;H;w%FE0}&ga zfReIubaZuZ6qv&Y(Z|cbvmBfJr4DrVU)@;Q*x;6xYeHmV0-W4kag6!!@`JlE=fpw0 zed==@%1nX7Kr5{7bYkCNGuASXU(!>KH+xF)Dr55(uax73s}*>far~s-Voc~R#N;a# znACUotQY6~B%bv{GL=9340C{&=__91XUyX3i~Gv3y{8gquGGMfHNh;_{j0lMP}5tF zcIN(FEotbf4rcsshrTLvT&dZQp1P0FRsW$R-Sz7k7i^?|*n;k=Pta1n6>XKDpuK9l zByH8atzo=Svzs{sB!@7EUR+ZP+`D+Y%cm)OG zl+`)xJ#+wDwzF5}=77t^+3>F~hg(}U&a+2%sG}TPJ1g)W>+JK|3h{bp5oYqNe~$6} zw2lHyWR5<*GhecQVfzz%%P@_v3zjc|q>)X&#yOZbqUdjI3s*j_VaY0?t1ytudqRMoc^#j%l z>eA3wn2L&0J#r0cNKOpLr=NU;O&iwZjJXAzUESFKNQTM8K4f#J`ORBj%J^bAA9$eh zfv>I&PQTL9cBrPX7%53fuyVA=I%0B`9omCE?hZJWmVhI51vt(+{{Eg?Y-lTGY+sId zI*KrhXZ=fk%;RbELY|`CPU|Yd)4Y9#kDuk){vt)#^-JxAn9*7!$uq5m5Hh_b7f&^t z@KReIX7K*Y?Kya@-Gn)vIauG3hl4%EaAiEM>Zw6NTNR4vYie8dXsJs4Y>w;i21d(dUti{A2`=qcZZuF{XuQL+gg2FHUK!~R|%uk-guEU>U zx8-S=v+f;`r$=OWBdmA^AMC2c?v6@qY%jy=-b%dLl82eX)_KlP?JSgRU-$rF|4)g> zlxginnBH1|XWH`dLTf%=YA(QwP5GGKoR6oP@-VF_2U8l1c(&1q=j#o;&BDt~dVaPJ z?>8H*v!G4li3e|Py7#snMbKNug-_PeV$vQDs(HoqOMVMOhtOPj z42_i5LMt?u2eC#Fi>AtWTwi2(i>hlBm#K#RM@-lFivG(cyM(kuCej#J^ zx0suY82=U8^>dvCc)F_qPf?~Z#uvBG@;M>Tx98(Ip64&LnDBga4xVeu#|bU4$RjcEFVtY!@i z%}NwCM5Ch8hjF?!Y7B=_o3#VAx{avKT#IV$YDvWXx{UQ`NZ*8d%|_H2K14(IdNk*5 zLTmmwTK>F_V$2e`c?V;7=Jr3ee_{Jm z8LNvpV0v3Fo^8#=)3keWra#kQz|-}5OsvesyGju6+57z^fIwV@#t;Q5?hdl40RM-9IZKbZ~(`T zTfoCR0Gjk1&KtH7FEA*_0)Mtt#7c(GiJlv+Z%gM9LHMD#H@2?eLEuw z$JoEyeWMBM25KcN&a1TR=R0zl+w+X?W}Hu%(v|z8Ozz5IoS%cKtvbf}Iy}{qfoaWJ zOl?ZX`EA7 z-~BvZw} zP7+(}?&gWKbR#OOo81KG!NZho{08WZrpLn;EoVmMPo)5>) zTAXB^Z5``!OPQ;`)m4bs7~8+l$#eY&`xmx9xzmJ6%z-DiXTz+GXM1xNCN^p@g(7Tz zBC)k*HK`a^m4cCskB9SYf3hMPBg&#Msw4s}wVwm_`Zt^1Jr7T8a>QWS^u0W}370Of}pi2FrBz4-2k}s&& zZ9-%ACN$-3L~}lK1?C4rgb!#h+KQG!_6~~nqNVr=6_f4=@dtUhS)+O_Fb#PKPJz}a+?klTQV`B z@vgm3V%|QbmN|S)A|_SxjHkVevwdVq2u2hKV?w?!rsewJDU&yC-AlTCI?op`n*#7Q zZGTmMIMx?MV@pL6b}=vbyeR{=ZP^Im?~%%WU^eH4Dmxlb%f4eP^O3eD9eaDptkDId zCD#KL85d~N=EzPs2z}HJ+Wr>k!q!6@^Z_ygRzerPR1!nPyU31Oj$FnH`SFYk65dB? z(pr=ze}Hnuhp14mLzQL&YFIm{%c4&(Zl*u@2(2PNU~bTu_o3t$I?J~+9@xeIpMA$N zb15(Ata2y5I2t`o3K+BjaI~RsJ@zRs*`7swd^`#Z3-#UPIlE0h!-rK5<-hwM?*p$~ zxiVYu=qZwuanaEMyJ`D-ZO+3^t3)7c`!>YXd`isTD)#f{uy*$n&->?i_KUoJVw*{l zX>A6^_HQmjq zXCI=S^8;P<4ObXz^wq?nzbX`+B_6ElIik*Zp7lU;+V4^3|NCg;+hB@ZFWG)(=wkW< z#s-1&krBKQdisRisP|A1y9)WSD^M7}3dJdFP?}0#z}i7oI%@}6^aJ{hs5gBe`GDqv z^=Qo9z!-ozL%|L-=I`SDURV$hnQPQNlk^jhX+1kT)N5iB0e{&iLI%{o~yMIPIo?gcCWUY@G^Vx zFS33oYIx7Eem|YhO>Gi&yiC^dGBAa8I8no!$oSrj@%?z(?u4>fOehY=MCR?2^8+wB z#}`wxJ%11n-g;nWjyK*k`C<-VpI;P=MdcBAw>k#z*Ct{cV~5Xq<{zOyIo_B7bNU%8 z#vsnb`9#vz(}*$3XI@gnencG~H@6j_y;+Y={w`gN0lMgSn)!dXV`H zFt5LeTJ>2}B_Bm$%r4}Ie+)z5I%N2*M27FX$O>2reaKQ6!E2z@0MVan>#`_ykmoMyp9a`uIn)BC5_nY%Jqak-YpJR`p;4EtLFA-A| zj4t*nN^^q|>}SK8;Bi=4Ss@}Kf_TA_sE&?~jkj*ylH&lmE_mPSf}+P)Q&VFtX4f2@ zopF>pc*x!s&ZZ2wGoC)tS%r@{D=%{Ww`uRMG-u=0_5!KDH=RAZr&zy#s)haeMgykT zY4KETIwn>#x3A=x&p3TdSpr6v@VqaI#Mt~`j57sde6}~t^q!cccb8lI*{(yB3X{ zCu(OtAkP0T;s84NdvugWGcFHiZ_iJ%|EBEAXfisZS$_#l%nO<{7HCXAfx6U#D2>|z zQ}}uqf>uiQpAoPS>Hc$&89bNqz+7ZU&xa{?Ax!a$k(;mt1@r}?UQnE|3MHAVQL0^}t>g_W)ujcLOjAdt!{dk=VCS*8aip~*F8C~#fo;z!Lo|vB>iRA^+SX~r{4@(lTxik)& zN@B5r{^Ns^NUWw$SXCOv+fb~ljKC+=;n-UhfzRt>VcnR7zy=jk*fUf!UNDtMp}4@0 zu{mdVG*+lqoknHq2~;SKvln;@RoXMCNI!?lbZb;=tWlxlZK@s060K2^Xu+J|F!T|h zKohbS%Aog<;g2YlBW&VE8VQe*w$WP1!}&KyEWYygUC zixu@fEmniyd}($+^g#a1htI!U`}XVU9XAFyR<*Z6la+};cMsT}K7+l1o;X^R3H!c! z;`Yn1vMw8QiO~`D{izLEnA*bpo-&2C_Q{Qztmmh}jJ>z?{4vddSB%Yc!o*DHySATu87~%jVMeJh=H*3Ti76aQv%|2Q zvL+`4tFwc#!sw4B24Bq8UB=rPu6R@9fVa}@u~>5ntJ58@k@3SJ<_#CKgW#hJL_nqo zVw4U@h_!$+;2=`Hwj#xS0~D9nBiZdkBzbLw(*I+qgFa;)@GuG!%~6zi76s9FMc99- z;w0nzeaMdc7@1LPp$T3l*}u|fE`(_42ekeRU<_nV5X3%W$TGfn0df=BKS*I8k$J(8 z9}su~fi0@fS%(G_YX!L<@&0zk0(;SvLmY9@SzHrXfpQ&cOLRz$_ajfzNu0fCj-c3J z{=bx5Vt#< zy|kBGnb)(AC(ipREwp>ix=m@+5QC?}E0scus4L zXDHLO7cf5E3Zv32@muv7NrtH{F(&;yrfY5RdZr!bFoyV$`MhPWJG>1p@X5M_VC7lv z062i?fUStVyc+S&OA+fh7crOKLag0eNV0!hk~HV}FkW7Y?90oM<@bS<`xi!SMR~%f zD2?5T;^^%tj@XX8;4Lutu1A&^eZl4TpmtvZwI^c%;RgcWk#Yk=*ka^FzAM!UixO6& zBkW84i*>pJ9V%H0Lpm zwmUiHG$tgQW4z)tMky^Yobq_uSqx*$J|@c+la2OxImekX!(}Wj^1+tk09+!6Pe`E; z!t&h_r?*3j>J*eQ`=JWjj5Oa5kmR-qNwoJQ=Xp>#%|*hc*+{gR1%>r2BwD_XxJ&bp zV7~xLw`I_JuSTZN2Rz?5pdfUsB>5p9!xXp~I^Xx9@pu=?%X6Ugd>d-tH<2FjHZp_e zLl?48suh}|mi_1lN)@aTs#i+*!s;yc42UhPHEv>!U@K}3#3dVdqh5a)Wm(o}Vx6Ru zJ&uM7?mvk0gQegeb##Q-Pte%hYPQ1$_GJ@6OT1AANB4pkNw=@TL%v4cInjZQW= zRhWSjoT2@Yv$sn*OZNtGG|$y)@icKe(R0Vd=@@e5ewkC7P;OVCCT&IglwM;(0Q$a`to8Z zT<0M9@@y#R2Q;2@k?y&Weq#yr{>%r0mm@EH75fS+Q5?SlWvQzq{IS3p3qMdRasm+x zm^Slv3vYL$A?F0K0?wQ-j6*{SdCdJU;{x}Yc)GbF+n7Z@;|9w+cLvSmGyCB>v+u?A z{$gNodJ}i-#D_=Vy!mN-ZhHYv#&ld}&HO-9KHlSO?R;YMXEhn{d|d{6dKuE0KdwPT zY_1w(Y5U`=l3-RAjR`#O$FrV4hS;2uMsEyfU2hCy`cXXNM`T{YlUiF0)1Jo&#_1!p zR*cuJFrJSmF`l2SxqvBZD@<0L!<57`n3`}_lK-VSg~u{1FiLlh{eknCqPNAf1_!*9 z=Yls1y)m~k81Gd_V?#qC_B5+;qAde<-Pv%xQiyPJh^WXNl;2x`;+_J1RcH2%M&b`z zI3Lhf$}_bv2=$CPD%Iyvlysc^{=LvMH_-WPhS7ulKIhd?y3A!xFq?6~EW|PB-cJkn~^FczpWZlEvN%-fy3J&Fc_5nvqEUY&-r zoDBH7xxt1!3;upyk`L(ZZI^2Sat!b{#Q-<251Lg~SGsA^(_rajhh28(aZ0bi`Mw(L zAlJs679(C{ZGS53dLqsjx&C<8_eL`o9b1=-3H6NkD`PN`b^Xc2>rE*L#H3vI@z}Q? z%Q$~DfpW;cKLyFcUC`0nun117G>!GgJGXMH&1Ih~($j%B! zrqTLChVjb2X!e$X7t=~qZ%^QfcnknTAsrK(ckEx>UxluqwJ< z+H&RutE66Gtx?1S>=~MfDbCx=nt%mo2Yfh3s7A3a8bO{8xODzBB0>XERG8a#o&4(e z;!Kb~#3^*te@iPwFiD4aQdD8@+fzKHdGez1Kv#`jJ<;~g-HXZ_eTD~u$bX0+-IMyt(X zmUf!LJf1zb(TZajqd0|8LTLLV6&4tuW+T-DpQayqmA(JD1>smzDb9B-F7y?^|5gK% zzUhSi+kO;&H>j`s!&g@A_;$xt{9(p*{L$=h)!*NLb9KhIUv_T%_D-|;?ZImMf$qGL zR?Zw&M=bvi=KAk)R&O@3G%qw~u(nU^j?~jrVqC3)Gxv!+^NHCli-cK; zsO|e=f;jV;=bN#1H#W;2qcbjHBbF7!0|1lxy*sqeLgZhh$Z%0gzUhjFoi8ge)Mt_ zFcv5whEU8QS80hE(!PiKtW^>RK~44!RORePRUT)F%l*hpu0UFfFYGT`;~v|24O z&Yf}|Am;)9Z60vv+Q9VsD#rb(3Y@uQgU|ikaW+?jQ#|*#5<54aoEx) zGGzPWR@lDq0TbA}doJT5USrNM$K-<*ob}t%qQc?6Tx8w4ippZ=N_cw=q|LyHZzrV#DpM5pf-TY2!ii1#)aUSaEok;Lqg*evx;@Lw?U~Mqz z;#-paC!L)Q6@7sE!dxWV&-yb_*#F4~Xk3{Sco0MAJs(;A>=A_07cdscja!7Gly^Co z^e*R$mZCa+xl|i0&-xf8S)Zai}iw!ap-T5 zZ-4!<{NH+wyIAj6rl-Ni%N?7Y$vt6G;Q)Da7qX}SYL5v|v$ppv<*6Daar><86K6Yt zc0Z{+hIxJ%CUbUfA~CZQ+0z?OyzB(MBgV15H-_~+5$}&kJBKG}_aoV#8?y1=q#Va% ziWB%v>Pg<7#xN!Gebog_Wsd)r#tm!qe%ND*f_0e^QRMB)y3>Z1@2(&ETmQEE`Tnnd zcin8@X3L?D_FUwfBA`yNMIz&YIImrZb>4(H`;~~gFduQ}XCeLq`v&J`BA&TH0{e-H zmtJT8fIWmuuOiv*HT=mB&deqaUbgX|G#84r+$ zq#|oADs)>=olE|r0z2}T$B>&m386u*xXe8&v2jr-D=${u_~M#e3y|Z0|050}`-S~atiBuXk7Ip*Jm>Sw3W8up z?Cv-t&wZ)JXOFQw^GB!KU}V|_#`l(ZQgs$jh%;Ydjwe%&VHoFVgzZ1BWbB`2!MK3^ zdiDS(vwt^(vBQeOFzl{Pfi-ynB5u?m|NDWGT6}GN4gd8or^DZaF~J|YSKS;ejp}LU zZXeDj=B8M2K5!p<|HKkHE{D=#K2nG&iaY-rbAs0pM_&+c`>Nyv5-+l5X!{BhZC^u* z-7Kg?O@OsQ4SNQ{2W0v!fPr`c!9O5;Kz`yf6pGs55B-5s?HZJjbD%hDFG}?1P|CUJ z%AypQjH#R%bm1OCNA4v~M{`HL)vd4Op5XmF4<5A7{(|%4b&6Nu&cQ(r$6lW6;sdG1f3St8bRCt}^!B7(huh>P?6Lyn1FG>r8gu$A^Ha@r+M@P3y2|NAAxfPLztoLsFj>ATumRq-e4;6LaLRhP9s(z zVjx3A9`J%Q)pxo^gEe+={_xaOKidCL-aj(+v}FI&I8!rQ>xR_@5!hFugd2U2;_F`2 z;2Z0!_~Qe)H~ahB-K(y*7w8-F!eC6WfJ)$tJy#&cVG$C}za!aywB_AcK-hlFg&E8X zW=i&-#M)uID2W{sm?3SzTnRg<4_^*L#CsAaSy>9}g31+=52(zHnpUMocbkj}kbTNspCWq0wbj-3Vev6IPTT)n5ebXt zBJ60*#bR=2&!YXmP|diXbMe#I-y2gNPY#|K&ginnpC3p}j}L2no~-Y?^2~R@Sj~{_ z-#zz-^V}bqdiUHH{k*ZsCnb4;Gc)3~sjLNx8J#s*J~%=wo-eUQVmD9wcVGW%>eXLf zYp`tQjB|n31&XMni1OUbTwpa)h!cuC_YUH$W+V2(OvIdjg|Wch96;0u5`_<7z94*n zg8c%OJMqIlZxMU&4$_0(MMelQgOQveBwnyKX$9&S3)CvtNIWC?s!gacl25GA5zV#n zFzF-V?QD-o?{E|uN^p(*O|lK#-*fdr_wYw3_^M}s;Cx3|f zMchA{asQD04`VDaMtcq~WjkRx`vtqI65-Uvc>mj8wBZjst^p6^ng7?X`Rzyl_;rKj zjSe+2dalsM9D?3!GtwPbBH3mE5-z-rc$?QG`;W1DS&9K-tX`G!f*~J}#2#UaGv^OH z-hk4Jy#n8bQ2Q^2F@!UO#0yr$E=5)RGE^p$i%_{5!v6Ep4shS%ITZ0aRhOwaFB}Qa ziw{4tfW<>VYk&h0tjadPJl=WOi=wH@pF){OHl@hI*4KhjR(H_Bsp zQgIX$Qcl1uiSd6DeE{+MQ&Q~lj>d<${7_itry`a6IQqW4@vG;yZ)|F?)MjvAC(;_h z9$OLOvNVa$tQD zWO>e!a6_6i&#hbMN#sQf@-LPmD@J3}2)1&76jyU%f=P@#cvHuVKJ%Qo*P0CS8hVi+vd~I^74Q8jgVw;{j zz)Q7=>8nE7?*=h|?;fgrw6Cket;7(Hv?vSm1?@lr=K|tw-eN8=3o(|jQaDp|PT&XM zAa;QFIaiQGAECB?0~*ISIae?nX{-mNdCiB`cMr+$shdp>XQ8P4_>^~b)|2WGLyW0e!lRD42OGZ zHcq#2FHdb6o~tCbhqiB48Y9{M1aj!#t@ZhlqsLR?<{70IwojWUp6>~s`H$1~hb5oG z@FdRPr<{`d`$P8sn85V12QX260nd@kXGu-~j#hDJ0QYd}{_6^w@jLSy_`^eW4*l={ z>Ys7FGq&K{x1T-F8Y2gpTij3Cz__Y)K)5GN#N2NK`s{J?G$kqe|7YJotodxjThJ$1a@LqL1Og)@u!1QLuW^3h4XH06dV|6Tr2BAgAcz>DaN>lC z5t4-5Lb>sqkgqsE&X5b7S@1$$mOmmReG$YxEfoS2I4I+R{$6`}kbZukzkfzfLH-Sw zkU+R6#lVR*|6{dAtSDFGIdb$oQ;{I?agOJ?FWElp$D_!%J>1}d;pE~RPCh*`uP^X= zW68fWnzMOB_WvYpej88g&ah&0s zs=kD`bzb;1Hy)Ska*%tqtxdirf4U~O2Ch%9uP)H*l%9z9-;X%X5XL#oK@#)+lrzKw zTg*ZdCDwwq04w5#$vq_efWQnV+7Tll=7@B4!7(MQ_D2d{j=y66 zqmmB8jPd_O`h`iv{Xfr~;a&C)j@4u!s=p4+-+yWTYyOUZ$=`P8M)#_kqIhUy&5;(e zg?s|!2)XnQQZ0!ErtK%5ego0wuOY&cI3e$Nf-=pmU2>b-JhQKrEA zB!!a)Ps9B`v<1yu7*8UG;IOBdX-#0$Z8ZQ$Uv`)|dVp%%09X05=Ik@PT z*vG^0Gku+18;kN{kQQ?iNr4|QreB2AOK&3a0`2|mYls&!16FS#%JNOl56zH7-~^L? zf)m!z2V~F(=(vAC?>7%QA&VrQ;T+BjJf7F948L&6eg2zdlqy4ZH2M$X)8p@oxZ5szyIAsTkrch zy4TfaL^*w~Hs)j2^T-|U@HSFx$t@^yf7*Zax!Hea|6+D%2qP$P!fJAgrMr`7#ETdq zpE=|xpAW$`ZV>YVk(?Jw+Q?ny`%sYOg`(^@M1?pZDK7YK9q{HC4}Cr6=Uk%)^*z^a z-k4rgQLYG%jKDEvC=QgV@iFuMS)A<=wSKX4YpmG6!<>I~wmZfe$-~cj(P;ATjwKhT znCTft-k!&a=bw;791nM4{733B>HPoCRMz?^Bb3K6mam&BtuT{&JhtXUz@ za&wQM?F-H>Vf!PQ%ZvH_-xA9&Y=3;hsk^a1}C6RhhGOFW5@BG;!p$$G!Y z`^V5fOd(JIJ9=;IVZGnK+JvgUPWyZDH#{t#@w+c>3~Q<~!ld#*BKraH?jMk2c!3lH zB(VOMBzgeE1&OmiUY!5DpGY4d`UT0X2d0XC0P%rJ(G&C{F2I-E0o+3<_K)RrR-iay z4KV@xhzYugI`XupL^&fqIza3Z#=wm)9x5E>!=h6IU)-6VTUvs!#CSxhQgNmx6CaY- z<899KPUB8#v3GMU`~2fs>lfVo!uFpacbDMg7jyi=_Q$0%_9s`5;OrT){}Hr%VgJJR zN3iBUo}As!a2{YG_xT;>%vnrFCECBZA)oz!4@9rNo1T2#F59WugfFbHrn8>AG zQxJ^wBrnPSv$MGax%Zl!1N@EG>Ot@!JvXl}$|%f-UrZDtG%D<>(_$%ib3Ioai77ec z=jTpL!QnAZ;^ue8rksj+0Q{|qLu&u1oZ{p9QsIsgCU{Qm!!d>oVGk6=my zu|7$(efou`m@B+YjPUl{Sa??FqNcag`a$@9{?&d*v3IelxKOW4^g?RDVI;V|k67}G zCtrAxIKbzK4SEgHXZ~dW3FH%#gmc6q1{lHx4dH{e-sBJS^ zayFT?{zyp-l6Zm}nma6S-jQ)x|7!a1_xwJCUw&cM)_-M(CeH+)s0c)+tFfK4{qxHc zF+HFBUEG-~_HB>luB={*(JThVeWXJN1O^j~BRp+P~=c{nY-aCbHL0 zUoerm!LzIvzDBI@KF*nhv{j(})}Va$|HAX+N<*DhaR&E}M4ske;tfc0oWtE?#Qj)3 zFWG*S1>*qL1f(3`2m6mFHXw;R$r1#oC^-X?nFk1LU>fnkI`4(ZrVq&RTf!Zr+)qj# zf+Fr>$j=IbG9?r$l?qk0O}KIU*FT5(_t?{e__OWJP3Cc#8rWtf<3NcLa~qVHSrUzB z7^hD&lB-`w&R*`{9HZkLKl}W{d5#a`Y>%*iF}ou${L)+x@x8-{?|p*y{#e2hJVvbF zZ;1DKnzQ{cvgZFvx(gO@4~0563$>k{82Ik@({KL~a?kG<_<)<&yH}NP-=Z?k3yIuG z7~`~-J4MJ3a^WT70AE7n*%{;sm?8B6L_I)=s0E5Tpx_P@wLr0BK;^=|;APeWz3$?J zb%6_r4I)QC)F&uXevbNlZ|Jo?+#wo(((-y-x$?!Yo`>Nc(esA1BZK(cIH9oIL%)_Mad(*AwLHev+~OPwih| z{GVj}|2SiQf$tacJh6Z?`Ezd?ys&~?VaAFAwD$KOy7~Pdhuwi}|G(tgef{ON>5X*- zdP5rblX6Zt*<&;JjK0R5LNizke3>`^_5&%R7cf*06t_|@fZXDe4-mUWI3FPP4W@CY zfYz6L1vn$1^W%I_*cO-)_o0&5Ze6-JqN9D0Z7M-q+ci06`vv~`mt5n2#Ls-we|?70 zlnoa}6fE<|>0h6TH!EWCVr~$o>%A~F^D-uIuE$L4C~mNy+ z-x%WgheY^*$C>vJV-6{N!859Jc$>Mya_;y%%-P`!6zAf|1{GEE#*VANPl&3^S zl06iDyU77IPr~~M4sp@{7d=4<10c?R(GMU_NX!XJy#Ub3t&%B;t&;k zMVJc|gnWpcxKAW3KrVMAC&c+6BP$C{ja~9g@ZWje{txH-&}G7zcyD3< zyJ?@Vl|L0@Qk4z?S7wvs?;_)9vY)A79&v&9 z9oi%E*X&>TfT4b%s0AoEACT(wCi8%|q&z_Q05K3*^pmao&sipXdQf_+Vl8L-GS2AcY*l zsiF?xz};iS1*u)%=1vjj0G`AJvlf^cx)vtRY!>O=$qnugr79k^m36pv{cE%Tn@^IT zf3Ri+JDQr!6FKK?tBsfJ|GnZkJY5usiNxlQ6*WKF|8UyxFz)Jkf_eXAjOBl$w!m+R zD>Rp{sksL$^lphP)PGZclQg57(nC! z!v2M1gnWRk*!{@moK|L9Ad*v}QC3ur8v|d;IG}rU{r;K$@-}z-)N+@Uo`-ZW;_XK(T1jc7b1m5q7 zxT6yH$HX*Syh={M4|BudRh*8B&ITFle-Cx1um8&Ia(=IwDXvHjIz+zE52U>SiJbk3 z5%qtHhyf%l5N{Ld7X(HynKJ`P*5nW1{E*lwCUy&|SO-wk2V@c#BsfI%p{tjZU&H`H5(FMl@&UvF&<6zkYvd;?j*E1(VE#yw@{BwjGBA_9fn1=W9bP_EtH!)y4@vj2fwH>c+n z7r|eb3aerjmJ##wCTnz08OYyHjE@;-dc~Z+8DsdnTwbRoj&8B5L&%uqV|OKq*nY~m z#JeJV!0^Nqob$89E8PDvKRXb+@)D6=hLJ!6^=J+^q#V&O!$30BWzr zPz8O6Jnmx3)m}zcY6uF9I&^jRZv1EIto;ApyZVBd`OPgVlO4}scarOC zA+bQ@0~7H-?foz9Ki-PFM1>FdZ|q;oI)KV^5tM;zk;58q4)?8RDuR)x&qPOiw~PU} zhd%sX>cijiSyu)JX6F=|ezgA;ocEbYevgR;CyXOMhuF_ON@I2souLRgMBe{p>;t?l@c{~MfXH*NN)p4E|E|3Hryb+;6M{2PaD@r3K*1HJq-dNM zayR)BsQuOI{>%fkXm9S2Ioj^=HT+xb_FlcWr~m3AL!J?SSqg~r|2^{W z&LHQ!u>bMI`i&90dW8?*oZkq}`H8)K((XRSgVOFk_V&fz-k*xF{jsbOOyPT9ARp*r z;t1B~M&n|h26c_~GLO`K(3_sVj*X?nZfFwRkQREBIKCB7IL_nFaB=}qgbxrB#~H!9 z!hQhlK7sXr!4LFPp1@Rc1gXduD(qk7u~f2u1F_tBN@wU&16T)4M{|9Pd>!w@b-Y)0 z{c}I3r>}33!K6oEW)f^lR9IaWi#Isy_bl!IDW3mRi1!gPnf2d^?E8y3e`(gA_APik zgp6Y!P|W`RRD|t6&%Xbg+-bYs5Q1Yy?(Q{Zp{21|#*W&N}*me%$FV3a?&m&Lh0?rUE;%qRv zMID#W2P}o^@(QT@)L|%wH(LYhimzBZTDWhZ^-_GIQL^) zs>b`=>oYq)fL#18ct-Ci`2fMyGmU$^rmzP%nQ?%SN!%ABWFq^4k}x)y_!F6yW{H=G z8+(Vj|7Py-JCTzJzg!(!n_A?!@jmFx)q(yQWd#|~B>TXSXbofVM#**)9Ty|sexbB8 zB%ZjSWbO_S_@G3)`I5aSJ1k_pzeo}V?Ox%sl)M1%vi`qZl63Di&;)Lh>|ezH22~KU zIUibI-GE!Szmo0$K3vOtVb?$RGy3}aX6Kj;2-l^+sXQI)IQO%hoG)vL{aK-N!}3fQ zEaP6DWyArzOB}#b#`8j!kmq}`=HidCkX$j6Xf9zPWmeWjyk)S*D((W_Pt2bq_Xs3% z#z4*+?_KTor`NcDU|>dhp$-|T{wScG=8(T5({B@f0C|F4S91r*N+@}&c3aKf0Pj;& zuB)WS;<+^UHFxFm`_OoNz**pR#Q&@%Kj0>0hV4L3;wj>Q-B|++r~Ru?Tiqzn0YBij zFZ4&)zbV&<2)zO>l^NJrpNf^G5m=Y+hYyJVU2XKZE9CoJNzT6&<#vfc89GqTJq(_!2 z5M>#jC{8*EQ`mN7`EQ2SX9KxD*Kr>(XZn5CLrc5P@cIy$v~ltH&%}2FXa6=LH)soT z0ye`G{t2>U_9Bmb&ZYd^yo@NAGSX!G{}tH2u)U71Ya0#OMTpa7AgNr3psIK{@%%qW zzAp=-H_l}H;B1bsbpJGGcZHbiJ#mVB076b?UH+?L#n&w=@Y{)G-y z)!99;(U?<=1Va{5Dh&v&O@dolB&@mDQ}_TO=eg(0lH7koBK(2y3;&&5^&>Z zO%fezKtuQc@>aYyBq`(!Q8NBFaF#HqSP6Yz8g+-W<4q0n`F|hmU&Q|=y#YzdixMRd9D& z0au6R|A)9Z-hJJf?{RTn4L_H)@OAzGo*wJqO@8s9=tGD}xr8Kg0O<5;G|PS5M}Dr2 z{VTuk_TUY(rovLxWvbC@N=9$C7kZTrl8yI9orRFTXiIcQSV+g~R2c+Atxo74pzkT?fc+C4lBAy=-@!lc%L*VE5gZ~NqG0`4hrUjrw9R?$Jik24V zU~q8I>>vGi{@VY))|R$IDzy@R;dTg)wSA*iu>aPKMBLJb;13EL{4VUMWYf33cK!eCy?1n7)wMo)?s$LP-(!sX?(Zga zuw@Jw(-RU12@sRyLPByW38C1y_bOYKWlJt9maS^b>b>{gd+)tVwtDZq_wv1OZAtEw z1TGNR9%FhRowfHl`>Z{`x#pS`gO+tZ0(8nvNLtS9e?oU9~tc6Q3S|AZ6l z1A5@t^Vp~SEDj!i<~r>^xE=>?qWuS+g7n^@r?H>s9XRqVjvjdqM-Dv$Wz`p;a^_{6 zGvwZZeMUS2X%pi5-`LzD=l_0#&+xlipG4oU%F0SqCkJDIHRM#Bqv3nv8`p}!1SbC z|LYs#pMky+rJ}-O_y>B!!dVkn91cNC|4pcbm^>#+aCb2z2<4ldapVShMtWaOrxlQpBiE1s3V_nY%O z_Kl4yUAuM-wW*;J{ucw!FX7*83k0SYE^m}bV1Ci~Q^4pGKpdAzyeCMUC(d7y1B+L- z02X@yM>UN4=%a&isj1%dDI|uYrm-F}{=d0r*g+podRhiNyj);nr$JqP2Tq@V4M$X- zkubkw&uVPny$U<`Dq+XY)e>#ru?jcSwq1`(=ZNzJ?bx*nyY?ty*P$n{PxV;||CjBR z;Ys~JyC5Cia{aGw+=laA$hE%7ai#KxI#kg=*A?r8i2!}T=?K?yvDSZrMWc_1^^b{r zk|AH;jl`HZhv(xmWdmvc3a_=IE?|EI^Pbum4z)oE>&GX@2B73x1vGShI*vKB&oF!W6HFR@DA5dMfS_5*gB6-vd5=26%%zW|^Jh&y zg`f%MkWJWpj0O9>fEzWbt5XT-73V0Bj;x+f>(8nRz={U7WbP%0@bw)gXP zgM+gH%&5C*T>2x_PrrZzhm^2q@1xkci);PPhb1{6t?^v{SLA@ey+q4+7uXlHojSk{ zaSd$&`&3@QY5n(M<*JTQ#{XAVSI|iG_2riQ8Q+gN6urNjS(me&&vr2265y(a1&b}1 zBaXie|FgvY9M^s!E5!LL_@88s*`)0!OZay=3pS@i7h_Xny=)N@8iXSf?4%7)4Qx)0(MV4uI$OZk$AdWr>8NaN<`;dx=Hg%Je!6=)UdzvA?zWE)>}^e9 zX{!ZY^G|V{wtp41rzQOF-cS91FZKA{)aRG+zjfy$cgTSiymf8?k|Y8{{>U}4yZpYSni??ka~oND%bzB7!ET=TY@tp zy-k^?=EqpzQq!USiF^N*F!z1B*SdxV<7_+I%{=XVpKZEM=wNO4n z{O)}ad-gm)U*1F5xtn@D^#DQF@xHv@7kC%6gSvpAE!_Wqwwt=cf#-2d{T&!xIf}qA zN0eR7L;paJjQ<}H{sm7D_B6lWUBtK!+UJM;OfciLpE%xvd9k(|kiH22!vA+W{uw{C zXtkR%fO-ULNx=6sM#3+lBgqv}K4!3YFhf>)CfZwiWnJ>0;H93vzV(I0#rDBrA+WQt zrtM!3b}q-DW4r;U&b=Vv|H$D-v2Wi)k_^~Q9bg6jTX!zm_HUK}EBN2Obx8(%CiVk+ z9%cN`&!KkiJ(yXaL{z*N_x@#uBcp?|-}aw)X5Xi^lORKMDV0zyBc?4gZE&!@pvBnIxa!P556l*@$_r|FhU0N3CS`v52l=U~y=`M`QX=zwtl{8l3@1+wSUK&0 zw$ZO};`DkPQ+W)Bi2wcjAC_dm3jPK5Z^r+1U;py@zaj@VZs&Oi*5KgrS8(S1C$M$Z zK^kL3+BzHLxc`6fS>2Ac@8jRIQ$zPmG*#$!W<+5)#2$-Y$7uuDgM~}f=~3}N&f+!5;r(PL|_<6p>tJ+uRg z^{z1%KW*om*|Mma6M8y2O;Rl!_{)HZ}pij9Vt^dLXz?h#a z8>QHRd8@5hu-=Aw8?OJXC%5Q+6w|(1Sd6j4M2a`cLfv3*bOFAMTdAmR(CrwMeSu%) z846vgp}AvM5^McAxI5sI%{h3o-nWhWZWx&U0UGC@f~xu|992=mp+l>1;J_o24)8_z z7qVc*{=ae*dw&u4cl^xnp=YuO{$bX*R#jv_IyNNhYX5+&{wmM(`*O}yU&m{0rMc*i z@xfvcP4kTRp1VT)iuL~{{LdM@e=Gh)44{bny9xgi_Js^!{TZ;foWTFM=Naw+OfjAA zhr0Lx*cvcb&EAearEFBUv}+7a&&j;?CA=kcrIPY0xO#dq$JYdA%=dHi(}uazHs~7v z7HXQ0L*?Wn68;Y#UVR<^LjT{fi|hM}e{XsHU(o|ra4-1)g)YFjzr8BYLS5%iFta;` z*c4B+w#l{IzAsq)-?)!)%J$~ktJ-zxv6y79r$v`jm@waq1tad|83O==54hITTxaku z-=iKNVuQu{zl{HR;(x}JaX{wG1ro9U*32Pt*ntJsn1ma13O!B&0gNwBaKT85AClcH zSYO5vo_;>aDKAB9-=N$V_)GAAwYph5IxY<+mKL~VsSB3?9XR$ z)l&~%$N#>Ak6`bChndfJ1OGSg|5xJwHqq`U#Q`ubK==o@9c13miC3V-xDQvK3&<&n zMc+V|yifa*=kvdT%Wv~DJDcm2Tk}#d%UXVLKaDBN{mk#<9^fCu{{m~vfwkn|NE;&S z%}V(9Ql}q47Yh+K7)uL44Q-8Prh2$yX^t572C1lTlH*FhfWNx>`(Db-Dam(s^TQ=` z#`@dp!zaR!ak)Ej!Te2V>OYTDXCB1~^@pzGfB&IJCHxEj-}aqLu|2or|FbO*UN_-%jZ(CF|z&Gl89gQ>ol z3u5gqr&F}$6T2d|cLo2q=>N<37rBC8hJR=J6ulWb9%O)7)&-o*Wj)Z$5I9;Jv&P&7 z1O)}*YH@|>o6+{iI)ALKX6JrDvis>6y@<0q z>m=-}oqQN8_!s*B?tKqR_}{TpCQk| zIEsVp;9z_PPG+LNbOf4f>*U&!H}P6?bMyQ8B}K4vvxd6XF*te{AUMj1wSJGn((XOz zU3wYXdQah;<{F$*r~a?{2#(PIcTmLt9e9ZN=i0yLe&+Msihq#7Z{f}z=nRfr9@C&y=RYShl4~4ho-|^+~_WdKn_YPNP)DI$s?~Z@tzhB4yqWR`q@z4Ge68^o8NwL7Q!51)_Xp6BdZ**mcBg%{Gnht$g zb`I<}k|gJtEaShWx&8g}@^alk#`$UJoMcT;*5C{>M@Y0W99(w8(Buy|Z}4-R(|rPG z&aQ^K#;PyHzo`8o^#2w7eivSY?_Y}}YA-_l{D-h`IE#c#KXmlT`F=k(xzX8B zsokE~__}sKq|U0&tCIE&0VulmWM||H1}9{NoD8%@`-h8i0$I+W_JJbKHYD z^6ISnUd9!$9tiiW2xCrgoH3AO0~Cj$Bf%FwMmn%CxBz!&C*6IGy%uNAtis9DkKp);hjCct z(WUkO;Dguo|J(QfLI#NSU*Lb^_Ei$?IQSHfY0&O}aWC9_EKzhdGk;-Sex<&zV$&aUt?+A=6}rhu zDeT8_h57s{aPlxkOqvY>BTmD?>r?swX`eTK7WxM3p{4cME%-ll>=7Jb{O_Iv%>5Cz zznk=bvHpv-e+B=W#oE7PHDmr*FNC#%)pdSL|AH!_D3cpI*x9P_~E==-0gEnuEL!FjR&7x{sr4uHTvd&j`^Z&)y){%=PAALIW;4S*S^ zotX64i*bMU5MXa=gt0e35PbpBtTz(x02D-Fs+9d}eVy5d;v9^OjNrq(*22O<`_|Uh zA5~m{i0`eZ{uLP&2y=6DXlQ7_kUo-_B<4=0>cP?XW0*%IdeCpyt@Pko&|q!-u+VxGGHKZ1Wz17O;5 z$1V6@2sr_SYq2gELOD+!A!@L6eGf*qFVGM0Gz^W_vbWE}-20QL|Ep;{ zjAO?i!~tdI{n7TfYu|m?A+W#eUe@#xdHwfc3;q2!EPh zYV<>Mr)-D)v0}d;P-+~R+&EBEiQ&u$j0c-xmbE1(%(r8XwZ28oFR2#j^_oC`lX8J( zsUJ+UZ`|VL&u9x^+@Se3#sM$}i2dg0U6@nsbpZ2zj0FriPW}HB<|8$+7;DIQ0Bazd z{fCN!fzkx{*_%QCv^p&GFCyGO0Hyf_UM&r+Kj_@E-p>A)DoW~1QxbAuWoe5Or<9?k zdk{g9`bf#qLvY+K#`^u9`~Fv9V*V@)Ez?>cT|4w_E2fQD1v;ocs9>r|X3Cx6@!CaIcX5-C(RQdyReYqEi zMp=3sylkzZapVvTbW zOAQAzgIL4c3`5T9m^R%*9{^+fY4elve;NNT>Hvve0iyo*yf}*fAnXq$`~boZDDr@p zbbx(W@H~V?|6|<$t70*f_>Vl#cpxLpCRQW*R!1!R@V+U~gyrz^d zoRE@=s=9j3zTpw29|G?U4UFB}(%kxfenC!>e~>5iO>~%Zw-=h{_fY58;ks{**z~jT z2-*Z&&o^Q2_;VPWt)o5Qaa_<}15NEUID6)CsHv}oiYjydk22@)2y=Z8vW~}r`>~C6 zzqW5>Kab5zw0#R}0dC>@JAaBT+!K7dllz0i>!7UhJNo=SVU5oV-1nz&YJd2AE&s-^ z_>B9SHZD{oa*cMtgr_DJtPax7Prd#w_+JpUfJI)A6c5B&0PG1i&srd}!gfdu!AKoXsF?o>Ty`v*&{eS z0(r$Hq4h1TyLtu({$*=Xb++}tb+w{KH!U?CZnSxb^ssum1IEd z?}M#7`|mmZ8pc-7z`*2j+WjAcuAUOkX({2%>9yP|JjQ+h8VURR4zboBvA=!)eb~xc zUn}_E{@J70wwboS&G$+8-%7i{*8TJooM3(Li|^u+^&y0_K0{m2HBA{CUlAKW5;@t^ zQLEgQ7maTA@fr6xk7@J$v;ne50I@Fm0IVD(KY*weCSm~?4=DNwEwUcSiVgq=>JH9~ zC-$Tbz>l#&!Hg{mIgRP?bC`*~h`HoTSWLIYT!t%95&>LG#&Ag@f*J3wt9}gXCyqkj z$N-)JzDUc?K~;T&W?M(+TZ6+R{}Q>wBa;L7cK3I^R9D}uTvA$T8XuPgTN?+)?H=LU zy_vCmY6y(HgsdVxq~xj~D0VYk{NAG8|4ZiKJ_TcQB^a8ng8n5X?)}$DF@L8u9*645 z$Dn*viS;@e@5`E>qUNvY`yuN1Z=;=m8~6QNHw&Bpa(xd`?}s_QI}bmN!>2ajg2`^U z2biItJS$(uzKs3bd0=w1>!pdR%>4004=e;SZpWV3HD!%YuJ=OU7cxN53jP;`FHqoL z)B|EYuoWF(fjWSc59o1-xj@SNELF@0Ye@K?j=F%E#HD?}Oqw(1@`5nM9HH@wRJ619 zOr)P1G*7G1ueG0XuxD92zyaYgvB=FYz_scc&7RKg?`?kS}HM7j>1HAN4sFy`#k1s4@hu9DNpxi8@$J7P$deXbYgtvB(os z`LqePJMQwO_xFbFx4(x$z-`Lc)v9Etv>HGA|21f_( zZRu)#zq0l!QZus<5E2S28(W-FKaG96x8bz-895e+h*v>a!e8MN^d{^2 zy$mboXNdi^%=3E;CKhYB?|%sA^;U81U(L86=KCLi42O@cmU8`e9U!i`_DivT%YFWY zzkj8!hp6i%XbXFS?mqGYj-B}f49pHNp4b7`>dOp=M&(+J-zVPrzTL}oQ;F#$ZGltF z4TO^l>wGiDN9q;IwO*0|^b1f1EU-ox7$+e10irkXJpF-l%q3rBJ@5q^y7A}>kaB{& z4okMc`H+)Xh&+wiSndPj&SO4_dO(sH7IK`iP#TP({7_8Q=VGun4<+nB??d1C*^?^R zw`(hoA3F*?`UzcK+>n@*2BBBCwRXPM`yKbF9ULO==|ACmubYyU%C$WZS1c^)qd(1h z9Q%mO{yX+@-_JUKQmzm2ztZDR;9u1G6xiRq>tW{jJcT1C ze+5m}aC7n1Mt)f|`l;i6-}p_wpYPXRWO%6Kwc*-a^rg6Ci1j=t*xy&w2?sGQNWwa6 zfr}b}QjWl7`UXV20PBbf86cg@m;i7eAjtrC+5&uN3!py`p{mpY7!w$;N&IVLAx#ep z8CS5FWs7<0#3%^?s*`|*e9Tm5AuGTEP6q6Eed-WS9NLMKs>--*W(+q^>I2+Y=jIe7 zRaMt(wzhS>)!8%fQvcA{z262ub#?Z>RM}8!SX`CwC2;RaJ>SyGg>lV>*t_d6l=mNj z`pLtvv(-dOiW!Pa%#fY0j=1E{5EAnig2LW}tJh1c?fV?_`_?h%_i@UNwJWd%pV}X!U-jE~-an@An4TKA7kxDv^Q94(jkCa`wRSwI>9sa56&1-CftmF8|DQ$?4}MtJ;00g0RzZ>j0swd(qKFga|4of zuH%0`!x~eWj+n~!#!O)trf3hGtIfe^buM};DF{o0R0UHxIGG0*4dG3NRc|H9`d>U@cq zo-I3Q*I(Y}3;b{1ejnF;uK#=3`)lti>^b^8RL=euhL$@K6m5dqrp)|_xn89o6CcaJ z_3P;sL!ITi{jA^78DNZ2M|I3t9m4dLt(X#f0gG*v2dw=g^$-0B6Z)TE%8)w1-@~5G8=-OR2uuz2;AU?Fe{U~DMn$mRcsgZ4 z0g5ZiQC@XTx2CZ{qp_`ZV;ki{XK&w2y@Nwaq95Y$P@j^Z{((NFzW!b%5tBbOG;~i- zU;ldHZ)k39d%w26LHQc{@0FI88s-<|At{-DiXcDkLmXgiYz9?T4N1@cF+x!{C(8>575^qdVGi)A0o$Rn~3$L&tK}_M}1$s{&XAt{5zQE zw|5P;?SC5kj=h1?7dFDxn>Gj5^&636{1vi1_y^^|H0#0CXXekapU0x_1+L|+5n{fB zwZgdei{7DPeWz_;j{3kf>&OV2y~LcLD~tyc`vp-SP{;s#$_i)32fMRAfG_s}L8`0` za+2{tv;_zmkj&3cqrV}|jB>zcIX;MS!dJtvSQd)uvM5Z~q++I}2!oA9$Vm%^vlVmW z&987zupgg%@GiD}`VmghMxb+En|Lyl_6Obp{#=36_?O&nRyw}3CC#X+r~N`{{!F9*Wls*b9neX!+To? zXSc`M%kL?eSw005vnODD=?PrWTMJG4`cG@DroC_}*6*nDD)#t&P{O~+@e?sUTN%@{ zb1(M+)c<$V|0iCHwO`cp7BxK&9RCGl{NINKd%P!Qc%r4Nl!lyt@Y&1f%kqLeFfrWq zQs1?lq>cnnbaE{bb%GWwcC-FJ*K}Lf{I%K38UTARYf8DmwSS870yDG?2wF6wy}+Eh zf(>JYB^f{+K-38KCjR}@0Oq&~e*ofjSR;TkAccBBx)JeDT_D#I^Vw!tETBG6=7#xF zAIwyQ0`;jtXED%SfsTR%L^_(n{IoKR8B?Qw>L672ZNb6qA47TXHrfpL;xz4tdb*l0 zH@OUZ*41*hw}Y#r1KgY(C7*(imnXcuJsF$h3TGE5I5^tF!p59_I%DYS>);IQaj4Sv za7bl4_uU^;_x_M}wNL1a-;UF#_v5l5eRv^P*fY-!)r~gDuTV!?!44$ny^qNB-y$sW zRRl!*jP?7UhKtvF>ikc^*5N7G*sW)7FMWMio`iw%6U^@uG5%|r2dE^)_zHXfA?o`F z53;`(_x$_z)0QuMeqz0+&2Ja`eC|8&xYX}+`)>C9rLBJ(bNhB5c^PVF8AoJ$3bE|t z-Pk0@`N*>GYs&uKI_25YI7~7(ZQA1u@yxg&;vLrX@e%*xZ&|VRBv_$M$Z<-mRbD0A;}; z+6#^x+=HWs4npO~VW_DbgNE7(oK-)C6USAc#&|>36US*|Q(-@?qf5m2BNeq{w72cW zomxqN2Sr5*s3=LIzVF32zDr0vsQ}nJ+J$i6z8L^zFM;PsT^Z( z58_|+_7Lm7u=R`D{$kDF&-y)w4n2v3jOi8qes>W2JJ`cptlFa{KlmBI1NHNe=t2|m9jqwOe?&oO_v}C_83+DP*5&O2>*V}Pjw_^_=kq5|F zV1a#+M=TL*1+k&NoXpFM|Dm-^5Oy! zAI$ZgeMn8tpM|>eJ{;P%1$#Dqj9s66g3X(#zkm8EKH=+4eEr$xPnk2fk$B&VeMfdm zb-2vz_23_2$C?}-h|O?EOr|s9b6t>8syTrhP3lvU|zrWDrlTtLtp=D=J(Ow ze^g1T?Xj0}eo{?u#`Fp7D<63psw(XLOWJ$j3FiGW-*4ZO)Ja~1ipCo_Z}b^^Ef}Jx zJPG}Sa$dhI`@R|3w>aIWL<5``(mhxwK$H9V6Sv~uk#;>%``d|m1FRtcOWFWL9Rbz~ zpXYJbVl!r~w`10BA7&kuG3`tl;HivRU)mIc85a=2zCrW}&QJzSr0ZfbgS7+mjjrQ= zA>Wp|fGZYry%;axi^alFOq9f6tTYi5%v%^^{NO}wHfEYjG1XR%;ihu*))t}VD&qzV z(@~HThqRbbB!mPZJ|uw05JZH9At)d~szc=O@6Y%gFYX20nUCjzz;GXg#`q#C*&oR{ zLDcCQ zpIG;I?_s~MW9yj*@Cr0ey@F%P&olOy>ptPIKY%elqr~(S@xS22b=-k5ehxf3a&K>=Onu-0 z#+Vy8M15dP=m9pg2ik61BK8b}2YrH+0iYg$P+m(p0LBL)L5sP<7wHpVoM0|(gZXBd zFS5d1DfbwTWAOP}g9CtJMamxOx_4l`1GL-;dnF z<b+0d0G4Q~%$9grwggI`(yhhQETq;FqNJ-pB6+#sWMCchBeH?)@U|d|dM#o{{YQ z=9aYen>|BY-;>bie*c{IGdOkn3HI`0JrA{qaQMW1%Icu$=l=-a-NT+9$EgFHd=;nkSjXE_kFkfb=;-czP3Bn{`~NFE zFfnw`RKqpRzN9GD_90fi+4GM&y%ZxP@bAp`oR-O%n0BU)&prQ??RHF9(nmm&`~sqP zh{Ha~9stS_@i_?KzCy@=7}^8{{*!en11?}LQy+8Lmob%Vipc_VOcq;Wy3`u8#der0 za>7ih3+Bo^F;ni1x$*$aRs>Q$gkqs85_8otn5#*^Y;6Xn8}cwsTft;g6~>$EFx1?D z{)SrgHPMzx|6&8rzgFpq;!-Q-8(cst*Z1VqeMm~(g2d#%BPQX`h>ia}qT^me7}xq0 z-23{!056|s;pzRXwC1~dtcQ!+Gq7`7M_b?e>vsN2CQs0Ru!jEqN1?5?O2Yn0=JlLl zJzpUM4j%t0_4xm!4eux1e^3ulS;u_epVKGsJJtaDAKD1trfu+L_WF2S!vANglnuHs zufqh^FkQ4_FfG=JXq|zQBa_ZcK~)0OiA?`w_+nGT%tZ0M@?} zwF4zNz&!zy7$ZQt05WMC%%yFx(3CZVgiV09h$36s1n4s?aiCqmk>rYnau@0b9+)RB zl>1=eY5?Z1Q5MvQn4)Yf)E80?lwz=@0)0I8v}B^ADI7I5cBs615&5Mm$Sl~5_>A|M zqw_n2C%lgE*q2$O=LLy^BVK@i$TRQ_e3tq9Ps7vqDf;!+v7X*j^y{yOquV;zJ3qy^ zAlmtC)-j%c9c%bLDb@1O)nh#mUFP(&mZ!=o*78}-=NCPEgpL0o{dX#&C+EqR=@0lV zbT9m#Ho`wM7x*38^?!|>2iDQg_ZW5_T94f)U&9%LO|Wo1%^KeR=%Afe#=gAne=9t| z{8rSZ_Jg+XNk8`bu{%Y5pL#rf&I14N)xbo+8P)2H$4pf`V~R5|UspiiLCG!nZ*L64wQJVQE6_$} z{vq}&{yX>n@31D%>j;c`1wr)T`Gr2ixcz4tQ}7)8f}e&@z%#et-&OMKuY;ZQ_c3~AD~Z>Yd&N8MO^Q}Bdeiud_B&deFYaUZh(=& zA92O>J?nV)U1~}!VOboM*VDMSuUmN3$9h&f^P2ZpU z_yA(pmofjITA1@Vfob{yMjQ__uHXQMxE~mEp-q8%0df62Ylkh;N4OLxuxt~ceNgNP zgg&sCL;GMpYY7xl7L=NE&9_*hN*)DWwPp+v?G)EsxEJuDuON(Te**0RX|xLzVzj9m zgLReYXMAB-T>z>pFQd5VIP>#9MndWagvbA!dHT;1^XuUovQEOf(Di-&e?fi!XYk;< z@6L7K#pg*lx-*{7^$A$oGmf9J{Fg1BkZO2bG+fI(9796*{XDqVYyKXF=RbfMW2h~ePc6vCdJ{}c-eb?N*O|ln9P4tovU1jf0apY%L}$q?rGg|Oa7h%sgY z>B|%N_rJt?Af!;f4kea@sNYjZnDag<#RN>cD@)gpQWqF!-rywb2TTVXr;mWO$s>hr zfcis%7WV`?m`=NZ=`4NBj+(vYHug>b1X@}fSm*m0>^kz46w`l5 z{Ws8M?=@%YS6R%H=;?2e_xrLQ@}27;y{%WZ+t@?0HP#B9zFHV}cr^F$(R@FK#}$e;!gPo(#(Ym>&_fL)UMDc>t;WA8Qy4Ee9mqbV zA>2nqP-aAFGdGDj#0eLeLqOX=nm%&~=_|;gji7*WgoQj7Q^piAmQdIVgngileuBzS z`Uql3=@_gjL*KPx^w2KQQRR*5QX`ZW979g_r$|ov1HvO;qFw(v_%L?Q(}(eUZj9xj zFVDf@McCLp3k&bUfs{JcGrvS z+r$37+|wIzKW}0AF)SUoz?MBzM2{~=)=qY?W9?toPO`V%%6Q^Wpri8^ZGgXI%;B@t z`G3Vc{&#SmHGDkT7cjRn8a=(8FUjlPceL)wGUryAGu5j!*ID2-T^xY1s7uuC8P{*W zECa-vA7Meio;Bv;tT7Q|hLH&F1LFDK3MJ5%mrR@rN+f3EsV{_SV?I<1Gr^}Z8E^^{ zekU;Lza$4{Sx;y(N)uDjT9}Gu+(10*3no)0r0LREpo@iE%7lE%gM4Ew=9^%lz=m>w zzCw{xR24-^m9XDco{x^Jv1lrDMOmIMax?Z5`|l$<>NN@b-oCW!F@Dd*{RucYJqZVU z+V$<8hm{rMc&ygqGX42Rrb^H=UJV`A>(D&E3a2$0!+(~&y0o8#rq0iC(eMq%`Tq}` z+&0liK>U*2J@(L7zmGA2`z8Df+`GFTWDdb0*jRmri@NXNF#B`wKk_r~7hb_htv^Da zb=kexQ?;-m90N15txuMH-#^(m)Yts}NNE~|Vm#Rk2475bVGe-vGSQx&YEPNK-XW9|GcnW~!VNJJ%6`Q`I+zVy zqNxyVOmios%qxTiOw4^RK{8seR~I_IBeJ?^;)xWJFt2Sk;%MQl-4lN&lT)3aXY{qG;| z%g+RgMb^NB@I%$+Bq4$NcAyDnU1_`Xq7N^EdI0e+YJVcj0rM%e%cas^M-s2+Q=Eu< zSB|++kM|&nBPn;8@;Js?LCB#lFY1ORaPJUDU4gwt7T8}v;D0)Ta)3I4pt)Eb%yW-0 z8_SsDxXYMhAHu~{>Hz6Mj3W#~Q%N}L3qw#<$hDvG1!)P#5fi?JF@b+%zTYdbW2~;g zzB$)Vm~)7Y z)<$H6Ho`)**yrdR0t3&&Kj!XKJ`T=HW3q-=wTD-uX zprQ`=bh;-dQrs|^#`vA=0P5~R(s4dNn0k8%_wb~WFs|9*OH?Lab05##V3dlnAm#}8 zW4gc_6S*Fkpo|#LcESkbbcfUJFhCumH`NK(ezEm#%QrF zp)U6fMo3S)f(-WYNKLU|?GH=tA+8`i%ow2|h6oEaLP)3~<%1pq0xuvan7w|&j1U-N z0Iz`4FtXSLm2r+B6 z^G_>cnBQl|SfGm-^rSy9i2HKZ6v!~L~W3&XvX@+0(EiB4W?bMmb!fn9b~l z5%d4dP*`BWG1m6VGDk+bDN>V6kq~c!xR@)fPiBD_+Bt!a9HO7yr6tc zw3lF_n=!zhHJI&e!DMR#Mmwr7*jkF#hD(+>+eKRS|mi-Lu zkeq3S*yJmW8&Jo&iyzas{|DCW_#=*={VQ|~_Q1+jn{kIeXzZ%c9hs8#eR)0o0q8xG z%th^};NlsF@kAH&g_zS%XoUV?Q;f%R-=9OfTshb7tBIrpzUDg3a|O+o#4i!CKgah4 z_UEsrV6rAzqVa0tois)nFi|Js{E7F*ObPoljeOmbg}D~S{1$O%gRF;4;YM6?k@zD6b7Lnm+~OnOQM`?SJoeO zV_jc2uKymC1#ZY-p0J=y?rBn!U67XIO8MZ1ES{57;EtqpD|iQuH<@7n!LJ1sLG^%pNYk5JYLP^F`s`I zQ2#IFKA?!i*L8ed&-WYnx~YUzj>VR8%(q^{OlK2DT52)ab`?EsrD$Rt;5F_AO6#If zR2`1oiXdba2O*QV&!e3{P+?vq3bP|5%4eRfpgiW?bF&ULj zaVV>gmgGPoZ4CM45hyN;K}mTm%4mx#FXVVZEbIJ*&>ldYfVBaNi$hV)I)ztRmqbt{ zZ60N15y;4NM=1RscC4AAqyI5f&%6zFop)&4`-HvzcEFc0jrkS6Xzj`k9h;Qnd1Tr5 zDc_O|}$ptcbb&#h7KC(CMZE>inf#=gUat%=@pvVmq@lNvD6-H4&i zdglFCp^I{%wTU`FeGzJyBUr^ep^9rsDCd5ptTG;@6>+TR8+DyZuSTP$JPp@Ml2Ob4 zg>}`m8`Q-x7C!-1&B3f0Vuiri3$SK9^EsoBp{Da!9M}2~Cv`T$$o43FBCepYu+YA? zwpO{nU*6-(xc~9s{+4IGNZy}jF5w{cfo9ehDvY8&;DPZldyG&P%tSM{KRu8=0cii@ zUSN_s{CHOtru*uE-byU?RA7Pjytz(dy`w~;#f~!KTh+Wk*9}K5A(XtghppfVMt8TYi4uyv&+o@1Y^~AL}hoMMFv;S`xj{66=P}C>IPco@gG`*UnhnKx+R+I?S-JRo*oQ$cA~ef z8J$h_XlbbCM^~Yx`5J8km1u6RL>ub{b$8dIx3`{g_qFJx9BF7vMR|1;Yy1ZwCfN>l z?x&$|x*u9bJE3u5Gwbhfhq1*8*n1eVZh#ANOH%t=yBl^5Psr=NjGb>DJ3p8?ohLpf z+TMcbQBR#jbe68v3apbamCC zqoW2btQS&STS%WkDsBF;2#vOYv!@R8eOY7t@_wAXxCQ6*wz9V85m>uvAUM_%d1c{f z=%nv+xK4R&womB?^NeKOMb=%uB_5a`>v(Oh>zd|NT|TDSBYYq|jIzKBLvijH=3Zee z$(uGq#tr6CHz=b`u{w?RzHG{X3Sfx){qb60lDfhq_Y6}!&NcxHtyq|Az~X!(=1H@& z^_ZBh#^?ldg2zhHH(G+m?o3oR#v{8dfVF>}5X6`rH^%N+x}0MD&ZD?!c7(k(4?ySA zAzZOL1AA`+_|blu%-G_R>dgF>?i!8ZNg4C9%#+vpugwFC^o7qbhG3X=ZQB_~P{;fN z#s(mtaYAYS7)_)+V2_evailyLNbx~Wk{5dEOBl!vMjv|%_ZBg4t~3h6j2Rkd>_BIG zHriS;xTYtfygU-wd48;?;fgTkFb77MGRL0#c(-%7Y5t-tQTwZ^*C8NH%ugq{@s`>pH4Bq>-fAhB_U*-Fh@%gXF1LHI8uZ@g0ZtU;l z-n_MhF1=i|l%=9EKMu_qk!VN^M0-LYy5a-TM;b~9#zb-$bA-b&&+*E#M8@>QFs3gU zN%3B+=jjMak|07GreSH;6||CtWyr8o@in3Xw^S?-6kTj8UqnE%0h=uhHlp zYJF>Pj6O{nud+@o^T4;v1H<>w_;)WBsb|kpKbT<8!lA~EQVwFrRc$8u7^0efh05$C zRFEpEH(V9Rto+8h?4;VN^!jTR$+*gX8D)$sE@9k3UQr}_1%@KGnEpQc|E@M>=8HXi zTYtH3&uF#A@J!RjiA8>o{H(v_XDzSOGSA68AoGCC12PZDJRtLc%mXqH$UGqPfXo9j z56CWGa%2vk9!9G?F)bYyh5Sa@C9cybNt^we_ULyP-uPb3B%I( ze}3+Gsx&5kVzkyUSB>lVl6l*V!Din8rh}VbjK7Ql&@BRaB-2UBvz>V8iD3tCp0XL7|?FXjb zxIe|+W?1`6#%~(F8w;#3$I@wc-rvnX?`}Wv=G*`B@jEBr*6Z(_faY87|IR0z`X%Gq zOXL5(bhvW@m)`$3G)ph;e9;_pK$y5?GHfu(B;#`=YQGw?GGrG z*Q`vcc>T|euUym2>u-O6+I(;2p2Tmt+xUj%`xg^%`#9gaKBwXdtlaC$42f~AY<*Sh5daZ5LDS(|@-(Rk`z$5*Bg5aSze`Ft@YH^!Uq7{B%V z?;O8%O7A|t@^}F8fbRVMTgR2|xW60ki0g0sB~BFIf8(87{}SW3e&csNE@tz_-LBl9 zmUtok`zqr%W=mWmexH_ft@uw||9u}Q#{~sG~=BI5CmtFsN>-BdZ|1$Y-V?6ba z&lgYRhWrxaTGzi@yts8-`;PHD<>#%}OUvu^Pvqryk*_!2x^w&%k$vljEq$uEWaTTx z&E8G^Z&86|S zHZFbW(hZ8?>(654dTCs`!KGJPD+9|%>87P`mG0z@>zC9zF~iN$=iheuvf3xUKwFHj zOx2Ckq^me>OCNvdcig<_*QOf<%|_pxbgkctgZ~-@%UEM&Xp6C zZd|zhYU#dL#y8wCzBH{X<63u&FHP{u_#MM56S#E3ozt_TH-G+k>dI&Hc<1!oIze|% z&#e=nbeH?Pc>*@vW&GwJsP!e|cNxC<3E%nj#RWHh;N5=S(i*ii2Y35bd$_;3J&iE1r?}DwB^Ot!1?!R7~v-Aiztek%5 zQT&3tPyW)m%^d%N%kTU^`5k!%K%N1427Zt;u%0X8&q&)z|E5^O|L^{fKVB%u zGJVH$&^xP_)B6y0N^imuL1#XxVLqOZGK(cRUh=p7zZ40Nn_D*y4@N9SX@M`w<@k|Q~3yDfjOAoH7tUTV_()i2% zfxiEDaA-i$+Sa0|t*ucsG&L$ZyLuJf-2;lAo*_j`OPgY#zhBWmHlpaA7`=mT=b2mX zUHq0Wxc|Y<4#jYHm!hq?<(cfv%yaJU?zI;U4ROlY0Q=00aLCyj+5w)h5B7&IkKW$) zu(vUS>80~9Fwlj8k-?(7hkIpiey&bSYx7f0y!XcDrf=o`hbP7q{cWv3Pm7HXG`eIw ze#}@82mM^Iw|7m7!j=k{}lZgY?COD82hmCEe z_+3vqp6@Eg`t~9`-dcq9t%Z2Ey%@orHE3$dMO&Ex8Y?y~QRzM&kD|3&2MrYt$cT1= zrHMYST)8ryo{?+WKQQ`}zP^!flz&DBhZJ3{ZLb7)cvYyHUc%ALSZwSl$EzJhc(NrA z>zZ<~z9Ab=*Jj|c>Qp>=Ego;wCc?Hp7sZtks7_Z$P3qrKo4N`0nOo6Vz~55z2^xyF z^S7NqbA>fB66|4Sq6crUfU?G>?%#BDbbTZI-^Y1xsH*zd%-VYVu%9b7Hy81~3-Cls zCe}8l;Yr@-6W3y}sw@Z(7kS~~Ja;^s?T)uf17UMD0og^q$d6G)M!*M1^LYo^LGPe2 z@;zLQ{}i=ppP?>iBkJ?EqNPL~4W;(*aWjFHmHlv4b?pbEBP0Jjey1lC12a>Kn#!x2 z^(@S0lv5({K}QLmY0APAb!m8_Iu5JLL$M~`6OU%vVQsoKUdeXEpNhP3pdtbno6?ce zR*6Rb-qx}JTuU`UX_N-CeYYXe`G1h?_9tWq`~^jke?tYIO-y5_T?sd37lu{-E?`O`~Tc{2@C{?s)$sTtc?&m78*#ecWvuJ@S98zVueJHLx^|UiVUv<$n#O< zZ#aN>m%k#)?!S@b^xw$ze+NZTf5p|5zvF897PJ&G%OdZOI#D6#EV6B+%%5cuN7amEs#e<1Pcrfk)){vfw zx`;IqT6iGxG@gpNfM2IsU~9hDxK%|&XIze3b83)QcZ|ahgkE|V0T+LZ5W_c-VDAwE-zke@0%wyC{i!ALYp(qapts(ui$07uWut?)I0)#s(F`!+oTFiEh9D zk%4}NpwWQ=#l+Ci{|yKZtT>(+jNdnB;?Zl-c(^POt8$&NI>ihR#azTgk!SEg#7R6D zdJ?O{wD5L@3yf;B8Uy;OtqUgmj`YkOq{{i0rfMRlFR8f$Vqkl5QllPvEwVZRMVqdJuw8ttj=TX|YFH9Z(6>7gNlm9F~z|AoMLclUQyp(%JrgBF+J9%7#Xfn%ubK| z+vrf!?<$IYVj~<5z(?l|`02dN=khW_4S$1Jvp*o!?QP_R5F2sCMz%iux&9RtB&?_Gd$iC8t1|5HXq*xG;~XC3z5g^= z4Ub1?<77$XaPD~j$yUt%dlM!VEekV>{HZ=g>HLVIZ*EpGFg3S){}qapm|Ve{LvQcRiYw-1_kt_- z@o{MklnT7?DChlwc=AkyChiYb$Adw~aeufbw9DfUqyiSD+o4v1>t5JkmmN^$oBsma^npV9_lyP-q!X=S4YPk z_dh`1FDolOsgV|n7r6GUqD+4<=`tRSJWoC+zlW;e!H`qfo$EhSH!<+;0A>~abpQOb z_uto3A_Q=*)N!=im42ui$(BmwXOyAl~Xtq`JO^{D|F%54S~m zdD)SH!I3-fe`MtUM5d>dd{hvOM{}I<5M}y9G3W6>*lF_n3H)b>3SLbxLvnkK{^;y9 z*S1MTANjp~VNOxAIIgJu2l!`lyj?LmT&gAQ0LU7qIF?f(YWx{@jJ!)9~jxGzcv_lIiW{vb8{BuEu6Q~sp3*11gccaMAy^$`zwQ@vjxe<~GI zlYNTukxtHkr($I2X6jH(jP)sMtMbi*%|C*V=8N#<{Rim0O1b(PqAlJ)hR4T9jIu#h zP22X{@4uwHOm9<$FIJ_R<9@E8KjGZ}G(-*e1*(#NPGM7)>tgdj*Xu$b>AwpNOmSa8 zohfIkM^SQD`PoeU`xu^ww_;$hTQPVW4Gs4yy1HB5jP+87pZ3e}BOe87{R+Vs ze~pMsuOY$qZ6t(V#MR2`E4SZ&dUUrv1^Eb=?a>e8*_2J=RMgI_Wlh2`n;Za5Z@Yn-s(e{J> z7hZ?=S;0g64HA}rg(%b4k>Gz4r9}nqx8HwaaZcLXnJ&D4p&Mx8f$-DBt~%}yIf>_^ z_2E{OkucEJr|9hLB6WWbbt$^3x3_UE>7AWX3`{Srzdc-kd-}Q+UDU<9dV9W*dIkpm zEiXN!FxceJ@S+^`J^w2FFZ>2Ulttl}e}{O_V<^eZioX5+lM8dxcI0{CPtW+nDr&3izXH{9Kij7087I#BI}}}Ao#YGN|5u=1>Hx)g zi5c;(`w(XOH$<3yNZ$WDVyr$vyxm452b@81e)hH7??1n=I3qaE8(zsa@Jh0QM}n0^ z?nzb%%=SihNnUhqLxZBKzV0hgO=E+ikNV)?-~iX!9z{Q&Ti;ip!R|gqeqLH;c;H$1 zIqX5O<8FjH?Lmn10ff38Mnw1p6c%Lt|Lt6Pe3jLi{v6vnZO7ucltnO5wWCw2Ok0(X zIyge77PL|kRQ9k0MUYLDRT4r%5(puLC1g(^`$j_c^=9AqeYs0+?!MpLC3k=3{Q^O` zplxTSf6Q-&A1~)_-#PC&-}%mY-{*M_MehFd+gpm|`5{my9)axI5y;8dQg-by#&3k8 zv#z{ZNpXjgFby_N`c|Xw6-t_C6i;ci6mMx%l#dfGB9<|7iJXyBU%ty!ay3(4ncs5$ z(t3FNyopmj|Bh4MWG9#5wC^&6MS7vWsVdFge?eb+v5kCno?8n?@H#lj9Ou@-6|xTY zs4vi7lx~ojOwZ~lF4NN7zMpB>&+S%5LVXYtRy6IujQk%TmoQ4g7GzS25x>RcEcPSS z&P~mZ)qQ?!F}8SlVC%N|*s|3FTef?^(`z0M`@e(M&gN)$|M~o(P~){@a0b2yXTU1B zSPNQ1eR&@)igWw2<1i$bpHPy2a*O#sHpwU~7RuoajEG{XaVf2jx5uCLpUdMjBB6x* zqd1q|-_O)^U&nakORvWHVfW|HVdLhR*sy6fHf)@Q^&4km-KHmS^z=&f4)%WT?!Qbb z-l$Ivf-`6}oc^oeIL-S10URM801;l`ri7ucqdi%vGci(?j*;EATd(!>nU)dK{E#Zt z_w}F07cc_;I3tpbGaAaHRBFXNrkl_*ZLRI`!54R9*S?>@bL%W@^qhnBn`Tizeu@p7 zXCWwT8(}ismG1r<4C<#<#R+Ct@LIV1--DIz18Dr6A!~u~ZO~+$MoCV(OrbH))SGOK z&S1S|Ml1P4_k-rd7>%z~srgR-xdJYup2|VAv=0~j@kI@l6^F)k6m5uFfgz8vd?quvX!d(T{KWyha}t69!GsDf@i> zwf-x$Y`!F?=UB<&lCzUjQwO6Gk0SKaYdC!LNqkE4XUq1PQ~s}AKNIV>(%eZ2&}dEa z*=mF2UjHfn(op=R9cs(8MeKxwy$7dh?Vc3p7CR|?Qk(QR;dL{A{;?mc^FTev|9-0fU-813oISZR;-T9E_&S!^SMSD-( zR~jQ>ygKX|q~2jNKsoQLRP_V5uNZf&R_f)PM-Q1K0n}r?OwAeW_Xfp9@cH0hw#`VdaYi($l}m2CY$1p zx%=<1D4A(<(rcGZ&Dd4EcTH(Q2KyD-6C(D&dC?P=Fp7&JcfghLIUMEQa5e_Q*%krx zX8|Ln&~_JLq%IdVH_JJ;%1RAN~({bqTUNPOW#0J`tyj2c^;t`pMkgk zQ`q;#OnB}5DR%DqIri^=0qZH&2?*Wd7?%vKv05i+Ua)iHYjgMC#ro`KuuU*F33rV# z{fyZewHK4AM_`Sku@Bz~OVke7llQ=te+15IKe$@X!POZK^u_^$slZqr98xY+qH?r# z??FMu>&VVqh}4XqBO&n_ghxMvz;g?5^s6WF+5WlMyYHWH(&yiB(r+m|KY12DA?sZ{ zanBA5<)UU9%Spl|ziEGw|C6*&5dUWrjs8)Wch+o6I0{EjC~P-_VW&LQLNWRz<#Sfb zkL?A=VdtEJv*|QkZRdcVRG5cbA?Qm1hx0ZP)8-?Rd=9&^0DUfaM)=daMoUjt1bbK+88)E;cyV{Vk4e{HEZoPXgZ70(|i><*=rCT^IJs5yoTu5 z6^Kt*kCc=>$jms78#hkT^WaZw=Njxe^a>J^PL0X*+&7gvA)_ID&g%Lf@ZU&ywUy?V zN&L9Evn)za7>2Dfj^=$Ht)r!IbeF;2Ujge-6|BQmunkwj!fSw8*bf=65nbJBXlRZ{ zb7KNpi1%n9eyP4D3AHr|xLFW{)3m=F_ov*jF*RLh6wlL6iYT7t{?GbPxCjV$!i0oz zs)tsX+Vg7+6-lr)<-s;o59?S59KtR*$GYL5I!{ixA)&7rRQ3}xF)DCdLF$N{nR$rw%jCa57+nC+?Z%DtG2Y4 zwW$ZIJ3G1xc}3;%{LI4e{-T;|r8TYjXL`7ee^MF8=4c2ld))H`<-{W7;nrb7IO*3*$k5zwXC$zoz?%+x@;X4xHQL zBDg(H)8j@LPK_gOk1LHbjliApW_4Qg$=)FQf1TE!pYwNgMpODC;wcFeRgozOXEYMt zDCTmXsVpu1Dn2QZb0+P&@vE$4T+GQp29+79(MU;-wO&uW-at8r-@wq&F9pIeMtocM zBqe-OP2Br8^-k=~`lj8leI4O#a=C0?MQQQ5vnkhfn{yJeqN5Z`h3$A<){RRVKIEc$ zhz5K}JM$GNT&Ar=HO7)-Z zzm&L-DWhClW0@qZxp{|AR?6gC{nhw|s0kj!rSRy?#C+1R{E}>WM>;Ny)}oIzA>Ad1 z(2%hKwW;r-<;J_{t5^ko+ZOZ@PoI%|8TIu|U+SoS1>q~x_u!jj&%RbtJj$A$c&~x} z{_RI{Q}J(9|NN0M%xh1@9MVQCC=JF7#9h8z9)iV{=dp+AhN7}iltp`@B5D)aI^yJ3 z(EHz^DeG<0rmV(j<3Z$QkPoe`#|QyL4q*;)j<1!6;j`|Xrpv;X#CnxrTi;042i*1>Cz=b-l@vvJ88QRjL{7l# z$P0M`RdK&Zd%+6y*X$*|LbPLababh~tYdV9QMtFTRjC<+O8xz`isI^}-G!Ljor!r> zmocZn2ajYQz|Zo0aD1SouFaxe&2w5F7;%{ygUv~oY9=MJbe%17^~lTc9q2m1$BZ>!0{0@8HM;e?Tn!v{Yh zp6TVHvnWw=&k&aJAeZz6y_BzZ+jYdl*cgGRn=vSdN#nw0CfT^yBxGcwt`BlAueT%~ zTZHt}i&1*@O|)dc4NguVx;lFIX-W6x-hZ`B{=Kxi%7#~3(=n5@0W*nbe3W>u1*Ct; zlJtd4(i{=N&I~#Y-`c)eFJO$?A;v|VuT;?PnH93qk#h7`$PD-`$|BxCW6oC8RAdfH zCGv;e+ZT()i#{iwb9Q|MW)+?)%~2Q`)4rTfV1sSV@OqVFtdn{ZVFeLco|P z_Oh5%jNNKt`ddr;|}R9E^%e1?dXxZTt<|5t_fZ_|plklrc`QSh{dgPPnHt z>DY@%_g#$qQ1T&V2XXD4!S3xBcelhp$GMD0^G{6iqYtO=!^e%uGSWrb7aA z93j0*w}bH2+xOq78fCOv%8N*Msv*94+H@28@7EPw=}RWQFrCKW#_5+)6tNs_jm1^& z?I*QYH_k6Ri^o}hmbmf9iDSOfQJJYF98pACSa%Z=KB}}@8NRriQ7gx3pCJ5=*3563 zN^fF%+DfC-{9Yr@_$6feEk<7GGBlJW4Y{|U+g8)EttK3v<>#@X^c*&GE~2oz=?Z_C z#|#aP+--yOesGu(v$`A=i`TM793^RAziBKNHPqW2P!O{Xd6zyyVfcC!$8AAfd5YA% z{n6&CMq!2z#*&UhkaP?J%4Y{!t20>~Q%v=`n~=1^a^kZzv^TQ+m5TT^CCi}@<~(Ke zTC3DCZEba7!6EM>@YH`G$o~(7%dJ3uNqnz+`;w080uV2SfcJ@CSPg`3#7If1LTa=9 zR89PoinJB1v9DP^NzP}84RJQ> zx6MX&L3F8m`$}H_m$uL?aQdwzu8DlOuofd}=P@c#zM|F>hom(!N-b%p)r2F;Z}DK$ z?F&fLJT^8)93aIEqPbyVB6A|rg_4KRz9q-RV_ zeCc+^C718JW$2Ib#( zbo6hGzZNNtOWTN`^G{>%fqB@xWi~$iXa->l+f*W%aDhll`LWyhPgo6%k#IeubYP`5 zitsz)863pfB7yjka$lH91Hj3kI5Y8jdue&)iOzw6l|tGxBofIB{R1O!l$BQ>x}KEM z7?IpXIyc6=_j@0bntzI*L?0fYRO?H1=GGckGZ{%2?PUWCgzoO;GGQ0;HK zw1aTn&2U^KTr}x3pz;)9i1Cosr=cwQ3UX6ok)4u^#R_C!wM>$nm{ z7@W@>!aSdZ*A5SC+OYunr7`Jtr{V9+78PS6Jk!1ZcQ8)EqhOz4ENx|%tg#1RO*jZg z%pTYhcf(nD7-*zNXCM|BN{5Zt3EogLN;tnK4D&_AC3@gY2x0Zq?=KH8#EBEHVFz(* znMD_G+8xFpTO8UuSmn32&tk2Fd4sgImi|hATh>{Zy)X=pl1s3coF_X&cw`_P^&zl! zm!P}(9Ihw*3u$r};Zpb#L_~au*!WLr{5QkfcRBLQW8!rt*+01Bjo;Zm%cGH|6js8j z#eKgw)ufEr8#3VNEQM{bnsBXZ*hgvz+v>qM#i&iKNrb<#mV%0kC|pZDh6@pU#a)A( z?GuELsR{dW$2$LP`>dALM0|#kI5DGehK<*<)!bEHX&r1Z*hbo5Aw0?==R!t!T6<3g z;ea_Pp{+m+eWcCXWFJeV@vv-z{BzoUG)%q zJ=HUV>hqv_mrTJEEL@=@@Z|O%7N?j+#-1trzkpLr<@qeeKzM+X^e9@LmeGpFXSCGT zZA>XCOb;l|=?yE%7Zns{NDA{328xSvb871vKkgqKeoUdH^D4y0s`OgQWpACGqG7yi zExl7earbOa+27s$epq2n-)c?{ULC2&vw|jkBIyC2)=hrPHVif&pj_PtH?w28Ev?<2 z>^UvMy1k`zqRQtVP?+ zl^CQvnA_ro%6xxRR#aY68797`q+FQI#ohha(6|^$^U&1R>a(VSwAz&S&u)stEYdqa zTjV!&hGJi5hJjOgNnJ^CZDH6-6kc40>crns-oFNo#m7-wR};aWjksT)X{PuPN#?XL{9WCZ11US~}8{xym& zEY;8nUuAQo#;lbTqbKs@a`g}0>mTKEm#(Ty$K#w3%pm>o z;*ua^zfrkNNAqCVX<|BUIz~+AYvi(F%0+pU4+Cs7Fzo0mBt2mi*v7)e7QI+v2 z#dU)eVB9@_N`|`6JX;k$rBh{dz<6#;sMY0S1Jib5l=89 z-DO9Uy%!_H_a&4>yoHvA!V>rTFLZJW9xn97k0`JIr+i;mpHlRy*2tc*A)OA{0OfWP z8XviAh;n+$>F8XDn$78S#Nn$IPv>3UU`jr+5II3Fp|0rS@ICdnm$tq{8p&UgMte(j zypcP~pUdY87%rDLWjrpQ5z})aQ&Rpy&mXJNAkE2CNjjtNS7)Cd&JOw=3NE~jnw!CN z4B+nZ8EMFC>`gw3o}{DbE{w8_iN#M(*((au(^*f}b`3c?Y*VSg-#Hl|5r~d>~B%L9XidU;`cFKPz8RaD1SBkCWlwYuM z_PCIE63S26a|3F^Z$#s=-}v~ivVFMzAF*cb3|xvi*yUcozOUi1gK{i3zO+T`KyL#l zRbz6{*$>JsNcSTmpJlh}=kbW|Qi`W?3WLeY)HHIg96Y>;a;eAg;rhoYkBdukuV14O zFPeHc3sAnAL{ zXt{TNa8R7>B<|ck-o45ix!+|c?Sb?9E};4}bPZ7$ERRD=X|AiF;Ho1zYa8hamLekd zY2xE&lXh$YKHl?7G`8h`;UpZ)Y@ry<&Hw2*8g>I?<8^GX-Z(p9&pZVOX`NAZ251c> z9Y6`XT77Um^?Bl+e}jvcmLn+m9ry;k>!@zY{F1GEDqbXQ#kbY(Vq;X&FF54`i>>W> z<+j#5m#wz~j$w*TiEkB*R-&2Cxm8!k;Kt2!NX`mq=;zii*Hb)dwyW;MgWp-dk=mGw zKgEn)Ht>pdv?biiYj3xX_l%8>HmYk{a)vpL*$Msprez`-kMb`z_TztN{qF7kZ+q`z zP7oKigyp}8^P0g3n1>k7R}V5AJjhhR_rAt{$Nq->Eqe`1K(Teu%)fJg**NSGvibi@ z;xP6cxRPQVokaS*y7tzs-t`p~ha1X8)tnef9p`FYQ&Y`nV?xD`RdiOB#r@syvyx&M zT0cp0{->AVqXVzR%SDFj$~tfT)l<% z=W&;n86^xstsEijmGV70ht!Z8l$v_{Su_-$YjVGSPkq|(96Aeet*7R&n$~22NW=(8 zJFHL&$tcDo?dniZ?Z!<1*HNCh|DJu2TX1!V8%}4o+pB%0q;+Mr;SzQqW!HB)N1A?=NV zv5UGsw2d?tSa@xso}N-=O;c7?b7#qRxlZr{vqMc|4dY*14ZW`Nz4aaY8w>rGy@s{_1kebYzW@LL literal 0 HcmV?d00001 diff --git a/src/cmd/synergys/synergys.rc b/src/cmd/synergys/synergys.rc new file mode 100644 index 00000000..860d6f54 --- /dev/null +++ b/src/cmd/synergys/synergys.rc @@ -0,0 +1,134 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include \r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_SYNERGY ICON "synergys.ico" +IDI_TASKBAR_NOT_RUNNING ICON "tb_idle.ico" +IDI_TASKBAR_NOT_WORKING ICON "tb_error.ico" +IDI_TASKBAR_NOT_CONNECTED ICON "tb_wait.ico" +IDI_TASKBAR_CONNECTED ICON "tb_run.ico" + +///////////////////////////////////////////////////////////////////////////// +// +// Menu +// + +IDR_TASKBAR MENU +BEGIN + POPUP "Synergy" + BEGIN + MENUITEM "Show Status", IDC_TASKBAR_STATUS + MENUITEM "Show Log", IDC_TASKBAR_SHOW_LOG + MENUITEM "Copy Log To Clipboard", IDC_TASKBAR_LOG + POPUP "Set Log Level" + BEGIN + MENUITEM "Error", IDC_TASKBAR_LOG_LEVEL_ERROR + MENUITEM "Warning", IDC_TASKBAR_LOG_LEVEL_WARNING + MENUITEM "Note", IDC_TASKBAR_LOG_LEVEL_NOTE + MENUITEM "Info", IDC_TASKBAR_LOG_LEVEL_INFO + MENUITEM "Debug", IDC_TASKBAR_LOG_LEVEL_DEBUG + MENUITEM "Debug1", IDC_TASKBAR_LOG_LEVEL_DEBUG1 + MENUITEM "Debug2", IDC_TASKBAR_LOG_LEVEL_DEBUG2 + END + MENUITEM "Reload Configuration", IDC_RELOAD_CONFIG + MENUITEM "Force Reconnect", IDC_FORCE_RECONNECT + MENUITEM "Reset Server", ID_SYNERGY_RESETSERVER + MENUITEM SEPARATOR + MENUITEM "Quit", IDC_TASKBAR_QUIT + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_TASKBAR_STATUS DIALOG 0, 0, 145, 60 +STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP +FONT 8, "MS Sans Serif" +BEGIN + EDITTEXT IDC_TASKBAR_STATUS_STATUS,3,3,139,12,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER + LISTBOX IDC_TASKBAR_STATUS_CLIENTS,3,17,139,40,NOT LBS_NOTIFY | LBS_SORT | LBS_NOINTEGRALHEIGHT | LBS_NOSEL | WS_VSCROLL | WS_TABSTOP +END + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_FAILED "Synergy is about to quit with errors or warnings. Please check the log then click OK." + IDS_INIT_FAILED "Synergy failed to initialize: %{1}" + IDS_UNCAUGHT_EXCEPTION "Uncaught exception: %{1}" +END + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/src/cmd/synergys/tb_error.ico b/src/cmd/synergys/tb_error.ico new file mode 100644 index 0000000000000000000000000000000000000000..746a87c9ec8ae70f24b4125afe9ca68defb2f6d1 GIT binary patch literal 318 zcmZusyA6Xd5PgscB3VLIX+veDOwBY@u9?8{1k4aIf%|Jb3Y{4t9eD?$OZQF)q4|7v^Hl|=e!@IsjRK@B2u}b(FJQe=z?=V5liH zWoPHjzBjX*nT1SN6a`+-89X}5txV(@w?b&ncnuP0lhP#!b);z;MJKxRrt5r?%Pbk_ z?N%_Tg0`uZoK7he#O%9wJeiXYR@<+l75<*=!?qP*0HwJ2|O6{7v9^DvFs zu!_Y~)D!l8cMcLvA>Yqua2!LMtJfQ~uh~C-l}Rwt@WUxQyu;vMf6!iXu5qpJ`0fd8 C{!x(t literal 0 HcmV?d00001 diff --git a/src/cmd/synergys/tb_run.ico b/src/cmd/synergys/tb_run.ico new file mode 100644 index 0000000000000000000000000000000000000000..88e160cbfcd029978599f49605ba1a3c7695027e GIT binary patch literal 318 zcmZvXJ#K_B5QRT>QHT}^QKbzPO1ZU9vz2R3fRNJr5IzCD8;(L}A7MN84cny1jNhAi z^COL+lJ|X&*-r&u76q#eLPafx?d1Px0X>%G9mGo6woTC*$N4x8%LKWVjJU*LBRA(t z*&)UlLNF;^_DhTq!s6VW^|KVov_j`difNtFX&-H(O$k3SDILcq=N9iD-8@T;1372! my*B6BBsAGS;Q0-Eqg$^!Uw{7 literal 0 HcmV?d00001 diff --git a/src/cmd/synergys/tb_wait.ico b/src/cmd/synergys/tb_wait.ico new file mode 100644 index 0000000000000000000000000000000000000000..257be0a1d1bc613eec8cc1838a1dfd25d4644844 GIT binary patch literal 318 zcmZvXF%H5o3`Ji7QN#e9SYe77nRA*>nK?mJi9LtNNy<&SB_ktS@h=k+vHidOZA%U` zW?k2zcWvM#wvckMXxJFSxZpn+z?@1UcuF zl1i)Vw8|M$8oa;3u2z*2ycgFRqu9Ap#396Zhplt1gb?~ejL|uFp_CFr025R~TS5=- dGfb`By0-J}?~kW-COE!+Lz;S;(X4i~`vJXUO(y^V literal 0 HcmV?d00001 diff --git a/src/gui/gui.pro b/src/gui/gui.pro new file mode 100644 index 00000000..13464d4e --- /dev/null +++ b/src/gui/gui.pro @@ -0,0 +1,90 @@ +QT += network +TEMPLATE = app +TARGET = synergy +DEPENDPATH += . \ + res +INCLUDEPATH += . \ + src +FORMS += res/MainWindowBase.ui \ + res/AboutDialogBase.ui \ + res/ServerConfigDialogBase.ui \ + res/ScreenSettingsDialogBase.ui \ + res/ActionDialogBase.ui \ + res/HotkeyDialogBase.ui \ + res/SettingsDialogBase.ui \ + res/SetupWizardBase.ui +SOURCES += src/main.cpp \ + src/MainWindow.cpp \ + src/AboutDialog.cpp \ + src/ServerConfig.cpp \ + src/ServerConfigDialog.cpp \ + src/ScreenSetupView.cpp \ + src/Screen.cpp \ + src/ScreenSetupModel.cpp \ + src/NewScreenWidget.cpp \ + src/TrashScreenWidget.cpp \ + src/ScreenSettingsDialog.cpp \ + src/BaseConfig.cpp \ + src/HotkeyDialog.cpp \ + src/ActionDialog.cpp \ + src/Hotkey.cpp \ + src/Action.cpp \ + src/KeySequence.cpp \ + src/KeySequenceWidget.cpp \ + src/SettingsDialog.cpp \ + src/AppConfig.cpp \ + src/QSynergyApplication.cpp \ + src/VersionChecker.cpp \ + src/SetupWizard.cpp \ + src/IpcLogReader.cpp +HEADERS += src/MainWindow.h \ + src/AboutDialog.h \ + src/ServerConfig.h \ + src/ServerConfigDialog.h \ + src/ScreenSetupView.h \ + src/Screen.h \ + src/ScreenSetupModel.h \ + src/NewScreenWidget.h \ + src/TrashScreenWidget.h \ + src/ScreenSettingsDialog.h \ + src/BaseConfig.h \ + src/HotkeyDialog.h \ + src/ActionDialog.h \ + src/Hotkey.h \ + src/Action.h \ + src/KeySequence.h \ + src/KeySequenceWidget.h \ + src/SettingsDialog.h \ + src/AppConfig.h \ + src/QSynergyApplication.h \ + src/VersionChecker.h \ + src/SetupWizard.h \ + src/IpcLogReader.h +RESOURCES += res/Synergy.qrc +RC_FILE = res/win/Synergy.rc +TRANSLATIONS = res/lang/nl_NL.ts +macx { + QMAKE_INFO_PLIST = res/mac/Synergy.plist + QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.4 + TARGET = Synergy + QSYNERGY_ICON.files = res/mac/Synergy.icns + QSYNERGY_ICON.path = Contents/Resources + QMAKE_BUNDLE_DATA += QSYNERGY_ICON + LIBS += -framework \ + ApplicationServices +} +debug { + OBJECTS_DIR = tmp/debug + MOC_DIR = tmp/debug + RCC_DIR = tmp/debug +} +release { + OBJECTS_DIR = tmp/release + MOC_DIR = tmp/release + RCC_DIR = tmp/release +} +win32 { + Debug:DESTDIR = ../../bin/Debug + Release:DESTDIR = ../../bin/Release +} +else:DESTDIR = ../../bin diff --git a/src/gui/gui.ts b/src/gui/gui.ts new file mode 100644 index 00000000..08092a21 --- /dev/null +++ b/src/gui/gui.ts @@ -0,0 +1,1062 @@ + + + + + AboutDialogBase + + + About Synergy + + + + + <html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:16px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:x-large; font-weight:600;"><span style=" font-size:x-large;">Synergy</span></p></body></html> + + + + + Version: + + + + + + + - + + + + + Hostname: + + + + + IP-Address: + + + + + &Ok + + + + + <p>The Synergy GUI is based on QSynergy by Volker Lanz<br/> +<br/> +Copyright © 2008 Volker Lanz (vl@fidra.de)<br/> +Copyright © 2010 Chris Schoeneman, Nick Bolton, Sorin Sbarnea</p> + + + + + ActionDialogBase + + + Configure Action + + + + + Choose the action to perform + + + + + Press a hotkey + + + + + Release a hotkey + + + + + Press and release a hotkey + + + + + only on these screens + + + + + Switch to screen + + + + + Switch in direction + + + + + left + + + + + right + + + + + up + + + + + down + + + + + Lock cursor to screen + + + + + toggle + + + + + on + + + + + off + + + + + This action is performed when + + + + + the hotkey is pressed + + + + + the hotkey is released + + + + + HotkeyDialogBase + + + Hotkey + + + + + Enter the specification for the hotkey: + + + + + MainWindow + + + &Apply + + + + + + &Start + + + + + &File + + + + + &Edit + + + + + &Window + + + + + &Help + + + + + <p>Version %1 is now available, <a href="%2">visit website</a>.</p> + + + + + Program can not be started + + + + + The executable<br><br>%1<br><br>could not be successfully started, although it does exist. Please check if you have sufficient permissions to run this program. + + + + + Synergy client not found + + + + + The executable for the synergy client does not exist. + + + + + Hostname is empty + + + + + Please fill in a hostname for the synergy client to connect to. + + + + + Cannot write configuration file + + + + + The temporary configuration file required to start synergy can not be written. + + + + + Configuration filename invalid + + + + + You have not filled in a valid configuration file for the synergy server. Do you want to browse for the configuration file now? + + + + + Synergy server not found + + + + + The executable for the synergy server does not exist. + + + + + Synergy terminated with an error + + + + + Synergy terminated unexpectedly with an exit code of %1.<br><br>Please see the log output for details. + + + + + &Stop + + + + + Synergy is running. + + + + + Synergy is starting. + + + + + Synergy is not running. + + + + + Browse for a synergys config file + + + + + Save configuration as... + + + + + Save failed + + + + + Could not save configuration to file. + + + + + MainWindowBase + + + Synergy + + + + + + &Start + + + + + &Server (share this computer's mouse and keyboard): + + + + + Use existing configuration: + + + + + &Configuration file: + + + + + &Browse... + + + + + Configure interactively: + + + + + &Configure Server... + + + + + &Client (use another computer's keyboard and mouse): + + + + + &Name of the server: + + + + + Ready + + + + + Log + + + + + &About Synergy... + + + + + &Quit + + + + + Quit + + + + + Ctrl+Q + + + + + Run + + + + + Ctrl+S + + + + + S&top + + + + + Stop + + + + + Ctrl+T + + + + + S&how Status + + + + + Ctrl+H + + + + + &Minimize + + + + + &Restore + + + + + Save configuration &as... + + + + + Save the interactively generated server configuration to a file. + + + + + Ctrl+Alt+S + + + + + Settings + + + + + Edit settings + + + + + Run Wizard + + + + + NewScreenWidget + + + Unnamed + + + + + QObject + + + Synergy Configurations (*.sgc);;All files (*.*) + + + + + Synergy Configurations (*.conf);;All files (*.*) + + + + + System tray is unavailable, quitting. + + + + + ScreenSettingsDialog + + + Screen name is empty + + + + + The name for a screen can not be empty. Please fill in a name or cancel the dialog. + + + + + ScreenSettingsDialogBase + + + Screen Settings + + + + + Screen &name: + + + + + A&liases + + + + + &Add + + + + + &Remove + + + + + &Modifier keys + + + + + &Shift: + + + + + + + + + Shift + + + + + + + + + Ctrl + + + + + + + + + Alt + + + + + + + + + Meta + + + + + + + + + Super + + + + + + + + + None + + + + + &Ctrl: + + + + + Al&t: + + + + + M&eta: + + + + + S&uper: + + + + + &Dead corners + + + + + Top-left + + + + + Top-right + + + + + Bottom-left + + + + + Bottom-right + + + + + Corner Si&ze: + + + + + &Fixes + + + + + Fix CAPS LOCK key + + + + + Fix NUM LOCK key + + + + + Fix SCROLL LOCK key + + + + + Fix XTest for Xinerama + + + + + ScreenSetupModel + + + <center>Screen: <b>%1</b></center><br>Double click to edit settings<br>Drag screen to the trashcan to remove it + + + + + ServerConfigDialogBase + + + Server Configuration + + + + + Screens and links + + + + + Drag a screen from the grid to the trashcan to remove it. + + + + + Configure the layout of your synergy server configuration. + + + + + Drag this button to the grid to add a new screen. + + + + + Drag new screens to the grid or move existing ones around. +Drag a screen to the trashcan to delete it. +Double click on a screen to edit its settings. + + + + + Hotkeys + + + + + &Hotkeys + + + + + &New + + + + + &Edit + + + + + &Remove + + + + + A&ctions + + + + + Ne&w + + + + + E&dit + + + + + Re&move + + + + + Advanced server settings + + + + + &Switch + + + + + Switch &after waiting + + + + + + + ms + + + + + Switch on double &tap within + + + + + &Options + + + + + &Check clients every + + + + + Use &relative mouse moves + + + + + S&ynchronize screen savers + + + + + Don't take &foreground window on Windows servers + + + + + &Dead corners + + + + + To&p-left + + + + + Top-rig&ht + + + + + &Bottom-left + + + + + Bottom-ri&ght + + + + + Cor&ner Size: + + + + + SettingsDialog + + + Save log file to... + + + + + SettingsDialogBase + + + Settings + + + + + &Advanced + + + + + Sc&reen name: + + + + + P&ort: + + + + + &Interface: + + + + + Enable &gamepad support for Windows + + + + + &Start + + + + + &Start Synergy after logging in + + + + + &Automatically start server/client + + + + + &Hide when server/client starts + + + + + Logging + + + + + &Logging level: + + + + + Log to file: + + + + + Browse... + + + + + Error + + + + + Warning + + + + + Note + + + + + Info + + + + + Debug + + + + + Debug1 + + + + + Debug2 + + + + + SetupWizard + + + Service (Windows only) + + + + + Setup Synergy + + + + + + Please select an option. + + + + + SetupWizardBase + + + Setup Synergy + + + + + Server or Client? + + + + + &Server (new setup) + + + + + This is the first computer you are configuring. Your keyboard and mouse are connected to this computer. This will allow you to move your mouse over to another computer's screen. There can only be one server in your setup. + + + + + &Client (add to setup) + + + + + You have already set up a server. This is a computer you wish to control using the server's keyboard and mouse. There can be many clients in your setup. + You have already set up a server. This a computer you wish to control using the server's keyboard and mouse. There can be many clients in your setup. + + + + + Startup Mode + + + + + &Service + + + + + Always run Synergy in the background automatically at startup. This allows you to use Synergy at the login prompt. If you experience problems while using this mode, you can run this wizard again and select the "Desktop" option. + + + + + &Desktop + + + + + Synergy will start when your desktop loads (after you login). This mode can help if you experience problems with the "Service" option. + + + + + &None + + + + + Do not start Synergy when your comptuer starts. + + + + + VersionChecker + + + Unknown + + + + diff --git a/src/gui/lang.cmd b/src/gui/lang.cmd new file mode 100644 index 00000000..10b4f811 --- /dev/null +++ b/src/gui/lang.cmd @@ -0,0 +1 @@ +lupdate gui.pro -ts gui.ts \ No newline at end of file diff --git a/src/gui/res/AboutDialogBase.ui b/src/gui/res/AboutDialogBase.ui new file mode 100644 index 00000000..b057bc61 --- /dev/null +++ b/src/gui/res/AboutDialogBase.ui @@ -0,0 +1,210 @@ + + + AboutDialogBase + + + Qt::ApplicationModal + + + true + + + + 0 + 0 + 450 + 255 + + + + + 0 + 0 + + + + + 450 + 250 + + + + + 450 + 255 + + + + About Synergy + + + true + + + + + + <html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:16px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:x-large; font-weight:600;"><span style=" font-size:x-large;">Synergy</span></p></body></html> + + + true + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + + + + + + + + 1 + 0 + + + + Version: + + + + + + + - + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 1 + 0 + + + + Hostname: + + + + + + + - + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 1 + 0 + + + + IP-Address: + + + + + + + - + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + Qt::Vertical + + + + 20 + 78 + + + + + + + + Qt::Horizontal + + + + 131 + 20 + + + + + + + + &Ok + + + + + + + <p>The Synergy GUI is based on QSynergy by Volker Lanz<br/> +<br/> +Copyright © 2008 Volker Lanz (vl@fidra.de)<br/> +Copyright © 2010 Chris Schoeneman, Nick Bolton, Sorin Sbarnea</p> + + + true + + + + + + + + + buttonOk + clicked() + AboutDialogBase + accept() + + + 315 + 374 + + + 301 + 3 + + + + + diff --git a/src/gui/res/ActionDialogBase.ui b/src/gui/res/ActionDialogBase.ui new file mode 100644 index 00000000..c6d86304 --- /dev/null +++ b/src/gui/res/ActionDialogBase.ui @@ -0,0 +1,581 @@ + + + ActionDialogBase + + + + 0 + 0 + 372 + 484 + + + + Configure Action + + + + + + Choose the action to perform + + + + + + Press a hotkey + + + true + + + + + + + Release a hotkey + + + + + + + Press and release a hotkey + + + + + + + + 1 + 0 + + + + + 256 + 0 + + + + + + + + + + + only on these screens + + + true + + + true + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 128 + 64 + + + + QAbstractItemView::ExtendedSelection + + + + + + + + + + Qt::Horizontal + + + + + + + + + Switch to screen + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + + + + + + + + + Switch in direction + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + + left + + + + + right + + + + + up + + + + + down + + + + + + + + + + + + Lock cursor to screen + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + + toggle + + + + + on + + + + + off + + + + + + + + + + + + + This action is performed when + + + + + + the hotkey is pressed + + + true + + + + + + + the hotkey is released + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + KeySequenceWidget + QLineEdit +
KeySequenceWidget.h
+
+
+ + + + buttonBox + accepted() + ActionDialogBase + accept() + + + 245 + 474 + + + 157 + 274 + + + + + buttonBox + rejected() + ActionDialogBase + reject() + + + 313 + 474 + + + 286 + 274 + + + + + m_pGroupType + toggled(bool) + m_pKeySequenceWidgetHotkey + setDisabled(bool) + + + 104 + 194 + + + 110 + 132 + + + + + m_pRadioSwitchInDirection + toggled(bool) + m_pKeySequenceWidgetHotkey + setDisabled(bool) + + + 118 + 322 + + + 81 + 129 + + + + + m_pRadioLockCursorToScreen + toggled(bool) + m_pKeySequenceWidgetHotkey + setDisabled(bool) + + + 101 + 353 + + + 68 + 126 + + + + + m_pRadioPress + toggled(bool) + m_pKeySequenceWidgetHotkey + setEnabled(bool) + + + 48 + 48 + + + 45 + 129 + + + + + m_pRadioRelease + toggled(bool) + m_pKeySequenceWidgetHotkey + setEnabled(bool) + + + 135 + 70 + + + 148 + 125 + + + + + m_pRadioPressAndRelease + toggled(bool) + m_pKeySequenceWidgetHotkey + setEnabled(bool) + + + 194 + 100 + + + 201 + 125 + + + + + m_pRadioSwitchToScreen + toggled(bool) + m_pComboSwitchToScreen + setEnabled(bool) + + + 148 + 291 + + + 350 + 290 + + + + + m_pRadioSwitchInDirection + toggled(bool) + m_pComboSwitchInDirection + setEnabled(bool) + + + 158 + 322 + + + 350 + 321 + + + + + m_pRadioLockCursorToScreen + toggled(bool) + m_pComboLockCursorToScreen + setEnabled(bool) + + + 180 + 353 + + + 350 + 352 + + + + + m_pRadioPress + toggled(bool) + m_pGroupBoxScreens + setEnabled(bool) + + + 25 + 47 + + + 33 + 155 + + + + + m_pRadioSwitchToScreen + toggled(bool) + m_pGroupBoxScreens + setDisabled(bool) + + + 48 + 278 + + + 98 + 153 + + + + + m_pRadioRelease + toggled(bool) + m_pGroupBoxScreens + setEnabled(bool) + + + 264 + 67 + + + 241 + 158 + + + + + m_pRadioPressAndRelease + toggled(bool) + m_pGroupBoxScreens + setEnabled(bool) + + + 286 + 98 + + + 290 + 156 + + + + + m_pRadioSwitchInDirection + toggled(bool) + m_pGroupBoxScreens + setDisabled(bool) + + + 38 + 313 + + + 64 + 195 + + + + + m_pRadioLockCursorToScreen + toggled(bool) + m_pGroupBoxScreens + setDisabled(bool) + + + 48 + 339 + + + 79 + 234 + + + + + m_pRadioSwitchToScreen + toggled(bool) + m_pKeySequenceWidgetHotkey + setDisabled(bool) + + + 84 + 280 + + + 185 + 123 + + + + +
diff --git a/src/gui/res/HotkeyDialogBase.ui b/src/gui/res/HotkeyDialogBase.ui new file mode 100644 index 00000000..cccccf22 --- /dev/null +++ b/src/gui/res/HotkeyDialogBase.ui @@ -0,0 +1,81 @@ + + + HotkeyDialogBase + + + + 0 + 0 + 344 + 86 + + + + Hotkey + + + + + + Enter the specification for the hotkey: + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + KeySequenceWidget + QLineEdit +
KeySequenceWidget.h
+
+
+ + + + buttonBox + accepted() + HotkeyDialogBase + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + HotkeyDialogBase + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/gui/res/MainWindowBase.ui b/src/gui/res/MainWindowBase.ui new file mode 100644 index 00000000..8add3827 --- /dev/null +++ b/src/gui/res/MainWindowBase.ui @@ -0,0 +1,395 @@ + + + MainWindowBase + + + + 0 + 0 + 600 + 500 + + + + + 0 + 0 + + + + + 500 + 400 + + + + Synergy + + + + + + + &Start + + + + + + + + 0 + 0 + + + + &Server (share this computer's mouse and keyboard): + + + true + + + true + + + + + + Use existing configuration: + + + + + + + + + &Configuration file: + + + m_pLineEditConfigFile + + + + + + + false + + + + + + + false + + + &Browse... + + + + + + + + + Configure interactively: + + + true + + + + + + + + + &Configure Server... + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + 0 + 0 + + + + &Client (use another computer's keyboard and mouse): + + + true + + + true + + + + + + &Name of the server: + + + m_pLineEditHostname + + + + + + + + + + + + + Ready + + + + + + + Log + + + + + + + 0 + 0 + + + + + Courier + + + + false + + + false + + + QTextEdit::NoWrap + + + true + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + :/res/icons/16x16/warning.png + + + + + + + + + + true + + + + + + + + &About Synergy... + + + + + &Quit + + + Quit + + + Ctrl+Q + + + + + &Start + + + Run + + + Ctrl+S + + + + + false + + + S&top + + + Stop + + + Ctrl+T + + + + + S&how Status + + + Ctrl+H + + + + + &Minimize + + + + + &Restore + + + + + Save configuration &as... + + + Save the interactively generated server configuration to a file. + + + Ctrl+Alt+S + + + + + Settings + + + Edit settings + + + + + Run Wizard + + + + + + + + + m_pButtonToggleStart + clicked() + m_pActionStartSynergy + trigger() + + + 361 + 404 + + + -1 + -1 + + + + + m_pRadioExternalConfig + toggled(bool) + m_pLineEditConfigFile + setEnabled(bool) + + + 156 + 179 + + + 169 + 209 + + + + + m_pRadioExternalConfig + toggled(bool) + m_pButtonBrowseConfigFile + setEnabled(bool) + + + 353 + 182 + + + 356 + 211 + + + + + m_pRadioInternalConfig + toggled(bool) + m_pButtonConfigureServer + setEnabled(bool) + + + 204 + 244 + + + 212 + 274 + + + + + diff --git a/src/gui/res/ScreenSettingsDialogBase.ui b/src/gui/res/ScreenSettingsDialogBase.ui new file mode 100644 index 00000000..95c48685 --- /dev/null +++ b/src/gui/res/ScreenSettingsDialogBase.ui @@ -0,0 +1,543 @@ + + + ScreenSettingsDialogBase + + + + 0 + 0 + 434 + 437 + + + + Screen Settings + + + + + + + + Screen &name: + + + m_pLineEditName + + + + + + + + + + + + + + true + + + A&liases + + + false + + + + + + + + + false + + + &Add + + + + + + + QAbstractItemView::ExtendedSelection + + + + + + + false + + + &Remove + + + + + + + Qt::Vertical + + + + 20 + 126 + + + + + + + + + + + &Modifier keys + + + false + + + + + + &Shift: + + + m_pComboBoxShift + + + + + + + + Shift + + + + + Ctrl + + + + + Alt + + + + + Meta + + + + + Super + + + + + None + + + + + + + + &Ctrl: + + + m_pComboBoxCtrl + + + + + + + 1 + + + + Shift + + + + + Ctrl + + + + + Alt + + + + + Meta + + + + + Super + + + + + None + + + + + + + + Al&t: + + + m_pComboBoxAlt + + + + + + + 2 + + + + Shift + + + + + Ctrl + + + + + Alt + + + + + Meta + + + + + Super + + + + + None + + + + + + + + M&eta: + + + m_pComboBoxMeta + + + + + + + 3 + + + + Shift + + + + + Ctrl + + + + + Alt + + + + + Meta + + + + + Super + + + + + None + + + + + + + + S&uper: + + + m_pComboBoxSuper + + + + + + + 4 + + + + Shift + + + + + Ctrl + + + + + Alt + + + + + Meta + + + + + Super + + + + + None + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + &Dead corners + + + false + + + + + + Top-left + + + + + + + Top-right + + + + + + + Bottom-left + + + + + + + Bottom-right + + + + + + + + + Corner Si&ze: + + + m_pSpinBoxSwitchCornerSize + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + &Fixes + + + false + + + + + + Fix CAPS LOCK key + + + + + + + Fix NUM LOCK key + + + + + + + Fix SCROLL LOCK key + + + + + + + Fix XTest for Xinerama + + + false + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + m_pButtonBox + accepted() + ScreenSettingsDialogBase + accept() + + + 222 + 502 + + + 157 + 274 + + + + + m_pButtonBox + rejected() + ScreenSettingsDialogBase + reject() + + + 290 + 508 + + + 286 + 274 + + + + + diff --git a/src/gui/res/ServerConfigDialogBase.ui b/src/gui/res/ServerConfigDialogBase.ui new file mode 100644 index 00000000..3c194e5f --- /dev/null +++ b/src/gui/res/ServerConfigDialogBase.ui @@ -0,0 +1,756 @@ + + ServerConfigDialogBase + + + + 0 + 0 + 740 + 514 + + + + Server Configuration + + + + + + 0 + + + + Screens and links + + + + + + + + true + + + Drag a screen from the grid to the trashcan to remove it. + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + :/res/icons/64x64/user-trash.png + + + + + + + + 1 + 0 + + + + Configure the layout of your synergy server configuration. + + + Qt::AlignCenter + + + true + + + + + + + Drag this button to the grid to add a new screen. + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + :/res/icons/64x64/video-display.png + + + + + + + + + + 0 + 273 + + + + + 16777215 + 273 + + + + true + + + false + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + + + + + 1 + 0 + + + + Drag new screens to the grid or move existing ones around. +Drag a screen to the trashcan to delete it. +Double click on a screen to edit its settings. + + + Qt::AlignCenter + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Hotkeys + + + + + + &Hotkeys + + + + + + + + + true + + + &New + + + + + + + false + + + &Edit + + + + + + + false + + + &Remove + + + + + + + Qt::Vertical + + + + 75 + 161 + + + + + + + + + + + A&ctions + + + + + + + + + false + + + Ne&w + + + + + + + false + + + E&dit + + + + + + + false + + + Re&move + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Advanced server settings + + + + + + &Switch + + + + + + + + true + + + Switch &after waiting + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + 10 + + + 10000 + + + 10 + + + 250 + + + + + + + ms + + + + + + + + + + + true + + + Switch on double &tap within + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + 10 + + + 10000 + + + 10 + + + 250 + + + + + + + ms + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + &Options + + + + + + + + true + + + &Check clients every + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + 1000 + + + 30000 + + + 1000 + + + 5000 + + + + + + + ms + + + + + + + + + true + + + Use &relative mouse moves + + + + + + + true + + + S&ynchronize screen savers + + + + + + + true + + + Don't take &foreground window on Windows servers + + + + + + + Qt::Vertical + + + + 20 + 16 + + + + + + + + + + + &Dead corners + + + false + + + + + + To&p-left + + + + + + + Top-rig&ht + + + + + + + &Bottom-left + + + + + + + Bottom-ri&ght + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Cor&ner Size: + + + m_pSpinBoxSwitchCornerSize + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok + + + + + + + + ScreenSetupView + QTableView +
ScreenSetupView.h
+ 1 +
+ + NewScreenWidget + QLabel +
NewScreenWidget.h
+
+ + TrashScreenWidget + QLabel +
TrashScreenWidget.h
+
+
+ + + + + + m_pButtonBox + accepted() + ServerConfigDialogBase + accept() + + + 572 + 424 + + + 377 + -8 + + + + + m_pButtonBox + rejected() + ServerConfigDialogBase + reject() + + + 641 + 424 + + + 595 + 1 + + + + + m_pCheckBoxSwitchDelay + toggled(bool) + m_pSpinBoxSwitchDelay + setEnabled(bool) + + + 110 + 63 + + + 110 + 63 + + + + + m_pCheckBoxSwitchDoubleTap + toggled(bool) + m_pSpinBoxSwitchDoubleTap + setEnabled(bool) + + + 110 + 63 + + + 110 + 63 + + + + + m_pCheckBoxHeartbeat + toggled(bool) + m_pSpinBoxHeartbeat + setEnabled(bool) + + + 110 + 63 + + + 110 + 63 + + + + + m_pListHotkeys + itemDoubleClicked(QListWidgetItem*) + m_pButtonEditHotkey + click() + + + 197 + 115 + + + 304 + 115 + + + + + m_pListActions + itemDoubleClicked(QListWidgetItem*) + m_pButtonEditAction + click() + + + 505 + 120 + + + 677 + 118 + + + + +
diff --git a/src/gui/res/SettingsDialogBase.ui b/src/gui/res/SettingsDialogBase.ui new file mode 100644 index 00000000..1a71051d --- /dev/null +++ b/src/gui/res/SettingsDialogBase.ui @@ -0,0 +1,355 @@ + + + SettingsDialogBase + + + + 0 + 0 + 383 + 470 + + + + Settings + + + + + + &Advanced + + + + + + Sc&reen name: + + + m_pLineEditScreenName + + + + + + + true + + + + + + + P&ort: + + + m_pSpinBoxGamePoll + + + + + + + &Interface: + + + m_pLineEditInterface + + + + + + + true + + + + + + + true + + + + 0 + 0 + + + + 65535 + + + 24800 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Logging + + + + + + &Logging level: + + + m_pComboLogLevel + + + + + + + Log to file: + + + + + + + false + + + + + + + false + + + Browse... + + + + + + + + Error + + + + + Warning + + + + + Note + + + + + Info + + + + + Debug + + + + + Debug1 + + + + + Debug2 + + + + + + + + + + + &Game device support + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + &Mode: + + + m_pLineEditInterface + + + + + + + + None + + + + + Next Generation (XInput) - 32-bit only + + + + + Legacy (JOYINFOEX) + + + + + + + + &Polling: + + + m_pLineEditInterface + + + + + + + Dynamic (align with client polling) + + + true + + + + + + + + + Static frequency: + + + + + + + true + + + + 0 + 0 + + + + 10 + + + 10000 + + + 10 + + + 60 + + + + + + + + + + + + &Startup + + + + + + &Start Synergy after logging in + + + + + + + &Automatically start server/client + + + + + + + &Hide when server/client starts + + + + + + + + + + m_pLineEditScreenName + m_pLineEditInterface + m_pCheckBoxAutoConnect + m_pComboLogLevel + m_pCheckBoxLogToFile + m_pLineEditLogFilename + m_pButtonBrowseLog + buttonBox + + + + + buttonBox + accepted() + SettingsDialogBase + accept() + + + 266 + 340 + + + 157 + 274 + + + + + buttonBox + rejected() + SettingsDialogBase + reject() + + + 334 + 340 + + + 286 + 274 + + + + + diff --git a/src/gui/res/SetupWizardBase.ui b/src/gui/res/SetupWizardBase.ui new file mode 100644 index 00000000..4948166e --- /dev/null +++ b/src/gui/res/SetupWizardBase.ui @@ -0,0 +1,276 @@ + + + SetupWizardBase + + + + 0 + 0 + 500 + 390 + + + + + 500 + 390 + + + + Setup Synergy + + + + + 0 + 0 + + + + Server or Client? + + + + + + + + + + 75 + true + + + + &Server (new setup) + + + + + + + + 0 + 0 + + + + This is the first computer you are configuring. Your keyboard and mouse are connected to this computer. This will allow you to move your mouse over to another computer's screen. There can only be one server in your setup. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 75 + true + + + + &Client (add to setup) + + + + + + + + 0 + 0 + + + + You have already set up a server. This is a computer you wish to control using the server's keyboard and mouse. There can be many clients in your setup. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 0 + 0 + + + + + + + + + + 0 + 0 + + + + Startup Mode + + + + + + + 75 + true + + + + &Service + + + true + + + + + + + Always run Synergy in the background automatically at startup. This allows you to use Synergy at the login prompt. If you experience problems while using this mode, you can run this wizard again and select the "Desktop" option. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 75 + true + + + + &Desktop + + + + + + + + 0 + 0 + + + + Synergy will start when your desktop loads (after you login). This mode can help if you experience problems with the "Service" option. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 75 + true + + + + &None + + + + + + + + 0 + 0 + + + + Do not start Synergy when your comptuer starts. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 0 + 0 + + + + + + + + + + diff --git a/src/gui/res/Synergy.qrc b/src/gui/res/Synergy.qrc new file mode 100644 index 00000000..ef0a5c75 --- /dev/null +++ b/src/gui/res/Synergy.qrc @@ -0,0 +1,10 @@ + + + icons/16x16/synergy-connected.png + icons/16x16/synergy-disconnected.png + icons/64x64/video-display.png + icons/64x64/user-trash.png + icons/16x16/warning.png + icons/256x256/synergy.ico + + diff --git a/src/gui/res/icons/16x16/synergy-connected.png b/src/gui/res/icons/16x16/synergy-connected.png new file mode 100644 index 0000000000000000000000000000000000000000..3aac760aa156571b3f4151069e4bd657545fb18f GIT binary patch literal 651 zcmV;60(AX}P)^&hV}nb1kyKvm<>t60U%xu#5J+ONQ^1==@ z>myrt2^zyV^KMSfdGh;#xtJsy!!O293^rm;3>jL5439rvVPF#6{-3dBr+_px8hNlq z;|WHc&FWH0x=;V!VEF#+H^V}+-3)8auVEb> zj~_#tVj08AtBV;fyt&B0$jHiIE^d1iB{KnCQUrAg+&dTUEoOMdH<5vrFdbY}TqFU;9UCwh+n5p(kfpUOtYRd&013gwkWdaMgb-Rn z4WWfpItiwegh1%&XF{kU#Do^(vhSZ+H47qgce(fOy{8pBJAa*-Z~mDb?OKC0IXf3o zh&_042_mN#;TcpXESv)L3$g?8{A^3*~uLt1;UAsO0PW1k^tJh3ZzjIi1Za0y2 z$Fihr0|r%;D3rytSx`W$HTr?!zwX%=F3_k3hL4WT5@wa8&_$YzsYW_?>hL_})MBMf z6+S4zXF!F#LRX@r%?d$u+a zCQ2M7j#LWcB!XB;lq6OhCyt2_h=oFFv@kweB8(JC!eU1TsDOhtwgK}v3nV6;ZBGL)MJL^~_nOSk)>Qx#@9K`a!BB89P$ zLP?%TCXbcLg|dD^kz6QraKdUj`N%S;G=)>FKE&W4QY6X~#mL1nd5mZrYwn=rYz|$i zQkWG_a^R?DaWNno1M2`Z=|zl@6aPsapi&PB7&RP8MXQrNoJ3adDC2D3PNZ2fe?#@etTqS&whH zb=2r|(T^K#CUaUD3`EPoZULFq(v&x)gpSUkO@>mVl1?jwAh3^a%NWecl4t5oW`$l! zXQn1Vb)qyHm0Tv0h|{ETQfXXVa+*jalBSB1ljCDzQ&W?ql60YrjE0t`4hWM*$x{u= zQXF>{w<>TO8!t;rPLGL~#<+ItVETyL5k?Knz6z~_4zBe-;{Gu%9w{b6V}hA!ighTm zQym$Qx%A|gT_ zzc{KaOIX&+fA7rwmvt3;3ut-wwXQ>)?$&?VirTCxw10VOvBtuZM^~hnD~II;-1Ep< zIsw*^kMQi1$N(Z6gO>@~QnCz)hi`X6VYCRR7E?#Dv*}pZrjBA~Q@iCTy~dm%wv^_? zcD5Q0%_^f)HEP-fHnDLL;@EgNrEtn3;QMKWI6ekW;7Ma5VkJU2MUV%+R1zVU#YISj zq6mpd3a2b0Rt%>^3_S1?FN70x<1rlzayTV15^;ZVY?4@*79W?Eo*pAfjh9G-N#Yop zEJ-X)OOBCAq?U@esF|hOe_0&R6`DVY11xzo4lq}qt*M~3V^TG+2sYs*o+xfW^kW4M zAlv0*|H#lm;A}{~2Iaz23K|C<@#VmB)^35ABu*L^6A>en#R*9Yy%aAJv#FSH*i-`i zSP`oe6M?PbNeVp*y#*XdffFN*6_ONs6Y|iPkV5arOCUCI3i^=Ba4H2&NO4&bh!mXS zQX~*TI0aisi7muWj3@@AIEC6m%5vgKUW!v0)Du#oBa4N6ycqN2A=V&;h>Q`*z$Z?z ztxSq-Wm2polVTm2jOfTjB#*txh&P!Gmnb7XWw<7=I*yBgsEvR~kBEzikB~;lA^;kZ zC_*HT5J@6LF%b~Opd^ZqfRGo1E(`=12QWlpVYEQ7!?=KO?=Ox^OM&Q5k;-BuLRqRX zIXOw3CINiX(&A#pl6XsJw&*dJG0sLlJOqL#P|>zWP>-yhZQm;X%8WhE{z|V-zTAa4 z0pgl;!{r(uC#f4QCqP`2Zn#|I<0N&%m}xhRZcRPEt2qPJp;3-Eg_a$4Tmj%Lx$Iq#G{R_&7=3a5(|unsmeE z8XqUA8!jh6T$66NT;t;;b;IQZh-=afmuq~Sq;9yJ0C7#agv-aisRONtogB(x%LcsX z1NRqpKM0foR|>X?uw2Q05Iu=3fTScDG=tqmf_saQ{(gBzh28|e@TB$1DeV3dt%#&E zr2Z%m8Ic0%kqMQ+)re@wE725-k6(!YhmoBq$#2}&2 zI7qEJxZV_sv~!5h-+@Me0JP+B2lpBlKTg6h|B8%YfnDRxKS=9%coPt7ZCRv+gu|sM z3-Y+`SOf1Z!Y3ZQ2nVADJWe8eTJ{pby^N5(C)#6@a~~3HWwF6LeW_Lp7rZ-(SAoJr z4~JhB=cSFw1KI z_y+PDeOh0t0~^1zTPZd<=@?Y zF6@?J0p0>G?wXdIER)*WIjjy}p*80z)Gf3Elr-4U73NG+Mqbvi0@&CDD|=blEtI{B z492NRT8&z7X?!5t_zWu>EIn0pp`ui4hO9UKmHK1U^R4Wb>b?bP^4l*5aD!R@$%AbI zFhRo&dW^NV*-$c~)NG=yfq`W{S_q^^xPAo&GX&Hld|&AHA}4h+T6$w&$PZ4=pgNLg z$5|j@XMiYUm_~2lKrHI?W+UEnI1$F&YCD8_jX9&uqt zs37Fq;=G#8M-Ux0pNR7+ogU(wgX>(P(y4I%5aeGfE5&io9R~T?Wg5C1@@FC6U0bTt zKpxMGVLDm?W08k3>u#o%MUWRl-p`nqlLGkx5LA9@hkSuU-V6Z`NtWD@}h8gwvA*2sq6?A;HXx@J~gFA0jv#_v>?(si2rFrhiW<0BO-?` zr12Vr7UV;eX<&bf6oX!+A$vJ#^ewi#aq#d@nmKsDSvIaA6S}S$g-wV;K_|ly_m|^{ z7wU_+(EBD6@7qqpuK26I)Xk&U!rs9TU3iKqn}Y7x{n%QRp3eaQ=wElsteVF>PLwwDV0E_ zQNySlYAiL8Qd4@WlzNtWftp7xq?S>usg2b8)Glg2^(l3V`kJ~(U8jDd9&)%GKTa5@ z6Q?I9nj_^TafWj8I8SrboXMQ2oGQ)&&JxZV&KAyg&VJ74oO7I7&d;2CT*UR|w&r%_ zMslUx6z&M_IBp@=$eqre%U#S}!`;f=%{|IJ!>#4s0-%ixXTDS4B5 z(|PlG%Xk}j+j)n0XLuKRb-ZQ|e~*qH{XFCznI2<2)E?y?vpp7jtoPXFaoFRW$7PTE zd_KQ5zZXBApTQr?pTwWWpU;1T|1Q6Ve~N#Jf6vp?vz=!@&w-vJJQbd1&)J?!J-2x7 z^E~Z&+4FZVU#~7+Vy|?sab9|_nO=*%HhF#Gb=vEySA%zucMos5_Xuy5_Z07H?{(h0 zy-#{y_HOVA_UYv_&?nDll24`25}&O;hkd^Dx#R2W8}2Lf9qFs_o#DI0_dVZZz88J# z{X+cu`la}d_bc^V;P!f6)I2|K9^b1EK;l15^Re z2P_ZR9&kF~R-kuakHDnBX9A}NE)M)K@MPf4An%}_K`B9spy@%&gLVa72)Y*>8Y~JP z5v&WIAG|5})8K0%o*_L$(n3@rRUvCb4u)I`<%V_(O$k+oz7V=D^l<3qFn(CCu*|SY zVe`V?2|E#1*D9n{Oso7>6|I)H`nXkXYi{eFtutF|TUWRKp!NCI58HHVlhlT8GpEhg zHmBP>XxpJ}Vq3cH+_vwwJ=gYOJ3+g&cE#;pX}6=@_w9M@`?eq1zP$aL?GLxV*&(b$ z{|?Fyb31&{;hT<}j(t1kbez_4ZO7vs?{(_jX>g~KPD?u->~ynp>&}UtCv{%b`IFAq zyM%U0=%VSesLQ@CHw3K&Ndm24iQtf+u4~7x8C^@euI_rGYeRUi@KND2!`}`6zMD@s zX}7{|i@F`?R@c2t_pI*EcHi9nn;zahq&u;B9kJ^A~#1~jA|V*=2)C$)LzZHduhKOd0 zc8h)!_Z2I}%f)9U{*rXb^OBvC+cEuO=$JQSzKRWv&5C_7_CRb?Tzs4{Zfo52`0nu& z<5$FAkcLUKrSql7WFE3)+4Hi!vU+)(+${e<{%ilp{>A+_^}jZt*8qCJx&fC4h7VK> zTs^QhL69&pVO2uypss@ygVqfCF|k{sDsg?{)ucX2lajV1-AWcF8JHr4UQc=ZE#J7N5;^MS28YScFI&{Zp^G35JUVG~D_}KBY$Degz>= ziWV2$)TC&ZX>L!-n6z@zgW?gzZx=J#(b{))Ub>08UHTAxvHp;uv!TLps-$1Z?2_8a z^2x7Ft}_lbt~XJpr%gM}VX&AwUfR2KR_VpEgtFyjznABie^?P*p|3bTrSFtEQ?5=; zpZeA`-ZbU31J8DSw({AF&m}##dO9_|VETdQyFFj^{N)*gXKbkSsVuJiVy1BB!kKqp z82!TTs!mlisxHkMJZtmpfZ4{`7hdfD;>wqJFKJ#nF-J0I>73@ds=1%d6V6*auW`O| z{%3y^{cXwLm<5FkPE^NLzxgu%W&O+NUm5huhK0ckr!KtoYSydU7YP>4U3B-g39o&+ zIA-z6*L_|ueZ6+cuq8W}c3=AP(#B;)%g!xNUjFWijw|M@c<_emjni)?zPWW}r23YnwHtCa9NHMaapR_r zo2oZ+HkWR`zGeKDFW*UhXXn<)t*hT{{qDT?(0irt-FScE`{zFx`oVz@r5|qnsK-Yu zwuNq+yPeuTWqaL@q8+t6$L>6}YsjtxyZi6nwkK-O+k3n2UH);akE=iN`K0O-X5Z9( z_i9RNZthp_zjR>2fo~6vIe7Na$U`R%4?TSBNcxe3N0W~3J2vpx-cRM9?)psn*^bZS zKHq*k_V~6hV!zmSBJRZYlkq2aekuEM_o)G=J~=(;^!_ucXO5iBJp1{%;pa}BA9en# z3*#@;ex>^A+Sl5zZ+}zvO~bb{zVrBQ?)O38FaDw94{QG3=kM><#?^j&G4cye7W3_j>yEFK;|`-%u~DKl*U=!>bMDjh>B5 zn))>DZXVkF9iw9y@?9g0tOoF6fc>V9SI~%a1QNWS2(p~8md0PZIh#;hL@{46wJ2yv zdS*JJI0&Bgz&8@+7vx8_q@0kF1sb@_y)HM z^z#dB)iyM=RcL72U_WwLE-c}kQvSZa{sI0$0Rcf_0RaJFI0*=2O@jZ71EvNAdqIyt->Hg@hC2b z$MNvwdwFpEBp?jt^16nIJd#E#Lc=Fliuqx$tbRYaTlZEu`wJv7#+j#5JbNt6t-E+z zX^L&V=7Se{roNh|N;|OD9M|Ubs2}OyKCC);ru5RC^j?eB&HCuILuY@w`_{I@=PuvN zFD#qAc>VSx=daw4A3R!J{^ILz?>Ktl>Vsg!;XrM9q)wiEk62QLuA&eg)L?RWsE4@n zl`vd`{W+)VB;5-x8fPv{$!(=H#oX?}#})AG8GGO~RAOx#RobXHGyS8j3V&RKUiK<5 z-ylCODKi)iLN|OrY<^$;)Tr{Q(nW9Z-w*Zgu~z?J-=y^y?hV@X#*n%Cy+2L-sZ;v< zq1$d1KfUM72Os}^*+)Z7y90FFM~~do`qJw2;qfX77wSVk8?-VCRS=EzKy|R?CsDmEIxvJN5qxt;s3V+4N=)*2TrPM~ zL!MV*+jVe$iF_JoAOuexEms$tz#_aEpd36<_UrWx#GSFT!lp*+jR!~q`*=Z zX2FvjI3c9$hxOSea44L&OYAu-pJ&bd(G{Cu5AM^Afjb8Nf5w1)+CUVZ%<)=>op1v3 z6h<{|&V?s`@DvC?S*lQh7|)a_Su3!aJG@v+)tHNDW0F=4ZyU@-u*n*E+Yso)#=b01xZ6I)S_dyyY!AH2489s$phz7& z2#2d3L!RYr4Mx-xp1I>K-E6Nxh~*k=h`e;B7PPPNCOi=A`G~4#B!f z`c&KtwQ$-?aC{$xlG1Z+^#U!FZp_p__Wg|23g2(B7I`<5um6;^Bi%R{ZmFdOi3wQB z9^m-#Z+fhr$b+f94H9vaZb~;=>w{simN+cMjwky*EXP3Ja(P+HCs^UT zKdcXQg+_SdP1XkvI9Q?<1&slCBA6zk%O+SW2*K?TuG0zY7}kbmJ8nPOuU`)V$G~Gi z;KEaKa`c7?0W6+;dJa=ep^X9|uA{b8kLwl&rzaW)Z)&HI_i+LwWZMOs`Qx{9Jgs7i zk?2p+;8BCo<6>H=Ex?h?!vTY7jlQtLA}4gQzS!VMYfUAEEoh|{`)`4psYr**u&p)T6M%SYok-%VQ4* z)bST2I2I7E3kd29aO+sq0cgY@Ml=vSef<-NV;u|XVuE5}g%|)Na{&0Ux5yfuWLC;m zY6YY8=JamaMrGnyOL+(d=oBWHNYcmTmTSz)BI~q)=z;=mi;0eKzzlaTqVy(9x z)Y>e|R04CKJ_{@4Dvf%41zF4a62UY56cCU);hB((HA5{{CI_A^oq2dhb>@NR5w`Z^ z)L5($GO5ZRsZEyFjQb1A{zPqTt{`KrjU&%m8%G{^er#B?kO=UAb!zC6Sh z1=><0%dgP3;FoDr9V$X9kf|34unpXta$CkhZcz#DKoZo$bpnCq1uY)%8u+sen*4zV zHbp7MGJ$2n^0EOPHQD`*^ z(_pQ^L~ggFsN6!c-U>;gm#qq3r6xMJNKryt$Ab?r@Js4aBg-P=4n_@374yN$6Vpna zJ{RY)7nndWO9gQOlmt!=`PzqNcuK=x2W!wk`*-3(X4KeB`#5i;C}+@=- zn*jqIxSqfv*$2pjT*0?Ln_>9~)!UStkS7*lK@+e!aBW1EqlT%OF#}}m!2rnsZQ&5I zhR|4rkl@D3z{)jvS|AK)6SM|T5f%=cV<#c31F8-OFhGwmuxl`4W-v1>=D;B&W{?7i z4CI7?EE!C~$8xcmuv{Q5K8T!#a?Yj=s4}SaY!RZ~K5PXcQW6`a{f8L0= zZ`rYa3zmS>?IgoYM_ucI-dEVAOW{7q)OW4KB))kf31(CtfuswYO(7fq}D6cW>@2EQkz{(v;Smm znwu&~ZJHY^No|@RRuYfoTHjn>+0YE-RW>$54XkMp^zjTk3=Aw?Hj|9ztrp zLT!Q?w~*QdHEqGOD(dpm7pGq7i#i{}-Fdd(Tzp}emB-oZ-%qjEORuum4SmkxOl>@h zSpLJMby9G^{4_T!t>PQziZ*A$`1uv*tFnv6F1b6B&N7V|QMV$fPsW*zORvj2b}LS~ z$}IHhH14}WDSr#^5c3)HO@W}x_)qaXbNAHV?MHuQ?j5T6^7E5NZl2ip^{I>XTXyU@ zd+KP@@p&tD+`Rn5omFd=9^7!7IW~Xod$%`lJofUc)ju(HJ6~V;!B5!3#pnBrH0PL0 z&m^U%yik0qB4My}*4-gvWzWQkZZ7d?BRRcv&h=>T;PLXS%zP9u{3~JqIqm#<9%jyu z3h*0o7~=p3zCUNfqa$lRJPdyCu+A9pRm+gn=ew&LKC+f6p zegwuQvu4ddi>c-vKoNS@toNG18zHeAj)TnYg+OmUeY@_&I-pj zl2V?PXY0_ZnZ0DYUK>wvD_ziZd7`J>22mapyAlcM%6l|`3#{qveET9xT?h_ufK((Yzv-?nTNH< zkG}cQzIAVYK}cL$^Q$ZOTiXv*b8}^79NQU_W=w%Lc9nkY4f`Wp6#Oe)uCJdha;YUM(UI}DlC64{FgVjI}JIOHH z*)cHZN(anX`QtO}SdcJtfQBQJ9TWYSMm3?Qvtt8K0E@86hpBjsB$77XS~N28<0Eq- zdM)^kjD-J)yIU6mx9#r6&S1Cv8SlRUk0Vbc literal 0 HcmV?d00001 diff --git a/src/gui/res/icons/16x16/synergy-disconnected.png b/src/gui/res/icons/16x16/synergy-disconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..12efe2d49f158a983518090264a6c3c52d69123d GIT binary patch literal 442 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 literal 0 HcmV?d00001 diff --git a/src/gui/res/icons/64x64/user-trash.png b/src/gui/res/icons/64x64/user-trash.png new file mode 100644 index 0000000000000000000000000000000000000000..05eb80ec30ea3b8e3714fc74f39e4ee527ae3195 GIT binary patch literal 3815 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 ppZ(=1%NYP?0Gt7E2EcpUz6L;Q@}F){)GP0kl+ literal 0 HcmV?d00001 diff --git a/src/gui/res/lang/nl_NL.ts b/src/gui/res/lang/nl_NL.ts new file mode 100644 index 00000000..a26cb08d --- /dev/null +++ b/src/gui/res/lang/nl_NL.ts @@ -0,0 +1,1061 @@ + + + + + AboutDialogBase + + + About Synergy + + + + + <html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:16px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:x-large; font-weight:600;"><span style=" font-size:x-large;">Synergy</span></p></body></html> + + + + + Version: + + + + + + + - + + + + + Hostname: + + + + + IP-Address: + + + + + &Ok + + + + + <p>The Synergy GUI is based on QSynergy by Volker Lanz<br/> +<br/> +Copyright © 2008 Volker Lanz (vl@fidra.de)<br/> +Copyright © 2010 Chris Schoeneman, Nick Bolton, Sorin Sbarnea</p> + + + + + ActionDialogBase + + + Configure Action + + + + + Choose the action to perform + + + + + Press a hotkey + + + + + Release a hotkey + + + + + Press and release a hotkey + + + + + only on these screens + + + + + Switch to screen + + + + + Switch in direction + + + + + left + + + + + right + + + + + up + + + + + down + + + + + Lock cursor to screen + + + + + toggle + + + + + on + + + + + off + + + + + This action is performed when + + + + + the hotkey is pressed + + + + + the hotkey is released + + + + + HotkeyDialogBase + + + Hotkey + + + + + Enter the specification for the hotkey: + + + + + MainWindow + + + &File + + + + + &Edit + + + + + &Window + + + + + &Help + + + + + <p>Version %1 is now available, <a href="%2">visit website</a>.</p> + + + + + Program can not be started + + + + + The executable<br><br>%1<br><br>could not be successfully started, although it does exist. Please check if you have sufficient permissions to run this program. + + + + + Synergy client not found + + + + + The executable for the synergy client does not exist. + + + + + Hostname is empty + + + + + Please fill in a hostname for the synergy client to connect to. + + + + + Cannot write configuration file + + + + + The temporary configuration file required to start synergy can not be written. + + + + + Configuration filename invalid + + + + + You have not filled in a valid configuration file for the synergy server. Do you want to browse for the configuration file now? + + + + + Synergy server not found + + + + + The executable for the synergy server does not exist. + + + + + Synergy terminated with an error + + + + + Synergy terminated unexpectedly with an exit code of %1.<br><br>Please see the log output for details. + + + + + &Stop + + + + + + &Start + &Beginnen + + + + &Apply + + + + + Synergy is running. + + + + + Synergy is starting. + + + + + Synergy is not running. + + + + + Browse for a synergys config file + + + + + Save configuration as... + + + + + Save failed + + + + + Could not save configuration to file. + + + + + MainWindowBase + + + Synergy + + + + + + &Start + &Beginnen + + + + &Server (share this computer's mouse and keyboard): + + + + + Use existing configuration: + + + + + &Configuration file: + + + + + &Browse... + + + + + Configure interactively: + + + + + &Configure Server... + + + + + &Client (use another computer's keyboard and mouse): + + + + + &Name of the server: + + + + + Ready + + + + + Log + + + + + &About Synergy... + + + + + &Quit + + + + + Quit + + + + + Ctrl+Q + + + + + Run + + + + + Ctrl+S + + + + + S&top + + + + + Stop + + + + + Ctrl+T + + + + + S&how Status + + + + + Ctrl+H + + + + + &Minimize + + + + + &Restore + + + + + Save configuration &as... + + + + + Save the interactively generated server configuration to a file. + + + + + Ctrl+Alt+S + + + + + Settings + + + + + Edit settings + + + + + Run Wizard + + + + + NewScreenWidget + + + Unnamed + + + + + QObject + + + Synergy Configurations (*.sgc);;All files (*.*) + + + + + Synergy Configurations (*.conf);;All files (*.*) + + + + + System tray is unavailable, quitting. + + + + + ScreenSettingsDialog + + + Screen name is empty + + + + + The name for a screen can not be empty. Please fill in a name or cancel the dialog. + + + + + ScreenSettingsDialogBase + + + Screen Settings + + + + + Screen &name: + + + + + A&liases + + + + + &Add + + + + + &Remove + + + + + &Modifier keys + + + + + &Shift: + + + + + + + + + Shift + + + + + + + + + Ctrl + + + + + + + + + Alt + + + + + + + + + Meta + + + + + + + + + Super + + + + + + + + + None + + + + + &Ctrl: + + + + + Al&t: + + + + + M&eta: + + + + + S&uper: + + + + + &Dead corners + + + + + Top-left + + + + + Top-right + + + + + Bottom-left + + + + + Bottom-right + + + + + Corner Si&ze: + + + + + &Fixes + + + + + Fix CAPS LOCK key + + + + + Fix NUM LOCK key + + + + + Fix SCROLL LOCK key + + + + + Fix XTest for Xinerama + + + + + ScreenSetupModel + + + <center>Screen: <b>%1</b></center><br>Double click to edit settings<br>Drag screen to the trashcan to remove it + + + + + ServerConfigDialogBase + + + Server Configuration + + + + + Screens and links + + + + + Drag a screen from the grid to the trashcan to remove it. + + + + + Configure the layout of your synergy server configuration. + + + + + Drag this button to the grid to add a new screen. + + + + + Drag new screens to the grid or move existing ones around. +Drag a screen to the trashcan to delete it. +Double click on a screen to edit its settings. + + + + + Hotkeys + + + + + &Hotkeys + + + + + &New + + + + + &Edit + + + + + &Remove + + + + + A&ctions + + + + + Ne&w + + + + + E&dit + + + + + Re&move + + + + + Advanced server settings + + + + + &Switch + + + + + Switch &after waiting + + + + + + + ms + + + + + Switch on double &tap within + + + + + &Options + + + + + &Check clients every + + + + + Use &relative mouse moves + + + + + S&ynchronize screen savers + + + + + Don't take &foreground window on Windows servers + + + + + &Dead corners + + + + + To&p-left + + + + + Top-rig&ht + + + + + &Bottom-left + + + + + Bottom-ri&ght + + + + + Cor&ner Size: + + + + + SettingsDialog + + + Save log file to... + + + + + SettingsDialogBase + + + Settings + + + + + &Advanced + + + + + Sc&reen name: + + + + + P&ort: + + + + + &Interface: + + + + + Enable &gamepad support for Windows + + + + + &Start + &Beginnen + + + + &Start Synergy after logging in + + + + + &Automatically start server/client + + + + + &Hide when server/client starts + + + + + Logging + + + + + &Logging level: + + + + + Log to file: + + + + + Browse... + + + + + Error + + + + + Warning + + + + + Note + + + + + Info + + + + + Debug + + + + + Debug1 + + + + + Debug2 + + + + + SetupWizard + + + Service (Windows only) + + + + + Setup Synergy + + + + + + Please select an option. + + + + + SetupWizardBase + + + Setup Synergy + + + + + Server or Client? + + + + + &Server (new setup) + + + + + This is the first computer you are configuring. Your keyboard and mouse are connected to this computer. This will allow you to move your mouse over to another computer's screen. There can only be one server in your setup. + + + + + &Client (add to setup) + + + + + You have already set up a server. This a computer you wish to control using the server's keyboard and mouse. There can be many clients in your setup. + + + + + Startup Mode + + + + + &Service + + + + + Always run Synergy in the background automatically at startup. This allows you to use Synergy at the login prompt. If you experience problems while using this mode, you can run this wizard again and select the "Desktop" option. + + + + + &Desktop + + + + + Synergy will start when your desktop loads (after you login). This mode can help if you experience problems with the "Service" option. + + + + + &None + + + + + Do not start Synergy when your comptuer starts. + + + + + VersionChecker + + + Unknown + + + + diff --git a/src/gui/res/mac/QSynergy.icns b/src/gui/res/mac/QSynergy.icns new file mode 100644 index 0000000000000000000000000000000000000000..0d87c5a551aa3e814751d3f6cd8d2702eca558e2 GIT binary patch literal 124558 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

6xvL;Uw=ZAqJu5s!S-&i_ctU1kVD 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 + + + + CFBundleIconFile + Synergy.icns + CFBundlePackageType + APPL + CFBundleGetInfoString + 0.9.0 + CFBundleSignature + ???? + CFBundleExecutable + Synergy + + diff --git a/src/gui/res/win/Synergy.rc b/src/gui/res/win/Synergy.rc new file mode 100644 index 00000000..62ec63d6 --- /dev/null +++ b/src/gui/res/win/Synergy.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "../icons/256x256/synergy.ico" diff --git a/src/gui/src/AboutDialog.cpp b/src/gui/src/AboutDialog.cpp new file mode 100644 index 00000000..d40bd4d1 --- /dev/null +++ b/src/gui/src/AboutDialog.cpp @@ -0,0 +1,46 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "AboutDialog.h" + +#include +#include +#include + +static QString getIPAddress() +{ + QList addresses = QNetworkInterface::allAddresses(); + + for (int i = 0; i < addresses.size(); i++) + if (addresses[i].protocol() == QAbstractSocket::IPv4Protocol && addresses[i] != QHostAddress(QHostAddress::LocalHost)) + return addresses[i].toString(); + + return "Unknown"; +} + +AboutDialog::AboutDialog(QWidget* parent, const QString& synergyApp) : + QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint), + Ui::AboutDialogBase() +{ + setupUi(this); + + m_versionChecker.setApp(synergyApp); + m_pLabelSynergyVersion->setText(m_versionChecker.getVersion()); + m_pLabelHostname->setText(QHostInfo::localHostName()); + m_pLabelIPAddress->setText(getIPAddress()); +} + diff --git a/src/gui/src/AboutDialog.h b/src/gui/src/AboutDialog.h new file mode 100644 index 00000000..9722f16b --- /dev/null +++ b/src/gui/src/AboutDialog.h @@ -0,0 +1,42 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(ABOUTDIALOG__H) + +#define ABOUTDIALOG__H + +#include +#include "VersionChecker.h" + +#include "ui_AboutDialogBase.h" + +class QWidget; +class QString; + +class AboutDialog : public QDialog, public Ui::AboutDialogBase +{ + Q_OBJECT + + public: + AboutDialog(QWidget* parent, const QString& synergyApp = QString()); + + private: + VersionChecker m_versionChecker; +}; + +#endif + diff --git a/src/gui/src/Action.cpp b/src/gui/src/Action.cpp new file mode 100644 index 00000000..0493b3c5 --- /dev/null +++ b/src/gui/src/Action.cpp @@ -0,0 +1,149 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "Action.h" + +#include +#include + +const char* Action::m_ActionTypeNames[] = +{ + "keyDown", "keyUp", "keystroke", + "switchToScreen", "switchInDirection", "lockCursorToScreen", + "mouseDown", "mouseUp", "mousebutton" +}; + +const char* Action::m_SwitchDirectionNames[] = { "left", "right", "up", "down" }; +const char* Action::m_LockCursorModeNames[] = { "toggle", "on", "off" }; + +Action::Action() : + m_KeySequence(), + m_Type(keystroke), + m_TypeScreenNames(), + m_SwitchScreenName(), + m_SwitchDirection(switchLeft), + m_LockCursorMode(lockCursorToggle), + m_ActiveOnRelease(false), + m_HasScreens(false) +{ +} + +QString Action::text() const +{ + QString text = QString(m_ActionTypeNames[keySequence().isMouseButton() ? type() + 6 : type() ]) + "("; + + switch (type()) + { + case keyDown: + case keyUp: + case keystroke: + { + text += keySequence().toString(); + + if (!keySequence().isMouseButton()) + { + const QStringList& screens = typeScreenNames(); + if (haveScreens() && !screens.isEmpty()) + { + text += ","; + + for (int i = 0; i < screens.size(); i++) + { + text += screens[i]; + if (i != screens.size() - 1) + text += ":"; + } + } + else + text += ",*"; + } + } + break; + + case switchToScreen: + text += switchScreenName(); + break; + + case switchInDirection: + text += m_SwitchDirectionNames[m_SwitchDirection]; + break; + + case lockCursorToScreen: + text += m_LockCursorModeNames[m_LockCursorMode]; + break; + + default: + Q_ASSERT(0); + break; + } + + text += ")"; + + return text; +} + +void Action::loadSettings(QSettings& settings) +{ + keySequence().loadSettings(settings); + setType(settings.value("type", keyDown).toInt()); + + typeScreenNames().clear(); + int numTypeScreens = settings.beginReadArray("typeScreenNames"); + for (int i = 0; i < numTypeScreens; i++) + { + settings.setArrayIndex(i); + typeScreenNames().append(settings.value("typeScreenName").toString()); + } + settings.endArray(); + + setSwitchScreenName(settings.value("switchScreenName").toString()); + setSwitchDirection(settings.value("switchInDirection", switchLeft).toInt()); + setLockCursorMode(settings.value("lockCursorToScreen", lockCursorToggle).toInt()); + setActiveOnRelease(settings.value("activeOnRelease", false).toBool()); + setHaveScreens(settings.value("hasScreens", false).toBool()); +} + +void Action::saveSettings(QSettings& settings) const +{ + keySequence().saveSettings(settings); + settings.setValue("type", type()); + + settings.beginWriteArray("typeScreenNames"); + for (int i = 0; i < typeScreenNames().size(); i++) + { + settings.setArrayIndex(i); + settings.setValue("typeScreenName", typeScreenNames()[i]); + } + settings.endArray(); + + settings.setValue("switchScreenName", switchScreenName()); + settings.setValue("switchInDirection", switchDirection()); + settings.setValue("lockCursorToScreen", lockCursorMode()); + settings.setValue("activeOnRelease", activeOnRelease()); + settings.setValue("hasScreens", haveScreens()); +} + +QTextStream& operator<<(QTextStream& outStream, const Action& action) +{ + if (action.activeOnRelease()) + outStream << ";"; + + outStream << action.text(); + + return outStream; +} + diff --git a/src/gui/src/Action.h b/src/gui/src/Action.h new file mode 100644 index 00000000..ee911421 --- /dev/null +++ b/src/gui/src/Action.h @@ -0,0 +1,88 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(ACTION_H) + +#define ACTION_H + +#include "KeySequence.h" + +#include +#include +#include + +class ActionDialog; +class QSettings; +class QTextStream; + +class Action +{ + friend class ActionDialog; + friend QTextStream& operator<<(QTextStream& outStream, const Action& action); + + public: + enum ActionType { keyDown, keyUp, keystroke, switchToScreen, switchInDirection, lockCursorToScreen, mouseDown, mouseUp, mousebutton }; + enum SwitchDirection { switchLeft, switchRight, switchUp, switchDown }; + enum LockCursorMode { lockCursorToggle, lockCursonOn, lockCursorOff }; + + public: + Action(); + + public: + QString text() const; + const KeySequence& keySequence() const { return m_KeySequence; } + void loadSettings(QSettings& settings); + void saveSettings(QSettings& settings) const; + int type() const { return m_Type; } + const QStringList& typeScreenNames() const { return m_TypeScreenNames; } + const QString& switchScreenName() const { return m_SwitchScreenName; } + int switchDirection() const { return m_SwitchDirection; } + int lockCursorMode() const { return m_LockCursorMode; } + bool activeOnRelease() const { return m_ActiveOnRelease; } + bool haveScreens() const { return m_HasScreens; } + + protected: + KeySequence& keySequence() { return m_KeySequence; } + void setKeySequence(const KeySequence& seq) { m_KeySequence = seq; } + void setType(int t) { m_Type = t; } + QStringList& typeScreenNames() { return m_TypeScreenNames; } + void setSwitchScreenName(const QString& n) { m_SwitchScreenName = n; } + void setSwitchDirection(int d) { m_SwitchDirection = d; } + void setLockCursorMode(int m) { m_LockCursorMode = m; } + void setActiveOnRelease(bool b) { m_ActiveOnRelease = b; } + void setHaveScreens(bool b) { m_HasScreens = b; } + + private: + KeySequence m_KeySequence; + int m_Type; + QStringList m_TypeScreenNames; + QString m_SwitchScreenName; + int m_SwitchDirection; + int m_LockCursorMode; + bool m_ActiveOnRelease; + bool m_HasScreens; + + static const char* m_ActionTypeNames[]; + static const char* m_SwitchDirectionNames[]; + static const char* m_LockCursorModeNames[]; +}; + +typedef QList ActionList; + +QTextStream& operator<<(QTextStream& outStream, const Action& action); + +#endif diff --git a/src/gui/src/ActionDialog.cpp b/src/gui/src/ActionDialog.cpp new file mode 100644 index 00000000..0dfde93d --- /dev/null +++ b/src/gui/src/ActionDialog.cpp @@ -0,0 +1,108 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "ActionDialog.h" + +#include "Hotkey.h" +#include "Action.h" +#include "ServerConfig.h" +#include "KeySequence.h" + +#include +#include + +ActionDialog::ActionDialog(QWidget* parent, ServerConfig& config, Hotkey& hotkey, Action& action) : + QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint), + Ui::ActionDialogBase(), + m_ServerConfig(config), + m_Hotkey(hotkey), + m_Action(action), + m_pButtonGroupType(new QButtonGroup(this)) +{ + setupUi(this); + + // work around Qt Designer's lack of a QButtonGroup; we need it to get + // at the button id of the checked radio button + QRadioButton* const typeButtons[] = { m_pRadioPress, m_pRadioRelease, m_pRadioPressAndRelease, m_pRadioSwitchToScreen, m_pRadioSwitchInDirection, m_pRadioLockCursorToScreen }; + + for (unsigned int i = 0; i < sizeof(typeButtons) / sizeof(typeButtons[0]); i++) + m_pButtonGroupType->addButton(typeButtons[i], i); + + m_pKeySequenceWidgetHotkey->setText(m_Action.keySequence().toString()); + m_pKeySequenceWidgetHotkey->setKeySequence(m_Action.keySequence()); + m_pButtonGroupType->button(m_Action.type())->setChecked(true); + m_pComboSwitchInDirection->setCurrentIndex(m_Action.switchDirection()); + m_pComboLockCursorToScreen->setCurrentIndex(m_Action.lockCursorMode()); + + if (m_Action.activeOnRelease()) + m_pRadioHotkeyReleased->setChecked(true); + else + m_pRadioHotkeyPressed->setChecked(true); + + m_pGroupBoxScreens->setChecked(m_Action.haveScreens()); + + int idx = 0; + foreach(const Screen& screen, serverConfig().screens()) + if (!screen.isNull()) + { + QListWidgetItem *pListItem = new QListWidgetItem(screen.name()); + m_pListScreens->addItem(pListItem); + if (m_Action.typeScreenNames().indexOf(screen.name()) != -1) + m_pListScreens->setCurrentItem(pListItem); + + m_pComboSwitchToScreen->addItem(screen.name()); + if (screen.name() == m_Action.switchScreenName()) + m_pComboSwitchToScreen->setCurrentIndex(idx); + + idx++; + } +} + +void ActionDialog::accept() +{ + if (!sequenceWidget()->valid() && m_pButtonGroupType->checkedId() >= 0 && m_pButtonGroupType->checkedId() < 3) + return; + + m_Action.setKeySequence(sequenceWidget()->keySequence()); + m_Action.setType(m_pButtonGroupType->checkedId()); + m_Action.setHaveScreens(m_pGroupBoxScreens->isChecked()); + + m_Action.typeScreenNames().clear(); + foreach(const QListWidgetItem* pItem, m_pListScreens->selectedItems()) + m_Action.typeScreenNames().append(pItem->text()); + + m_Action.setSwitchScreenName(m_pComboSwitchToScreen->currentText()); + m_Action.setSwitchDirection(m_pComboSwitchInDirection->currentIndex()); + m_Action.setLockCursorMode(m_pComboLockCursorToScreen->currentIndex()); + m_Action.setActiveOnRelease(m_pRadioHotkeyReleased->isChecked()); + + QDialog::accept(); +} + +void ActionDialog::on_m_pKeySequenceWidgetHotkey_keySequenceChanged() +{ + if (sequenceWidget()->keySequence().isMouseButton()) + { + m_pGroupBoxScreens->setEnabled(false); + m_pListScreens->setEnabled(false); + } + else + { + m_pGroupBoxScreens->setEnabled(true); + m_pListScreens->setEnabled(true); + } +} diff --git a/src/gui/src/ActionDialog.h b/src/gui/src/ActionDialog.h new file mode 100644 index 00000000..8bbee52a --- /dev/null +++ b/src/gui/src/ActionDialog.h @@ -0,0 +1,55 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(ACTIONDIALOG_H) + +#define ACTIONDIALOG_H + +#include + +#include "ui_ActionDialogBase.h" + +class Hotkey; +class Action; +class QRadioButton; +class QButtonGroup; +class ServerConfig; + +class ActionDialog : public QDialog, public Ui::ActionDialogBase +{ + Q_OBJECT + + public: + ActionDialog(QWidget* parent, ServerConfig& config, Hotkey& hotkey, Action& action); + + protected slots: + void accept(); + void on_m_pKeySequenceWidgetHotkey_keySequenceChanged(); + + protected: + const KeySequenceWidget* sequenceWidget() const { return m_pKeySequenceWidgetHotkey; } + const ServerConfig& serverConfig() const { return m_ServerConfig; } + + private: + const ServerConfig& m_ServerConfig; + Hotkey& m_Hotkey; + Action& m_Action; + + QButtonGroup* m_pButtonGroupType; +}; + +#endif diff --git a/src/gui/src/AppConfig.cpp b/src/gui/src/AppConfig.cpp new file mode 100644 index 00000000..68c162a2 --- /dev/null +++ b/src/gui/src/AppConfig.cpp @@ -0,0 +1,180 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "AppConfig.h" + +#include +#include + +#if defined(Q_OS_WIN) +const char AppConfig::m_SynergysName[] = "synergys.exe"; +const char AppConfig::m_SynergycName[] = "synergyc.exe"; +const char AppConfig::m_SynergyLogDir[] = "log/"; +#else +const char AppConfig::m_SynergysName[] = "synergys"; +const char AppConfig::m_SynergycName[] = "synergyc"; +const char AppConfig::m_SynergyLogDir[] = "/var/log/"; +#endif + +static const char* logLevelNames[] = +{ + "ERROR", + "WARNING", + "NOTE", + "INFO", + "DEBUG", + "DEBUG1", + "DEBUG2" +}; + +AppConfig::AppConfig(QSettings* settings) : + m_pSettings(settings), + m_AutoConnect(false), + m_ScreenName(), + m_Port(24800), + m_Interface(), + m_LogLevel(0), + m_AutoStart(false), + m_AutoHide(false), + m_AutoStartPrompt(false), + m_WizardHasRun(false), + m_GameModeIndex(0), + m_GamePollingDynamic(true), + m_GamePollingFrequency(60) +{ + Q_ASSERT(m_pSettings); + + loadSettings(); +} + +AppConfig::~AppConfig() +{ + saveSettings(); +} + +QString AppConfig::synergyLogDir() const +{ +#if defined(Q_OS_WIN) + // on windows, we want to log to program files + return synergyProgramDir() + "log/"; +#else + // on unix, we'll log to the standard log dir + return "/var/log/"; +#endif +} + +QString AppConfig::synergyProgramDir() const +{ + // synergy binaries should be in the same dir. + return QCoreApplication::applicationDirPath() + "/"; +} + +void AppConfig::persistLogDir() +{ + QDir dir = synergyLogDir(); + + // persist the log directory + if (!dir.exists()) + { + dir.mkpath(dir.path()); + } +} + +QString AppConfig::logLevelText() const +{ + return logLevelNames[logLevel()]; +} + +void AppConfig::setAutoStart(bool b) +{ + m_AutoStart = b; + + // always create or delete the links/files/entries even if they exist already, + // in case it was broken. + +#if defined(Q_OS_LINUX) + + QString desktopFileName("synergy.desktop"); + QString desktopFilePath("/usr/share/applications/" + desktopFileName); + QString autoStartPath(QDir::homePath() + "/.config/autostart/" + desktopFileName); + + if (b) + { + QFile::link(desktopFilePath, autoStartPath); + } + else + { + QFile::remove(autoStartPath); + } + +#elif defined(Q_OS_WIN) + + QSettings settings("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", QSettings::NativeFormat); + QString path("Synergy"); + + if (b) + { + settings.setValue(path, QCoreApplication::applicationFilePath()); + } + else + { + settings.remove(path); + } + settings.sync(); + +#endif + + // TODO: mac os x auto start +} + +void AppConfig::loadSettings() +{ + m_AutoConnect = settings().value("autoConnect", false).toBool(); + m_ScreenName = settings().value("screenName", QHostInfo::localHostName()).toString(); + m_Port = settings().value("port", 24800).toInt(); + m_Interface = settings().value("interface").toString(); + m_LogLevel = settings().value("logLevel", 2).toInt(); + m_LogToFile = settings().value("logToFile", false).toBool(); + m_LogFilename = settings().value("logFilename", synergyLogDir() + "synergy.log").toString(); + m_AutoStart = settings().value("autoStart", false).toBool(); + m_AutoHide = settings().value("autoHide", true).toBool(); + m_AutoStartPrompt = settings().value("autoStartPrompt", true).toBool(); + m_WizardHasRun = settings().value("wizardHasRun", false).toBool(); + m_ProcessMode = (ProcessMode)settings().value("processMode", Desktop).toInt(); + m_GameModeIndex = settings().value("gameModeIndex", 0).toInt(); + m_GamePollingDynamic = settings().value("gamePollingDynamic", true).toBool(); + m_GamePollingFrequency = settings().value("gamePollingFrequency", 60).toInt(); +} + +void AppConfig::saveSettings() +{ + settings().setValue("autoConnect", m_AutoConnect); + settings().setValue("screenName", m_ScreenName); + settings().setValue("port", m_Port); + settings().setValue("interface", m_Interface); + settings().setValue("logLevel", m_LogLevel); + settings().setValue("logToFile", m_LogToFile); + settings().setValue("logFilename", m_LogFilename); + settings().setValue("autoStart", m_AutoStart); + settings().setValue("autoHide", m_AutoHide); + settings().setValue("autoStartPrompt", m_AutoStartPrompt); + settings().setValue("wizardHasRun", m_WizardHasRun); + settings().setValue("processMode", m_ProcessMode); + settings().setValue("gameModeIndex", m_GameModeIndex); + settings().setValue("gamePollingDynamic", m_GamePollingDynamic); + settings().setValue("gamePollingFrequency", m_GamePollingFrequency); +} diff --git a/src/gui/src/AppConfig.h b/src/gui/src/AppConfig.h new file mode 100644 index 00000000..beae4f0e --- /dev/null +++ b/src/gui/src/AppConfig.h @@ -0,0 +1,112 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(APPCONFIG_H) + +#define APPCONFIG_H + +#include + +class QSettings; +class SettingsDialog; + +enum ProcessMode { + Service, + Desktop +}; + +class AppConfig +{ + friend class SettingsDialog; + friend class MainWindow; + friend class SetupWizard; + + public: + AppConfig(QSettings* settings); + ~AppConfig(); + + public: + bool autoConnect() const { return m_AutoConnect; } + const QString& screenName() const { return m_ScreenName; } + int port() const { return m_Port; } + const QString& interface() const { return m_Interface; } + int logLevel() const { return m_LogLevel; } + bool logToFile() const { return m_LogToFile; } + const QString& logFilename() const { return m_LogFilename; } + QString logLevelText() const; + bool autoStart() const { return m_AutoStart; } + bool autoHide() const { return m_AutoHide; } + bool autoStartPrompt() const { return m_AutoStartPrompt; } + bool wizardHasRun() const { return m_WizardHasRun; } + ProcessMode processMode() const { return m_ProcessMode; } + int gameModeIndex() const { return m_GameModeIndex; } + bool gamePollingDynamic() const { return m_GamePollingDynamic; } + int gamePollingFrequency() const { return m_GamePollingFrequency; } + + QString synergysName() const { return m_SynergysName; } + QString synergycName() const { return m_SynergycName; } + QString synergyProgramDir() const; + QString synergyLogDir() const; + + bool detectPath(const QString& name, QString& path); + void persistLogDir(); + + protected: + QSettings& settings() { return *m_pSettings; } + void setAutoConnect(bool b) { m_AutoConnect = b; } + void setScreenName(const QString& s) { m_ScreenName = s; } + void setPort(int i) { m_Port = i; } + void setInterface(const QString& s) { m_Interface = s; } + void setLogLevel(int i) { m_LogLevel = i; } + void setLogToFile(bool b) { m_LogToFile = b; } + void setLogFilename(const QString& s) { m_LogFilename = s; } + void setAutoStart(bool b); + void setAutoHide(bool b) { m_AutoHide = b; } + void setAutoStartPrompt(bool b) { m_AutoStartPrompt = b; } + void setWizardHasRun(bool b) { m_WizardHasRun = b; } + void setProcessMode(ProcessMode p) { m_ProcessMode = p; } + void setGameModeIndex(int i) { m_GameModeIndex = i; } + void setGamePollingDynamic(bool b) { m_GamePollingDynamic = b; } + void setGamePollingFrequency(int i) { m_GamePollingFrequency = i; } + + void loadSettings(); + void saveSettings(); + + private: + QSettings* m_pSettings; + bool m_AutoConnect; + QString m_ScreenName; + int m_Port; + QString m_Interface; + int m_LogLevel; + bool m_LogToFile; + QString m_LogFilename; + bool m_AutoStart; + bool m_AutoHide; + bool m_AutoStartPrompt; + bool m_WizardHasRun; + ProcessMode m_ProcessMode; + int m_GameModeIndex; + bool m_GamePollingDynamic; + int m_GamePollingFrequency; + + static const char m_SynergysName[]; + static const char m_SynergycName[]; + static const char m_SynergyLogDir[]; +}; + +#endif diff --git a/src/gui/src/BaseConfig.cpp b/src/gui/src/BaseConfig.cpp new file mode 100644 index 00000000..1cf9a3c2 --- /dev/null +++ b/src/gui/src/BaseConfig.cpp @@ -0,0 +1,45 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "BaseConfig.h" + +const char* BaseConfig::m_ModifierNames[] = +{ + "shift", + "ctrl", + "alt", + "meta", + "super", + "none" +}; + +const char* BaseConfig::m_FixNames[] = +{ + "halfDuplexCapsLock", + "halfDuplexNumLock", + "halfDuplexScrollLock", + "xtestIsXineramaUnaware" +}; + +const char* BaseConfig::m_SwitchCornerNames[] = +{ + "top-left", + "top-right", + "bottom-left", + "bottom-right" +}; + diff --git a/src/gui/src/BaseConfig.h b/src/gui/src/BaseConfig.h new file mode 100644 index 00000000..541d3f82 --- /dev/null +++ b/src/gui/src/BaseConfig.h @@ -0,0 +1,90 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(BASECONFIG_H) + +#define BASECONFIG_H + +#include +#include +#include + +class BaseConfig +{ + public: + enum Modifier { DefaultMod = -1, Shift, Ctrl, Alt, Meta, Super, None, NumModifiers }; + enum SwitchCorner { TopLeft, TopRight, BottomLeft, BottomRight, NumSwitchCorners }; + enum Fix { CapsLock, NumLock, ScrollLock, XTest, NumFixes }; + + protected: + BaseConfig() {} + virtual ~BaseConfig() {} + + protected: + template + void readSettings(QSettings& settings, T1& array, const QString& arrayName, const T2& deflt) + { + int entries = settings.beginReadArray(arrayName + "Array"); + array.clear(); + for (int i = 0; i < entries; i++) + { + settings.setArrayIndex(i); + QVariant v = settings.value(arrayName, deflt); + array.append(v.value()); + } + settings.endArray(); + } + + template + void readSettings(QSettings& settings, T1& array, const QString& arrayName, const T2& deflt, int entries) + { + Q_ASSERT(array.size() >= entries); + settings.beginReadArray(arrayName + "Array"); + for (int i = 0; i < entries; i++) + { + settings.setArrayIndex(i); + QVariant v = settings.value(arrayName, deflt); + array[i] = v.value(); + } + settings.endArray(); + } + + template + void writeSettings(QSettings& settings, const T& array, const QString& arrayName) const + { + settings.beginWriteArray(arrayName + "Array"); + for (int i = 0; i < array.size(); i++) + { + settings.setArrayIndex(i); + settings.setValue(arrayName, array[i]); + } + settings.endArray(); + } + + + public: + static const char* modifierName(int idx) { return m_ModifierNames[idx]; } + static const char* fixName(int idx) { return m_FixNames[idx]; } + static const char* switchCornerName(int idx) { return m_SwitchCornerNames[idx]; } + + private: + static const char* m_ModifierNames[]; + static const char* m_FixNames[]; + static const char* m_SwitchCornerNames[]; +}; + +#endif diff --git a/src/gui/src/Hotkey.cpp b/src/gui/src/Hotkey.cpp new file mode 100644 index 00000000..00cb7d72 --- /dev/null +++ b/src/gui/src/Hotkey.cpp @@ -0,0 +1,74 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "Hotkey.h" + +#include + +Hotkey::Hotkey() : + m_KeySequence(), + m_Actions() +{ +} + +QString Hotkey::text() const +{ + QString text = keySequence().toString(); + + if (keySequence().isMouseButton()) + return "mousebutton(" + text + ")"; + + return "keystroke(" + text + ")"; +} + +void Hotkey::loadSettings(QSettings& settings) +{ + keySequence().loadSettings(settings); + + actions().clear(); + int num = settings.beginReadArray("actions"); + for (int i = 0; i < num; i++) + { + settings.setArrayIndex(i); + Action a; + a.loadSettings(settings); + actions().append(a); + } + + settings.endArray(); +} + +void Hotkey::saveSettings(QSettings& settings) const +{ + keySequence().saveSettings(settings); + + settings.beginWriteArray("actions"); + for (int i = 0; i < actions().size(); i++) + { + settings.setArrayIndex(i); + actions()[i].saveSettings(settings); + } + settings.endArray(); +} + +QTextStream& operator<<(QTextStream& outStream, const Hotkey& hotkey) +{ + for (int i = 0; i < hotkey.actions().size(); i++) + outStream << "\t" << hotkey.text() << " = " << hotkey.actions()[i] << endl; + + return outStream; +} diff --git a/src/gui/src/Hotkey.h b/src/gui/src/Hotkey.h new file mode 100644 index 00000000..6f1f49bc --- /dev/null +++ b/src/gui/src/Hotkey.h @@ -0,0 +1,65 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(HOTKEY_H) + +#define HOTKEY_H + +#include +#include +#include + +#include "Action.h" +#include "KeySequence.h" + +class HotkeyDialog; +class ServerConfigDialog; +class QSettings; + +class Hotkey +{ + friend class HotkeyDialog; + friend class ServerConfigDialog; + friend QTextStream& operator<<(QTextStream& outStream, const Hotkey& hotkey); + + public: + Hotkey(); + + public: + QString text() const; + const KeySequence& keySequence() const { return m_KeySequence; } + const ActionList& actions() const { return m_Actions; } + + void loadSettings(QSettings& settings); + void saveSettings(QSettings& settings) const; + + protected: + KeySequence& keySequence() { return m_KeySequence; } + void setKeySequence(const KeySequence& seq) { m_KeySequence = seq; } + ActionList& actions() { return m_Actions; } + + + private: + KeySequence m_KeySequence; + ActionList m_Actions; +}; + +typedef QList HotkeyList; + +QTextStream& operator<<(QTextStream& outStream, const Hotkey& hotkey); + +#endif diff --git a/src/gui/src/HotkeyDialog.cpp b/src/gui/src/HotkeyDialog.cpp new file mode 100644 index 00000000..060666d8 --- /dev/null +++ b/src/gui/src/HotkeyDialog.cpp @@ -0,0 +1,40 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "HotkeyDialog.h" + +#include +#include + +HotkeyDialog::HotkeyDialog (QWidget* parent, Hotkey& hotkey) : + QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint), + Ui::HotkeyDialogBase(), + m_Hotkey(hotkey) +{ + setupUi(this); + + m_pKeySequenceWidgetHotkey->setText(m_Hotkey.text()); +} + +void HotkeyDialog::accept() +{ + if (!sequenceWidget()->valid()) + return; + + hotkey().setKeySequence(sequenceWidget()->keySequence()); + QDialog::accept(); +} diff --git a/src/gui/src/HotkeyDialog.h b/src/gui/src/HotkeyDialog.h new file mode 100644 index 00000000..c22726e9 --- /dev/null +++ b/src/gui/src/HotkeyDialog.h @@ -0,0 +1,48 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(HOTKEYDIALOG_H) + +#define HOTKEYDIALOG_H + +#include "ui_HotkeyDialogBase.h" +#include "Hotkey.h" + +#include + +class HotkeyDialog : public QDialog, public Ui::HotkeyDialogBase +{ + Q_OBJECT + + public: + HotkeyDialog(QWidget* parent, Hotkey& hotkey); + + public: + const Hotkey& hotkey() const { return m_Hotkey; } + + protected slots: + void accept(); + + protected: + const KeySequenceWidget* sequenceWidget() const { return m_pKeySequenceWidgetHotkey; } + Hotkey& hotkey() { return m_Hotkey; } + + private: + Hotkey& m_Hotkey; +}; + +#endif diff --git a/src/gui/src/IpcLogReader.cpp b/src/gui/src/IpcLogReader.cpp new file mode 100644 index 00000000..19fae86c --- /dev/null +++ b/src/gui/src/IpcLogReader.cpp @@ -0,0 +1,67 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "IpcLogReader.h" + +#if defined(Q_OS_WIN) +#define WIN32_LEAN_AND_MEAN +#include +#endif + +IpcLogReader::IpcLogReader() +{ +} + +IpcLogReader::~IpcLogReader() +{ +} + +void +IpcLogReader::run() +{ +#if defined(Q_OS_WIN) + + const WCHAR* name = L"\\\\.\\pipe\\SynergyLog"; + + HANDLE pipe = CreateFile( + name, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); + + if (pipe == INVALID_HANDLE_VALUE) + { + receivedLine( + QString("ERROR: could not connect to service log, error: ") + + QString::number(GetLastError())); + return; + } + + char buffer[1024]; + DWORD bytesRead; + + while (true) + { + if (!ReadFile(pipe, buffer, sizeof(buffer), &bytesRead, NULL)) { + break; + } + + buffer[bytesRead] = '\0'; + + QString text = QString::fromAscii(buffer, bytesRead); + text = text.trimmed().append("\n"); + receivedLine(text); + } +#endif +} diff --git a/src/gui/src/IpcLogReader.h b/src/gui/src/IpcLogReader.h new file mode 100644 index 00000000..13dcbaa4 --- /dev/null +++ b/src/gui/src/IpcLogReader.h @@ -0,0 +1,32 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 +#include + +class IpcLogReader : public QThread +{ + Q_OBJECT +public: + IpcLogReader(); + virtual ~IpcLogReader(); + void run(); +signals: + void receivedLine(const QString& text); +}; diff --git a/src/gui/src/KeySequence.cpp b/src/gui/src/KeySequence.cpp new file mode 100644 index 00000000..e063bd58 --- /dev/null +++ b/src/gui/src/KeySequence.cpp @@ -0,0 +1,236 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "KeySequence.h" + +#include +#include + +// this table originally comes from Qt sources (gui/kernel/qkeysequence.cpp) +// and is heavily modified for QSynergy +static const struct +{ + int key; + const char* name; +} keyname[] = +{ + { Qt::Key_Space, "Space" }, + { Qt::Key_Escape, "Escape" }, + { Qt::Key_Tab, "Tab" }, + { Qt::Key_Backtab, "LeftTab" }, + { Qt::Key_Backspace, "BackSpace" }, + { Qt::Key_Return, "Return" }, + { Qt::Key_Insert, "Insert" }, + { Qt::Key_Delete, "Delete" }, + { Qt::Key_Pause, "Pause" }, + { Qt::Key_Print, "Print" }, + { Qt::Key_SysReq, "SysReq" }, + { Qt::Key_Home, "Home" }, + { Qt::Key_End, "End" }, + { Qt::Key_Left, "Left" }, + { Qt::Key_Up, "Up" }, + { Qt::Key_Right, "Right" }, + { Qt::Key_Down, "Down" }, + { Qt::Key_PageUp, "PageUp" }, + { Qt::Key_PageDown, "PageDown" }, + { Qt::Key_CapsLock, "CapsLock" }, + { Qt::Key_NumLock, "NumLock" }, + { Qt::Key_ScrollLock, "ScrollLock" }, + { Qt::Key_Menu, "Menu" }, + { Qt::Key_Help, "Help" }, + { Qt::Key_Enter, "KP_Enter" }, + { Qt::Key_Clear, "Clear" }, + + { Qt::Key_Back, "WWWBack" }, + { Qt::Key_Forward, "WWWForward" }, + { Qt::Key_Stop, "WWWStop" }, + { Qt::Key_Refresh, "WWWRefresh" }, + { Qt::Key_VolumeDown, "AudioDown" }, + { Qt::Key_VolumeMute, "AudioMute" }, + { Qt::Key_VolumeUp, "AudioUp" }, + { Qt::Key_MediaPlay, "AudioPlay" }, + { Qt::Key_MediaStop, "AudioStop" }, + { Qt::Key_MediaPrevious,"AudioPrev" }, + { Qt::Key_MediaNext, "AudioNext" }, + { Qt::Key_HomePage, "WWWHome" }, + { Qt::Key_Favorites, "WWWFavorites" }, + { Qt::Key_Search, "WWWSearch" }, + { Qt::Key_Standby, "Sleep" }, + { Qt::Key_LaunchMail, "AppMail" }, + { Qt::Key_LaunchMedia, "AppMedia" }, + { Qt::Key_Launch0, "AppUser1" }, + { Qt::Key_Launch1, "AppUser2" }, + { Qt::Key_Select, "Select" }, + + { 0, 0 } +}; + +KeySequence::KeySequence() : + m_Sequence(), + m_Modifiers(0), + m_IsValid(false) +{ +} + +bool KeySequence::isMouseButton() const +{ + return !m_Sequence.isEmpty() && m_Sequence.last() < Qt::Key_Space; +} + +QString KeySequence::toString() const +{ + QString result; + + for (int i = 0; i < m_Sequence.size(); i++) + { + result += keyToString(m_Sequence[i]); + + if (i != m_Sequence.size() - 1) + result += "+"; + } + + return result; +} + +bool KeySequence::appendMouseButton(int button) +{ + return appendKey(button, 0); +} + +bool KeySequence::appendKey(int key, int modifiers) +{ + if (m_Sequence.size() == 4) + return true; + + switch(key) + { + case Qt::Key_AltGr: + return false; + + case Qt::Key_Control: + case Qt::Key_Alt: + case Qt::Key_Shift: + case Qt::Key_Meta: + case Qt::Key_Menu: + { + int mod = modifiers & (~m_Modifiers); + if (mod) + { + m_Sequence.append(mod); + m_Modifiers |= mod; + } + } + break; + + default: + // see if we can handle this key, if not, don't accept it + if (keyToString(key).isEmpty()) + break; + + m_Sequence.append(key); + setValid(true); + return true; + } + + return false; +} + +void KeySequence::loadSettings(QSettings& settings) +{ + sequence().clear(); + int num = settings.beginReadArray("keys"); + for (int i = 0; i < num; i++) + { + settings.setArrayIndex(i); + sequence().append(settings.value("key", 0).toInt()); + } + settings.endArray(); + + setModifiers(0); + setValid(true); +} + +void KeySequence::saveSettings(QSettings& settings) const +{ + settings.beginWriteArray("keys"); + for (int i = 0; i < sequence().size(); i++) + { + settings.setArrayIndex(i); + settings.setValue("key", sequence()[i]); + } + settings.endArray(); +} + +QString KeySequence::keyToString(int key) +{ + // nothing there? + if (key == 0) + return ""; + + // a hack to handle mouse buttons as if they were keys + if (key < Qt::Key_Space) + { + switch(key) + { + case Qt::LeftButton: return "1"; + case Qt::RightButton: return "2"; + case Qt::MidButton: return "3"; + } + + return "4"; // qt only knows three mouse buttons, so assume it's an unknown fourth one + } + + // modifiers? + if (key & Qt::ShiftModifier) + return "Shift"; + + if (key & Qt::ControlModifier) + return "Control"; + + if (key & Qt::AltModifier) + return "Alt"; + + if (key & Qt::MetaModifier) + return "Meta"; + + // treat key pad like normal keys (FIXME: we should have another lookup table for keypad keys instead) + key &= ~Qt::KeypadModifier; + + // a printable 7 bit character? + if (key < 0x80 && key != Qt::Key_Space) + return QChar(key & 0x7f).toLower(); + + // a function key? + if (key >= Qt::Key_F1 && key <= Qt::Key_F35) + return QString::fromUtf8("F%1").arg(key - Qt::Key_F1 + 1); + + // a special key? + int i=0; + while (keyname[i].name) + { + if (key == keyname[i].key) + return QString::fromUtf8(keyname[i].name); + i++; + } + + // representable in ucs2? + if (key < 0x10000) + return QString("\\u%1").arg(QChar(key).toLower().unicode(), 4, 16, QChar('0')); + + // give up, synergy probably won't handle this + return ""; +} diff --git a/src/gui/src/KeySequence.h b/src/gui/src/KeySequence.h new file mode 100644 index 00000000..a4198615 --- /dev/null +++ b/src/gui/src/KeySequence.h @@ -0,0 +1,57 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(KEYSEQUENCE_H) + +#define KEYSEQUENCE_H + +#include +#include + +class QSettings; + +class KeySequence +{ + public: + KeySequence(); + + public: + QString toString() const; + bool appendKey(int modifiers, int key); + bool appendMouseButton(int button); + bool isMouseButton() const; + bool valid() const { return m_IsValid; } + int modifiers() const { return m_Modifiers; } + void saveSettings(QSettings& settings) const; + void loadSettings(QSettings& settings); + const QList& sequence() const { return m_Sequence; } + + private: + void setValid(bool b) { m_IsValid = b; } + void setModifiers(int i) { m_Modifiers = i; } + QList& sequence() { return m_Sequence; } + + private: + QList m_Sequence; + int m_Modifiers; + bool m_IsValid; + + static QString keyToString(int key); +}; + +#endif + diff --git a/src/gui/src/KeySequenceWidget.cpp b/src/gui/src/KeySequenceWidget.cpp new file mode 100644 index 00000000..ba6a73b6 --- /dev/null +++ b/src/gui/src/KeySequenceWidget.cpp @@ -0,0 +1,143 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "KeySequenceWidget.h" + +#include + +KeySequenceWidget::KeySequenceWidget(QWidget* parent, const KeySequence& seq) : + QPushButton(parent), + m_KeySequence(seq), + m_BackupSequence(seq), + m_Status(Stopped), + m_MousePrefix("mousebutton("), + m_MousePostfix(")"), + m_KeyPrefix("keystroke("), + m_KeyPostfix(")") +{ + setFocusPolicy(Qt::NoFocus); + updateOutput(); +} + +void KeySequenceWidget::setKeySequence(const KeySequence& seq) +{ + keySequence() = seq; + backupSequence() = seq; + + setStatus(Stopped); + updateOutput(); +} + +void KeySequenceWidget::mousePressEvent(QMouseEvent* event) +{ + event->accept(); + + if (status() == Stopped) + { + startRecording(); + return; + } + + if (m_KeySequence.appendMouseButton(event->button())) + stopRecording(); + + updateOutput(); +} + +void KeySequenceWidget::startRecording() +{ + keySequence() = KeySequence(); + setDown(true); + setFocus(); + grabKeyboard(); + setStatus(Recording); +} + +void KeySequenceWidget::stopRecording() +{ + if (!keySequence().valid()) + { + keySequence() = backupSequence(); + updateOutput(); + } + + setDown(false); + focusNextChild(); + releaseKeyboard(); + setStatus(Stopped); + emit keySequenceChanged(); +} + +bool KeySequenceWidget::event(QEvent* event) +{ + if (status() == Recording) + { + switch(event->type()) + { + case QEvent::KeyPress: + keyPressEvent(static_cast(event)); + return true; + + case QEvent::MouseButtonRelease: + event->accept(); + return true; + + case QEvent::ShortcutOverride: + event->accept(); + return true; + + case QEvent::FocusOut: + stopRecording(); + if (!valid()) + { + keySequence() = backupSequence(); + updateOutput(); + } + break; + + default: + break; + } + } + + return QPushButton::event(event); +} + +void KeySequenceWidget::keyPressEvent(QKeyEvent* event) +{ + event->accept(); + + if (status() == Stopped) + return; + + if (m_KeySequence.appendKey(event->key(), event->modifiers())) + stopRecording(); + + updateOutput(); +} + +void KeySequenceWidget::updateOutput() +{ + QString s; + + if (m_KeySequence.isMouseButton()) + s = mousePrefix() + m_KeySequence.toString() + mousePostfix(); + else + s = keyPrefix() + m_KeySequence.toString() + keyPostfix(); + + setText(s); +} diff --git a/src/gui/src/KeySequenceWidget.h b/src/gui/src/KeySequenceWidget.h new file mode 100644 index 00000000..c4599523 --- /dev/null +++ b/src/gui/src/KeySequenceWidget.h @@ -0,0 +1,80 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(KEYSEQUENCEWIDGET__H) + +#define KEYSEQUENCEWIDGET__H + +#include + +#include "KeySequence.h" + +class KeySequenceWidget : public QPushButton +{ + Q_OBJECT + + public: + KeySequenceWidget(QWidget* parent, const KeySequence& seq = KeySequence()); + + signals: + void keySequenceChanged(); + + public: + const QString& mousePrefix() const { return m_MousePrefix; } + const QString& mousePostfix() const { return m_MousePostfix; } + const QString& keyPrefix() const { return m_KeyPrefix; } + const QString& keyPostfix() const { return m_KeyPostfix; } + + void setMousePrefix(const QString& s) { m_MousePrefix = s; } + void setMousePostfix(const QString& s) { m_MousePostfix = s; } + void setKeyPrefix(const QString& s) { m_KeyPrefix = s; } + void setKeyPostfix(const QString& s) { m_KeyPostfix = s; } + + const KeySequence& keySequence() const { return m_KeySequence; } + const KeySequence& backupSequence() const { return m_BackupSequence; } + void setKeySequence(const KeySequence& seq); + + bool valid() const { return keySequence().valid(); } + + protected: + void mousePressEvent(QMouseEvent*); + void keyPressEvent(QKeyEvent*); + bool event(QEvent* event); + void appendToSequence(int key); + void updateOutput(); + void startRecording(); + void stopRecording(); + KeySequence& keySequence() { return m_KeySequence; } + KeySequence& backupSequence() { return m_BackupSequence; } + + private: + enum Status { Stopped, Recording }; + void setStatus(Status s) { m_Status = s; } + Status status() const { return m_Status; } + + private: + KeySequence m_KeySequence; + KeySequence m_BackupSequence; + Status m_Status; + QString m_MousePrefix; + QString m_MousePostfix; + QString m_KeyPrefix; + QString m_KeyPostfix; +}; + +#endif + diff --git a/src/gui/src/MainWindow.cpp b/src/gui/src/MainWindow.cpp new file mode 100644 index 00000000..3c12a142 --- /dev/null +++ b/src/gui/src/MainWindow.cpp @@ -0,0 +1,722 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#define WEBSITE_ADDRESS "synergy-foss.org" + +#include "MainWindow.h" +#include "AboutDialog.h" +#include "ServerConfigDialog.h" +#include "SettingsDialog.h" +#include "SetupWizard.h" + +#include +#include +#include +#include + +#if defined(Q_OS_MAC) +#include +#endif + +#if defined(Q_OS_WIN) +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#if defined(Q_OS_WIN) +static const char synergyConfigName[] = "synergy.sgc"; +static const QString synergyConfigFilter(QObject::tr("Synergy Configurations (*.sgc);;All files (*.*)")); +#else +static const char synergyConfigName[] = "synergy.conf"; +static const QString synergyConfigFilter(QObject::tr("Synergy Configurations (*.conf);;All files (*.*)")); +#endif + +static const char* synergyIconFiles[] = +{ + ":/res/icons/16x16/synergy-disconnected.png", + ":/res/icons/16x16/synergy-disconnected.png", + ":/res/icons/16x16/synergy-connected.png" +}; + +class QThreadImpl : public QThread +{ +public: + static void msleep(unsigned long msecs) + { + QThread::msleep(msecs); + } +}; + +MainWindow::MainWindow(QSettings& settings, AppConfig& appConfig) : + m_Settings(settings), + m_AppConfig(appConfig), + m_pSynergy(NULL), + m_SynergyState(synergyDisconnected), + m_ServerConfig(&m_Settings, 5, 3), + m_pTempConfigFile(NULL), + m_pTrayIcon(NULL), + m_pTrayIconMenu(NULL), + m_alreadyHidden(false), + m_SetupWizard(NULL) +{ + setupUi(this); + + createMenuBar(); + loadSettings(); + initConnections(); + + m_pUpdateIcon->hide(); + m_pUpdateLabel->hide(); + m_versionChecker.setApp(appPath(appConfig.synergycName())); + + m_SetupWizard = new SetupWizard(*this, false); + connect(m_SetupWizard, SIGNAL(finished(int)), this, SLOT(refreshStartButton())); + + if (appConfig.processMode() == Service) + { + connect(&m_IpcLogReader, SIGNAL(receivedLine(const QString&)), this, SLOT(appendLog(const QString&))); + m_IpcLogReader.start(); + } +} + +MainWindow::~MainWindow() +{ + stopSynergy(); + saveSettings(); + delete m_SetupWizard; +} + +void MainWindow::start(bool firstRun) +{ + refreshStartButton(); + + if (!firstRun && appConfig().autoConnect() && appConfig().processMode() == Desktop) + startSynergy(); + + createTrayIcon(); + + // always show. auto-hide only happens when we have a connection. + show(); + + m_versionChecker.checkLatest(); +} + +void MainWindow::refreshStartButton() +{ + if (appConfig().processMode() == Service) + { + m_pButtonToggleStart->setText(tr("&Apply")); + } + else + { + m_pButtonToggleStart->setText(tr("&Start")); + } +} + +void MainWindow::setStatus(const QString &status) +{ + m_pStatusLabel->setText(status); +} + +void MainWindow::createTrayIcon() +{ + m_pTrayIconMenu = new QMenu(this); + + m_pTrayIconMenu->addAction(m_pActionStartSynergy); + m_pTrayIconMenu->addAction(m_pActionStopSynergy); + m_pTrayIconMenu->addSeparator(); + + m_pTrayIconMenu->addAction(m_pActionMinimize); + m_pTrayIconMenu->addAction(m_pActionRestore); + m_pTrayIconMenu->addSeparator(); + m_pTrayIconMenu->addAction(m_pActionQuit); + + m_pTrayIcon = new QSystemTrayIcon(this); + m_pTrayIcon->setContextMenu(m_pTrayIconMenu); + + setIcon(synergyDisconnected); + + m_pTrayIcon->show(); +} + +void MainWindow::createMenuBar() +{ + QMenuBar* menubar = new QMenuBar(this); + QMenu* pMenuFile = new QMenu(tr("&File"), menubar); + QMenu* pMenuEdit = new QMenu(tr("&Edit"), menubar); + QMenu* pMenuWindow = new QMenu(tr("&Window"), menubar); + QMenu* pMenuHelp = new QMenu(tr("&Help"), menubar); + + menubar->addAction(pMenuFile->menuAction()); + menubar->addAction(pMenuEdit->menuAction()); +#if !defined(Q_OS_MAC) + menubar->addAction(pMenuWindow->menuAction()); +#endif + menubar->addAction(pMenuHelp->menuAction()); + + pMenuFile->addAction(m_pActionStartSynergy); + pMenuFile->addAction(m_pActionStopSynergy); + pMenuFile->addSeparator(); + pMenuFile->addAction(m_pActionWizard); + pMenuFile->addAction(m_pActionSave); + pMenuFile->addSeparator(); + pMenuFile->addAction(m_pActionQuit); + pMenuEdit->addAction(m_pActionSettings); + pMenuWindow->addAction(m_pActionMinimize); + pMenuWindow->addAction(m_pActionRestore); + pMenuHelp->addAction(m_pActionAbout); + + setMenuBar(menubar); +} + +void MainWindow::loadSettings() +{ + // the next two must come BEFORE loading groupServerChecked and groupClientChecked or + // disabling and/or enabling the right widgets won't automatically work + m_pRadioExternalConfig->setChecked(settings().value("externalConfig", false).toBool()); + m_pRadioInternalConfig->setChecked(settings().value("internalConfig", true).toBool()); + + m_pGroupServer->setChecked(settings().value("groupServerChecked", false).toBool()); + m_pLineEditConfigFile->setText(settings().value("configFile", QDir::homePath() + "/" + synergyConfigName).toString()); + m_pGroupClient->setChecked(settings().value("groupClientChecked", true).toBool()); + m_pLineEditHostname->setText(settings().value("serverHostname").toString()); +} + +void MainWindow::initConnections() +{ + connect(m_pActionMinimize, SIGNAL(triggered()), this, SLOT(hide())); + connect(m_pActionRestore, SIGNAL(triggered()), this, SLOT(showNormal())); + connect(m_pActionStartSynergy, SIGNAL(triggered()), this, SLOT(startSynergy())); + connect(m_pActionStopSynergy, SIGNAL(triggered()), this, SLOT(stopSynergy())); + connect(m_pActionQuit, SIGNAL(triggered()), qApp, SLOT(quit())); + connect(&m_versionChecker, SIGNAL(updateFound(const QString&)), this, SLOT(updateFound(const QString&))); + + if (m_pTrayIcon) + connect(m_pTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(iconActivated(QSystemTrayIcon::ActivationReason))); +} + +void MainWindow::saveSettings() +{ + // program settings + settings().setValue("groupServerChecked", m_pGroupServer->isChecked()); + settings().setValue("externalConfig", m_pRadioExternalConfig->isChecked()); + settings().setValue("configFile", m_pLineEditConfigFile->text()); + settings().setValue("internalConfig", m_pRadioInternalConfig->isChecked()); + settings().setValue("groupClientChecked", m_pGroupClient->isChecked()); + settings().setValue("serverHostname", m_pLineEditHostname->text()); + + settings().sync(); +} + +void MainWindow::setIcon(qSynergyState state) +{ + QIcon icon; + icon.addFile(synergyIconFiles[state]); + + if (m_pTrayIcon) + m_pTrayIcon->setIcon(icon); +} + +void MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason) +{ + if (reason == QSystemTrayIcon::DoubleClick) + { + if (isVisible()) + { + hide(); + } + else + { + showNormal(); + activateWindow(); + } + } +} + +void MainWindow::logOutput() +{ + if (m_pSynergy) + { + QString text(m_pSynergy->readAllStandardOutput()); + foreach(QString line, text.split(QRegExp("\r|\n|\r\n"))) + { + if (!line.isEmpty()) + { + appendLog(line); + if (line.contains("has connected") || + line.contains("connected to server")) + { + // only set connected state and hide, if we get + // "has connected" message. this is a little bit + // hacky, but it works for now (until we have IPC). + setSynergyState(synergyConnected); + + // only hide once after each new connection. + if (!m_alreadyHidden && appConfig().autoHide()) + { + hide(); + m_alreadyHidden = true; + } + } + } + } + } +} + +void MainWindow::logError() +{ + if (m_pSynergy) + { + appendLog(m_pSynergy->readAllStandardError()); + } +} + +void MainWindow::updateFound(const QString &version) +{ + m_pUpdateIcon->show(); + m_pUpdateLabel->show(); + m_pUpdateLabel->setText( + tr("

Version %1 is now available, visit website.

") + .arg(version).arg("http://synergy-foss.org")); +} + +void MainWindow::appendLog(const QString& text) +{ + foreach(QString line, text.split(QRegExp("\r|\n|\r\n"))) + if (!line.isEmpty()) + m_pLogOutput->append(line); +} + +void MainWindow::clearLog() +{ + m_pLogOutput->clear(); +} + +void MainWindow::startSynergy() +{ + // TODO: refactor this out into 2 methods. + bool desktopMode = appConfig().processMode() == Desktop; + bool serviceMode = appConfig().processMode() == Service; + + if (desktopMode) + { + // cause the service to stop creating processes. + sendDaemonCommand("", false); + + stopSynergy(); + setSynergyState(synergyConnecting); + } + + QString app; + QStringList args; + + args << "-f" << "--no-tray" << "--debug" << appConfig().logLevelText(); + + if (!appConfig().screenName().isEmpty()) + args << "--name" << appConfig().screenName(); + + if (appConfig().gameModeIndex() != 0) + { + if (appConfig().gameModeIndex() == 1) + { + args << "--game-mode" << "xinput"; + } + else if (appConfig().gameModeIndex() == 2) + { + args << "--game-mode" << "joyinfoex"; + } + + if (appConfig().gamePollingDynamic()) + { + args << "--game-poll" << "dynamic"; + } + else + { + args << "--game-poll" << "static"; + args << "--game-poll-freq" << QString::number(appConfig().gamePollingFrequency()); + } + } + + if (desktopMode) + { + setSynergyProcess(new QProcess(this)); + } + + if ((synergyType() == synergyClient && !clientArgs(args, app)) + || (synergyType() == synergyServer && !serverArgs(args, app))) + { + if (desktopMode) + { + stopSynergy(); + return; + } + } + + if (desktopMode) + { + connect(synergyProcess(), SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(synergyFinished(int, QProcess::ExitStatus))); + connect(synergyProcess(), SIGNAL(readyReadStandardOutput()), this, SLOT(logOutput())); + connect(synergyProcess(), SIGNAL(readyReadStandardError()), this, SLOT(logError())); + } + + // put a space between last log output and new instance. + if (!m_pLogOutput->toPlainText().isEmpty()) + appendLog(""); + + if (desktopMode) + { + appendLog("starting " + QString(synergyType() == synergyServer ? "server" : "client")); + } + + if (serviceMode) + { + appendLog("applying service mode: " + QString(synergyType() == synergyServer ? "server" : "client")); + } + + // show command if debug log level... + if (appConfig().logLevel() >= 4) { + appendLog(QString("command: %1 %2").arg(app, args.join(" "))); + } + + appendLog("config file: " + configFilename()); + appendLog("log level: " + appConfig().logLevelText()); + + if (appConfig().logToFile()) + appendLog("log file: " + appConfig().logFilename()); + + if (desktopMode) + { + synergyProcess()->start(app, args); + if (!synergyProcess()->waitForStarted()) + { + stopSynergy(); + show(); + QMessageBox::warning(this, tr("Program can not be started"), QString(tr("The executable

%1

could not be successfully started, although it does exist. Please check if you have sufficient permissions to run this program.").arg(app))); + return; + } + } + + if (serviceMode) + { + QString command(app + " " + args.join(" ")); + sendDaemonCommand(command, true); + } +} + +bool MainWindow::clientArgs(QStringList& args, QString& app) +{ + app = appPath(appConfig().synergycName()); + + if (!QFile::exists(app)) + { + show(); + QMessageBox::warning(this, tr("Synergy client not found"), + tr("The executable for the synergy client does not exist.")); + return false; + } + + if (m_pLineEditHostname->text().isEmpty()) + { + show(); + QMessageBox::warning(this, tr("Hostname is empty"), + tr("Please fill in a hostname for the synergy client to connect to.")); + return false; + } + + if (appConfig().logToFile()) + { + appConfig().persistLogDir(); + args << "--log" << appConfig().logFilename(); + } + + args << m_pLineEditHostname->text() + ":" + QString::number(appConfig().port()); + + return true; +} + +QString MainWindow::configFilename() +{ + QString filename; + if (m_pRadioInternalConfig->isChecked()) + { + // TODO: no need to use a temporary file, since we need it to + // be permenant (since it'll be used for Windows services, etc). + m_pTempConfigFile = new QTemporaryFile(); + if (!m_pTempConfigFile->open()) + { + QMessageBox::critical(this, tr("Cannot write configuration file"), tr("The temporary configuration file required to start synergy can not be written.")); + return false; + } + + serverConfig().save(*m_pTempConfigFile); + filename = m_pTempConfigFile->fileName(); + + m_pTempConfigFile->close(); + } + else + { + if (!QFile::exists(m_pLineEditConfigFile->text())) + { + if (QMessageBox::warning(this, tr("Configuration filename invalid"), + tr("You have not filled in a valid configuration file for the synergy server. " + "Do you want to browse for the configuration file now?"), QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes + || !on_m_pButtonBrowseConfigFile_clicked()) + return false; + } + + filename = m_pLineEditConfigFile->text(); + } + return filename; +} + +QString MainWindow::address() +{ + return (!appConfig().interface().isEmpty() ? appConfig().interface() : "") + ":" + QString::number(appConfig().port()); +} + +QString MainWindow::appPath(const QString& name) +{ + return appConfig().synergyProgramDir() + name; +} + +bool MainWindow::serverArgs(QStringList& args, QString& app) +{ + app = appPath(appConfig().synergysName()); + + if (!QFile::exists(app)) + { + QMessageBox::warning(this, tr("Synergy server not found"), + tr("The executable for the synergy server does not exist.")); + return false; + } + + if (appConfig().logToFile()) + { + appConfig().persistLogDir(); + args << "--log" << appConfig().logFilename(); + } + + args << "-c" << configFilename() << "--address" << address(); + + return true; +} + +void MainWindow::stopSynergy() +{ + if (synergyProcess()) + { + appendLog("stopping synergy"); + + if (synergyProcess()->isOpen()) + synergyProcess()->close(); + delete synergyProcess(); + setSynergyProcess(NULL); + + setSynergyState(synergyDisconnected); + } + + // HACK: deleting the object deletes the physical file, which is + // bad, since it could be in use by the Windows service! + //delete m_pTempConfigFile; + m_pTempConfigFile = NULL; + + // reset so that new connects cause auto-hide. + m_alreadyHidden = false; +} + +void MainWindow::synergyFinished(int exitCode, QProcess::ExitStatus) +{ + // on Windows, we always seem to have an exit code != 0. +#if !defined(Q_OS_WIN) + if (exitCode != 0) + { + QMessageBox::critical(this, tr("Synergy terminated with an error"), QString(tr("Synergy terminated unexpectedly with an exit code of %1.

Please see the log output for details.")).arg(exitCode)); + stopSynergy(); + } +#else + Q_UNUSED(exitCode); +#endif + + setSynergyState(synergyDisconnected); + + // do not call stopSynergy() in case of clean synergy shutdown, because this must have (well, should have...) + // come from our own delete synergyProcess() in stopSynergy(), so we would do a double-delete... +} + +void MainWindow::setSynergyState(qSynergyState state) +{ + // ignore state stuff when in service mode (for now anyway). + if (appConfig().processMode() == Service) + return; + + if (synergyState() == state) + return; + + if (state == synergyConnected || state == synergyConnecting) + { + disconnect (m_pButtonToggleStart, SIGNAL(clicked()), m_pActionStartSynergy, SLOT(trigger())); + connect (m_pButtonToggleStart, SIGNAL(clicked()), m_pActionStopSynergy, SLOT(trigger())); + m_pButtonToggleStart->setText(tr("&Stop")); + } + else + { + disconnect (m_pButtonToggleStart, SIGNAL(clicked()), m_pActionStopSynergy, SLOT(trigger())); + connect (m_pButtonToggleStart, SIGNAL(clicked()), m_pActionStartSynergy, SLOT(trigger())); + m_pButtonToggleStart->setText(tr("&Start")); + } + + m_pGroupClient->setEnabled(state == synergyDisconnected); + m_pGroupServer->setEnabled(state == synergyDisconnected); + m_pActionStartSynergy->setEnabled(state == synergyDisconnected); + m_pActionStopSynergy->setEnabled(state == synergyConnected); + + switch (state) + { + case synergyConnected: + setStatus(tr("Synergy is running.")); + break; + case synergyConnecting: + setStatus(tr("Synergy is starting.")); + break; + case synergyDisconnected: + setStatus(tr("Synergy is not running.")); + break; + } + + setIcon(state); + + m_SynergyState = state; +} + +void MainWindow::setVisible(bool visible) +{ + m_pActionMinimize->setEnabled(visible); + m_pActionRestore->setEnabled(!visible); + QMainWindow::setVisible(visible); + +#if MAC_OS_X_VERSION_10_7 + // dock hide only supported on lion :( + ProcessSerialNumber psn = { 0, kCurrentProcess }; + GetCurrentProcess(&psn); + if (visible) + TransformProcessType(&psn, kProcessTransformToForegroundApplication); + else + TransformProcessType(&psn, kProcessTransformToBackgroundApplication); +#endif +} + +bool MainWindow::on_m_pButtonBrowseConfigFile_clicked() +{ + QString fileName = QFileDialog::getOpenFileName(this, tr("Browse for a synergys config file"), QString(), synergyConfigFilter); + + if (!fileName.isEmpty()) + { + m_pLineEditConfigFile->setText(fileName); + return true; + } + + return false; +} + +bool MainWindow::on_m_pActionSave_triggered() +{ + QString fileName = QFileDialog::getSaveFileName(this, tr("Save configuration as...")); + + if (!fileName.isEmpty() && !serverConfig().save(fileName)) + { + QMessageBox::warning(this, tr("Save failed"), tr("Could not save configuration to file.")); + return true; + } + + return false; +} + +void MainWindow::on_m_pActionAbout_triggered() +{ + AboutDialog dlg(this, appPath(appConfig().synergycName())); + dlg.exec(); +} + +void MainWindow::on_m_pActionSettings_triggered() +{ + SettingsDialog dlg(this, appConfig()); + dlg.exec(); +} + +void MainWindow::on_m_pButtonConfigureServer_clicked() +{ + ServerConfigDialog dlg(this, serverConfig(), appConfig().screenName()); + dlg.exec(); +} + +void MainWindow::sendDaemonCommand(const QString& command, bool showErrors) +{ + sendIpcMessage(Command, command.toStdString().c_str(), showErrors); +} + +// TODO: put this in an IPC client class. +void MainWindow::sendIpcMessage(qIpcMessage type, const char* data, bool showErrors) +{ +#if defined(Q_OS_WIN) + + const WCHAR* name = L"\\\\.\\pipe\\Synergy"; + char message[1024]; + message[0] = type; + char* messagePtr = message; + messagePtr++; + strcpy(messagePtr, data); + + HANDLE pipe = CreateFile( + name, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); + + if (showErrors && pipe == INVALID_HANDLE_VALUE) + { + appendLog(QString("ERROR: could not connect to service, error: ") + + QString::number(GetLastError())); + return; + } + + DWORD dwMode = PIPE_READMODE_MESSAGE; + BOOL stateSuccess = SetNamedPipeHandleState(pipe, &dwMode, NULL, NULL); + + if (showErrors && !stateSuccess) + { + appendLog(QString("ERROR: could not set service pipe state, error: ") + + QString::number(GetLastError())); + return; + } + + DWORD written; + BOOL writeSuccess = WriteFile( + pipe, message, strlen(message), &written, NULL); + + if (showErrors && !writeSuccess) + { + appendLog(QString("ERROR: could not write to service pipe, error: ") + + QString::number(GetLastError())); + return; + } + + CloseHandle(pipe); + +#endif +} + +void MainWindow::on_m_pActionWizard_triggered() +{ + m_SetupWizard->show(); +} diff --git a/src/gui/src/MainWindow.h b/src/gui/src/MainWindow.h new file mode 100644 index 00000000..d0dfb9b7 --- /dev/null +++ b/src/gui/src/MainWindow.h @@ -0,0 +1,149 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(MAINWINDOW__H) + +#define MAINWINDOW__H + +#include +#include +#include +#include +#include + +#include "ui_MainWindowBase.h" + +#include "ServerConfig.h" +#include "AppConfig.h" +#include "VersionChecker.h" +#include "IpcLogReader.h" + +class QAction; +class QMenu; +class QLineEdit; +class QGroupBox; +class QPushButton; +class QTextEdit; +class QComboBox; +class QTabWidget; +class QCheckBox; +class QRadioButton; +class QTemporaryFile; + +class LogDialog; +class QSynergyApplication; +class SetupWizard; + +class MainWindow : public QMainWindow, public Ui::MainWindowBase +{ + Q_OBJECT + + friend class QSynergyApplication; + friend class SetupWizard; + + public: + enum qSynergyState + { + synergyDisconnected, + synergyConnecting, + synergyConnected + }; + + enum qSynergyType + { + synergyClient, + synergyServer + }; + + enum qIpcMessage { + Command = 1 + }; + + public: + MainWindow(QSettings& settings, AppConfig& appConfig); + ~MainWindow(); + + public: + void setVisible(bool visible); + int synergyType() const { return m_pGroupClient->isChecked() ? synergyClient : synergyServer; } + int synergyState() const { return m_SynergyState; } + QString hostname() const { return m_pLineEditHostname->text(); } + QString configFilename(); + QString address(); + QString appPath(const QString& name); + void start(bool firstRun); + void clearLog(); + + public slots: + void appendLog(const QString& text); + + protected slots: + void on_m_pGroupClient_toggled(bool on) { m_pGroupServer->setChecked(!on); } + void on_m_pGroupServer_toggled(bool on) { m_pGroupClient->setChecked(!on); } + bool on_m_pButtonBrowseConfigFile_clicked(); + void on_m_pButtonConfigureServer_clicked(); + bool on_m_pActionSave_triggered(); + void on_m_pActionAbout_triggered(); + void on_m_pActionSettings_triggered(); + void on_m_pActionWizard_triggered(); + void synergyFinished(int exitCode, QProcess::ExitStatus); + void iconActivated(QSystemTrayIcon::ActivationReason reason); + void startSynergy(); + void stopSynergy(); + void logOutput(); + void logError(); + void updateFound(const QString& version); + void refreshStartButton(); + + protected: + QSettings& settings() { return m_Settings; } + AppConfig& appConfig() { return m_AppConfig; } + QProcess*& synergyProcess() { return m_pSynergy; } + void setSynergyProcess(QProcess* p) { m_pSynergy = p; } + ServerConfig& serverConfig() { return m_ServerConfig; } + void initConnections(); + void createMenuBar(); + void createStatusBar(); + void createTrayIcon(); + void loadSettings(); + void saveSettings(); + void setIcon(qSynergyState state); + void setSynergyState(qSynergyState state); + bool checkForApp(int which, QString& app); + bool clientArgs(QStringList& args, QString& app); + bool serverArgs(QStringList& args, QString& app); + void setStatus(const QString& status); + void sendDaemonCommand(const QString& command, bool showErrors); + void sendIpcMessage(qIpcMessage type, const char* buffer, bool showErrors); + + private: + QSettings& m_Settings; + AppConfig& m_AppConfig; + QProcess* m_pSynergy; + int m_SynergyState; + ServerConfig m_ServerConfig; + QTemporaryFile* m_pTempConfigFile; + QSystemTrayIcon* m_pTrayIcon; + QMenu* m_pTrayIconMenu; + bool m_alreadyHidden; + VersionChecker m_versionChecker; + SetupWizard* m_SetupWizard; + IpcLogReader m_IpcLogReader; +}; + +#endif + diff --git a/src/gui/src/NewScreenWidget.cpp b/src/gui/src/NewScreenWidget.cpp new file mode 100644 index 00000000..f3e4b25e --- /dev/null +++ b/src/gui/src/NewScreenWidget.cpp @@ -0,0 +1,47 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "NewScreenWidget.h" +#include "ScreenSetupModel.h" + +#include +#include + +NewScreenWidget::NewScreenWidget(QWidget* parent) : + QLabel(parent) +{ +} + +void NewScreenWidget::mousePressEvent(QMouseEvent* event) +{ + Screen newScreen(tr("Unnamed")); + + QByteArray itemData; + QDataStream dataStream(&itemData, QIODevice::WriteOnly); + dataStream << -1 << -1 << newScreen; + + QMimeData* pMimeData = new QMimeData; + pMimeData->setData(ScreenSetupModel::mimeType(), itemData); + + QDrag* pDrag = new QDrag(this); + pDrag->setMimeData(pMimeData); + pDrag->setPixmap(*pixmap()); + pDrag->setHotSpot(event->pos()); + + pDrag->exec(Qt::CopyAction, Qt::CopyAction); +} + diff --git a/src/gui/src/NewScreenWidget.h b/src/gui/src/NewScreenWidget.h new file mode 100644 index 00000000..a26fceb3 --- /dev/null +++ b/src/gui/src/NewScreenWidget.h @@ -0,0 +1,39 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(NEWSCREENWIDGET__H) + +#define NEWSCREENWIDGET__H + +#include + +class QMouseEvent; +class QWidget; + +class NewScreenWidget : public QLabel +{ + Q_OBJECT + + public: + NewScreenWidget(QWidget* parent); + + protected: + void mousePressEvent(QMouseEvent* event); +}; + +#endif + diff --git a/src/gui/src/QSynergyApplication.cpp b/src/gui/src/QSynergyApplication.cpp new file mode 100644 index 00000000..c0b0dcf8 --- /dev/null +++ b/src/gui/src/QSynergyApplication.cpp @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "QSynergyApplication.h" +#include "MainWindow.h" + +#include +#include + +QSynergyApplication::QSynergyApplication(int& argc, char** argv) : + QApplication(argc, argv) +{ +} + +void QSynergyApplication::commitData(QSessionManager&) +{ + foreach(QWidget* widget, topLevelWidgets()) + { + MainWindow* mainWindow = qobject_cast(widget); + if (mainWindow) + mainWindow->saveSettings(); + } +} + diff --git a/src/gui/src/QSynergyApplication.h b/src/gui/src/QSynergyApplication.h new file mode 100644 index 00000000..a6e9223f --- /dev/null +++ b/src/gui/src/QSynergyApplication.h @@ -0,0 +1,36 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(QSYNERGYAPPLICATION__H) + +#define QSYNERGYAPPLICATION__H + +#include + +class QSessionManager; + +class QSynergyApplication : public QApplication +{ + public: + QSynergyApplication(int& argc, char** argv); + + public: + void commitData(QSessionManager& manager); +}; + +#endif + diff --git a/src/gui/src/Screen.cpp b/src/gui/src/Screen.cpp new file mode 100644 index 00000000..b35b2eee --- /dev/null +++ b/src/gui/src/Screen.cpp @@ -0,0 +1,146 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "Screen.h" + +#include +#include + +Screen::Screen() : + m_Pixmap(QPixmap(":res/icons/64x64/video-display.png")), + m_Swapped(false) +{ + init(); +} + +Screen::Screen(const QString& name) : + m_Pixmap(QPixmap(":res/icons/64x64/video-display.png")), + m_Swapped(false) +{ + init(); + setName(name); +} + +void Screen::init() +{ + name().clear(); + aliases().clear(); + modifiers().clear(); + switchCorners().clear(); + fixes().clear(); + setSwitchCornerSize(0); + + // m_Modifiers, m_SwitchCorners and m_Fixes are QLists we use like fixed-size arrays, + // thus we need to make sure to fill them with the required number of elements. + for (int i = 0; i < NumModifiers; i++) + modifiers() << i; + + for (int i = 0; i < NumSwitchCorners; i++) + switchCorners() << false; + + for (int i = 0; i < NumFixes; i++) + fixes() << false; +} + +void Screen::loadSettings(QSettings& settings) +{ + setName(settings.value("name").toString()); + + if (name().isEmpty()) + return; + + setSwitchCornerSize(settings.value("switchCornerSize").toInt()); + + readSettings(settings, aliases(), "alias", QString("")); + readSettings(settings, modifiers(), "modifier", static_cast(DefaultMod), NumModifiers); + readSettings(settings, switchCorners(), "switchCorner", false, NumSwitchCorners); + readSettings(settings, fixes(), "fix", false, NumFixes); +} + +void Screen::saveSettings(QSettings& settings) const +{ + settings.setValue("name", name()); + + if (name().isEmpty()) + return; + + settings.setValue("switchCornerSize", switchCornerSize()); + + writeSettings(settings, aliases(), "alias"); + writeSettings(settings, modifiers(), "modifier"); + writeSettings(settings, switchCorners(), "switchCorner"); + writeSettings(settings, fixes(), "fix"); +} + +QTextStream& Screen::writeScreensSection(QTextStream& outStream) const +{ + outStream << "\t" << name() << ":" << endl; + + for (int i = 0; i < modifiers().size(); i++) + if (modifier(i) != i) + outStream << "\t\t" << modifierName(i) << " = " << modifierName(modifier(i)) << endl; + + for (int i = 0; i < fixes().size(); i++) + outStream << "\t\t" << fixName(i) << " = " << (fixes()[i] ? "true" : "false") << endl; + + outStream << "\t\t" << "switchCorners = none "; + for (int i = 0; i < switchCorners().size(); i++) + if (switchCorners()[i]) + outStream << "+" << switchCornerName(i) << " "; + outStream << endl; + + outStream << "\t\t" << "switchCornerSize = " << switchCornerSize() << endl; + + return outStream; +} + +QTextStream& Screen::writeAliasesSection(QTextStream& outStream) const +{ + if (!aliases().isEmpty()) + { + outStream << "\t" << name() << ":" << endl; + + foreach (const QString& alias, aliases()) + outStream << "\t\t" << alias << endl; + } + + return outStream; +} + +QDataStream& operator<<(QDataStream& outStream, const Screen& screen) +{ + return outStream + << screen.name() + << screen.switchCornerSize() + << screen.aliases() + << screen.modifiers() + << screen.switchCorners() + << screen.fixes() + ; +} + +QDataStream& operator>>(QDataStream& inStream, Screen& screen) +{ + return inStream + >> screen.m_Name + >> screen.m_SwitchCornerSize + >> screen.m_Aliases + >> screen.m_Modifiers + >> screen.m_SwitchCorners + >> screen.m_Fixes + ; +} diff --git a/src/gui/src/Screen.h b/src/gui/src/Screen.h new file mode 100644 index 00000000..63771281 --- /dev/null +++ b/src/gui/src/Screen.h @@ -0,0 +1,104 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(SCREEN__H) + +#define SCREEN__H + +#include +#include +#include +#include + +#include "BaseConfig.h" + +class QSettings; +class QTextStream; + +class ScreenSettingsDialog; + +class Screen : public BaseConfig +{ + friend QDataStream& operator<<(QDataStream& outStream, const Screen& screen); + friend QDataStream& operator>>(QDataStream& inStream, Screen& screen); + friend class ScreenSettingsDialog; + friend class ScreenSetupModel; + friend class ScreenSetupView; + + public: + Screen(); + Screen(const QString& name); + + public: + const QPixmap* pixmap() const { return &m_Pixmap; } + const QString& name() const { return m_Name; } + const QStringList& aliases() const { return m_Aliases; } + + bool isNull() const { return m_Name.isEmpty(); } + int modifier(int m) const { return m_Modifiers[m] == DefaultMod ? m : m_Modifiers[m]; } + const QList& modifiers() const { return m_Modifiers; } + bool switchCorner(int c) const { return m_SwitchCorners[c]; } + const QList& switchCorners() const { return m_SwitchCorners; } + int switchCornerSize() const { return m_SwitchCornerSize; } + bool fix(Fix f) const { return m_Fixes[f]; } + const QList& fixes() const { return m_Fixes; } + + void loadSettings(QSettings& settings); + void saveSettings(QSettings& settings) const; + QTextStream& writeScreensSection(QTextStream& outStream) const; + QTextStream& writeAliasesSection(QTextStream& outStream) const; + + bool swapped() const { return m_Swapped; } + + protected: + void init(); + void setName(const QString& name) { m_Name = name; } + QPixmap* pixmap() { return &m_Pixmap; } + QString& name() { return m_Name; } + + void setPixmap(const QPixmap& pixmap) { m_Pixmap = pixmap; } + QStringList& aliases() { return m_Aliases; } + void setModifier(int m, int n) { m_Modifiers[m] = n; } + QList& modifiers() { return m_Modifiers; } + void addAlias(const QString& alias) { m_Aliases.append(alias); } + void setSwitchCorner(int c, bool on) { m_SwitchCorners[c] = on; } + QList& switchCorners() { return m_SwitchCorners; } + void setSwitchCornerSize(int val) { m_SwitchCornerSize = val; } + void setFix(int f, bool on) { m_Fixes[f] = on; } + QList& fixes() { return m_Fixes; } + void setSwapped(bool on) { m_Swapped = on; } + + private: + QPixmap m_Pixmap; + QString m_Name; + + QStringList m_Aliases; + QList m_Modifiers; + QList m_SwitchCorners; + int m_SwitchCornerSize; + QList m_Fixes; + + bool m_Swapped; +}; + +typedef QList ScreenList; + +QDataStream& operator<<(QDataStream& outStream, const Screen& screen); +QDataStream& operator>>(QDataStream& inStream, Screen& screen); + +#endif + diff --git a/src/gui/src/ScreenSettingsDialog.cpp b/src/gui/src/ScreenSettingsDialog.cpp new file mode 100644 index 00000000..96276e89 --- /dev/null +++ b/src/gui/src/ScreenSettingsDialog.cpp @@ -0,0 +1,121 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "ScreenSettingsDialog.h" +#include "Screen.h" + +#include +#include + +ScreenSettingsDialog::ScreenSettingsDialog(QWidget* parent, Screen* pScreen) : + QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint), + Ui::ScreenSettingsDialogBase(), + m_pScreen(pScreen) +{ + setupUi(this); + + QRegExp validScreenName("[a-z_][a-z0-9\\._-]{,31}", Qt::CaseInsensitive); + + m_pLineEditName->setText(m_pScreen->name()); + m_pLineEditName->setValidator(new QRegExpValidator(validScreenName, m_pLineEditName)); + m_pLineEditName->selectAll(); + + m_pLineEditAlias->setValidator(new QRegExpValidator(validScreenName, m_pLineEditName)); + + for (int i = 0; i < m_pScreen->aliases().count(); i++) + new QListWidgetItem(m_pScreen->aliases()[i], m_pListAliases); + + m_pComboBoxShift->setCurrentIndex(m_pScreen->modifier(Screen::Shift)); + m_pComboBoxCtrl->setCurrentIndex(m_pScreen->modifier(Screen::Ctrl)); + m_pComboBoxAlt->setCurrentIndex(m_pScreen->modifier(Screen::Alt)); + m_pComboBoxMeta->setCurrentIndex(m_pScreen->modifier(Screen::Meta)); + m_pComboBoxSuper->setCurrentIndex(m_pScreen->modifier(Screen::Super)); + + m_pCheckBoxCornerTopLeft->setChecked(m_pScreen->switchCorner(Screen::TopLeft)); + m_pCheckBoxCornerTopRight->setChecked(m_pScreen->switchCorner(Screen::TopRight)); + m_pCheckBoxCornerBottomLeft->setChecked(m_pScreen->switchCorner(Screen::BottomLeft)); + m_pCheckBoxCornerBottomRight->setChecked(m_pScreen->switchCorner(Screen::BottomRight)); + m_pSpinBoxSwitchCornerSize->setValue(m_pScreen->switchCornerSize()); + + m_pCheckBoxCapsLock->setChecked(m_pScreen->fix(Screen::CapsLock)); + m_pCheckBoxNumLock->setChecked(m_pScreen->fix(Screen::NumLock)); + m_pCheckBoxScrollLock->setChecked(m_pScreen->fix(Screen::ScrollLock)); + m_pCheckBoxXTest->setChecked(m_pScreen->fix(Screen::XTest)); +} + +void ScreenSettingsDialog::accept() +{ + if (m_pLineEditName->text().isEmpty()) + { + QMessageBox::warning(this, tr("Screen name is empty"), tr("The name for a screen can not be empty. Please fill in a name or cancel the dialog.")); + return; + } + + m_pScreen->init(); + + m_pScreen->setName(m_pLineEditName->text()); + + for (int i = 0; i < m_pListAliases->count(); i++) + m_pScreen->addAlias(m_pListAliases->item(i)->text()); + + m_pScreen->setModifier(Screen::Shift, m_pComboBoxShift->currentIndex()); + m_pScreen->setModifier(Screen::Ctrl, m_pComboBoxCtrl->currentIndex()); + m_pScreen->setModifier(Screen::Alt, m_pComboBoxAlt->currentIndex()); + m_pScreen->setModifier(Screen::Meta, m_pComboBoxMeta->currentIndex()); + m_pScreen->setModifier(Screen::Super, m_pComboBoxSuper->currentIndex()); + + m_pScreen->setSwitchCorner(Screen::TopLeft, m_pCheckBoxCornerTopLeft->isChecked()); + m_pScreen->setSwitchCorner(Screen::TopRight, m_pCheckBoxCornerTopRight->isChecked()); + m_pScreen->setSwitchCorner(Screen::BottomLeft, m_pCheckBoxCornerBottomLeft->isChecked()); + m_pScreen->setSwitchCorner(Screen::BottomRight, m_pCheckBoxCornerBottomRight->isChecked()); + m_pScreen->setSwitchCornerSize(m_pSpinBoxSwitchCornerSize->value()); + + m_pScreen->setFix(Screen::CapsLock, m_pCheckBoxCapsLock->isChecked()); + m_pScreen->setFix(Screen::NumLock, m_pCheckBoxNumLock->isChecked()); + m_pScreen->setFix(Screen::ScrollLock, m_pCheckBoxScrollLock->isChecked()); + m_pScreen->setFix(Screen::XTest, m_pCheckBoxXTest->isChecked()); + + QDialog::accept(); +} + +void ScreenSettingsDialog::on_m_pButtonAddAlias_clicked() +{ + if (!m_pLineEditAlias->text().isEmpty() && m_pListAliases->findItems(m_pLineEditAlias->text(), Qt::MatchFixedString).isEmpty()) + { + new QListWidgetItem(m_pLineEditAlias->text(), m_pListAliases); + m_pLineEditAlias->clear(); + } +} + +void ScreenSettingsDialog::on_m_pLineEditAlias_textChanged(const QString& text) +{ + m_pButtonAddAlias->setEnabled(!text.isEmpty()); +} + +void ScreenSettingsDialog::on_m_pButtonRemoveAlias_clicked() +{ + QList items = m_pListAliases->selectedItems(); + + for (int i = 0; i < items.count(); i++) + delete items[i]; +} + +void ScreenSettingsDialog::on_m_pListAliases_itemSelectionChanged() +{ + m_pButtonRemoveAlias->setEnabled(!m_pListAliases->selectedItems().isEmpty()); +} + diff --git a/src/gui/src/ScreenSettingsDialog.h b/src/gui/src/ScreenSettingsDialog.h new file mode 100644 index 00000000..876b31e4 --- /dev/null +++ b/src/gui/src/ScreenSettingsDialog.h @@ -0,0 +1,52 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(SCREENSETTINGSDIALOG__H) + +#define SCREENSETTINGSDIALOG__H + +#include + +#include "ui_ScreenSettingsDialogBase.h" + +class QWidget; +class QString; + +class Screen; + +class ScreenSettingsDialog : public QDialog, public Ui::ScreenSettingsDialogBase +{ + Q_OBJECT + + public: + ScreenSettingsDialog(QWidget* parent, Screen* pScreen = NULL); + + public slots: + void accept(); + + private slots: + void on_m_pButtonAddAlias_clicked(); + void on_m_pButtonRemoveAlias_clicked(); + void on_m_pLineEditAlias_textChanged(const QString& text); + void on_m_pListAliases_itemSelectionChanged(); + + private: + Screen* m_pScreen; +}; + +#endif + diff --git a/src/gui/src/ScreenSetupModel.cpp b/src/gui/src/ScreenSetupModel.cpp new file mode 100644 index 00000000..f2fb4126 --- /dev/null +++ b/src/gui/src/ScreenSetupModel.cpp @@ -0,0 +1,142 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "ScreenSetupModel.h" +#include "Screen.h" + +#include +#include + +const QString ScreenSetupModel::m_MimeType = "application/x-qsynergy-screen"; + +ScreenSetupModel::ScreenSetupModel(ScreenList& screens, int numColumns, int numRows) : + QAbstractTableModel(NULL), + m_Screens(screens), + m_NumColumns(numColumns), + m_NumRows(numRows) +{ + if (m_NumColumns * m_NumRows > screens.size()) + qFatal("Not enough elements (%u) in screens QList for %d columns and %d rows", screens.size(), m_NumColumns, m_NumRows); +} + +QVariant ScreenSetupModel::data(const QModelIndex& index, int role) const +{ + if (index.isValid() && index.row() < m_NumRows && index.column() < m_NumColumns) + { + switch(role) + { + case Qt::DecorationRole: + if (screen(index).isNull()) + break; + return QIcon(*screen(index).pixmap()); + + case Qt::ToolTipRole: + if (screen(index).isNull()) + break; + return QString(tr( + "
Screen: %1
" + "
Double click to edit settings" + "
Drag screen to the trashcan to remove it")).arg(screen(index).name()); + + case Qt::DisplayRole: + if (screen(index).isNull()) + break; + return screen(index).name(); + } + } + + return QVariant(); +} + +Qt::ItemFlags ScreenSetupModel::flags(const QModelIndex& index) const +{ + if (!index.isValid() || index.row() >= m_NumRows || index.column() >= m_NumColumns) + return 0; + + if (!screen(index).isNull()) + return Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled; + + return Qt::ItemIsDropEnabled; +} + +Qt::DropActions ScreenSetupModel::supportedDropActions() const +{ + return Qt::MoveAction | Qt::CopyAction; +} + +QStringList ScreenSetupModel::mimeTypes() const +{ + return QStringList() << m_MimeType; +} + +QMimeData* ScreenSetupModel::mimeData(const QModelIndexList& indexes) const +{ + QMimeData* pMimeData = new QMimeData(); + QByteArray encodedData; + + QDataStream stream(&encodedData, QIODevice::WriteOnly); + + foreach (const QModelIndex& index, indexes) + if (index.isValid()) + stream << index.column() << index.row() << screen(index); + + pMimeData->setData(m_MimeType, encodedData); + + return pMimeData; +} + +bool ScreenSetupModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) +{ + if (action == Qt::IgnoreAction) + return true; + + if (!data->hasFormat(m_MimeType)) + return false; + + if (!parent.isValid() || row != -1 || column != -1) + return false; + + QByteArray encodedData = data->data(m_MimeType); + QDataStream stream(&encodedData, QIODevice::ReadOnly); + + int sourceColumn = -1; + int sourceRow = -1; + + stream >> sourceColumn; + stream >> sourceRow; + + // don't drop screen onto itself + if (sourceColumn == parent.column() && sourceRow == parent.row()) + return false; + + Screen droppedScreen; + stream >> droppedScreen; + + Screen oldScreen = screen(parent.column(), parent.row()); + if (!oldScreen.isNull() && sourceColumn != -1 && sourceRow != -1) + { + // mark the screen so it isn't deleted after the dragndrop succeeded + // see ScreenSetupView::startDrag() + oldScreen.setSwapped(true); + screen(sourceColumn, sourceRow) = oldScreen; + } + + screen(parent.column(), parent.row()) = droppedScreen; + + return true; +} + diff --git a/src/gui/src/ScreenSetupModel.h b/src/gui/src/ScreenSetupModel.h new file mode 100644 index 00000000..1537e7f1 --- /dev/null +++ b/src/gui/src/ScreenSetupModel.h @@ -0,0 +1,70 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(SCREENSETUPMODEL__H) + +#define SCREENSETUPMODEL__H + +#include +#include +#include +#include + +#include "Screen.h" + +class ScreenSetupView; +class ServerConfigDialog; + +class ScreenSetupModel : public QAbstractTableModel +{ + Q_OBJECT + + friend class ScreenSetupView; + friend class ServerConfigDialog; + + public: + ScreenSetupModel(ScreenList& screens, int numColumns, int numRows); + + public: + static const QString& mimeType() { return m_MimeType; } + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + int rowCount() const { return m_NumRows; } + int columnCount() const { return m_NumColumns; } + int rowCount(const QModelIndex&) const { return rowCount(); } + int columnCount(const QModelIndex&) const { return columnCount(); } + Qt::DropActions supportedDropActions() const; + Qt::ItemFlags flags(const QModelIndex& index) const; + QStringList mimeTypes() const; + QMimeData* mimeData(const QModelIndexList& indexes) const; + + protected: + bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); + const Screen& screen(const QModelIndex& index) const { return screen(index.column(), index.row()); } + Screen& screen(const QModelIndex& index) { return screen(index.column(), index.row()); } + const Screen& screen(int column, int row) const { return m_Screens[row * m_NumColumns + column]; } + Screen& screen(int column, int row) { return m_Screens[row * m_NumColumns + column]; } + + private: + ScreenList& m_Screens; + const int m_NumColumns; + const int m_NumRows; + + static const QString m_MimeType; +}; + +#endif + diff --git a/src/gui/src/ScreenSetupView.cpp b/src/gui/src/ScreenSetupView.cpp new file mode 100644 index 00000000..b238eaed --- /dev/null +++ b/src/gui/src/ScreenSetupView.cpp @@ -0,0 +1,160 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "ScreenSetupView.h" +#include "ScreenSetupModel.h" +#include "ScreenSettingsDialog.h" + +#include +#include + +ScreenSetupView::ScreenSetupView(QWidget* parent) : + QTableView(parent) +{ + setDropIndicatorShown(true); + setDragDropMode(DragDrop); + setSelectionMode(SingleSelection); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + setIconSize(QSize(64, 64)); + horizontalHeader()->hide(); + verticalHeader()->hide(); +} + +void ScreenSetupView::setModel(ScreenSetupModel* model) +{ + QTableView::setModel(model); + setTableSize(); +} + +ScreenSetupModel* ScreenSetupView::model() const +{ + return qobject_cast(QTableView::model()); +} + +void ScreenSetupView::setTableSize() +{ + for (int i = 0; i < model()->columnCount(); i++) + setColumnWidth(i, width() / model()->columnCount()); + + for (int i = 0; i < model()->rowCount(); i++) + setRowHeight(i, height() / model()->rowCount()); +} + +void ScreenSetupView::resizeEvent(QResizeEvent* event) +{ + setTableSize(); + event->ignore(); +} + +void ScreenSetupView::mouseDoubleClickEvent(QMouseEvent* event) +{ + if (event->buttons() & Qt::LeftButton) + { + int col = columnAt(event->pos().x()); + int row = rowAt(event->pos().y()); + + if (!model()->screen(col, row).isNull()) + { + ScreenSettingsDialog dlg(this, &model()->screen(col, row)); + dlg.exec(); + } + } + else + event->ignore(); +} + +void ScreenSetupView::dragEnterEvent(QDragEnterEvent* event) +{ + // we accept anything that enters us by a drag as long as the + // mime type is okay. anything else is dealt with in dragMoveEvent() + if (event->mimeData()->hasFormat(ScreenSetupModel::mimeType())) + event->accept(); + else + event->ignore(); +} + +void ScreenSetupView::dragMoveEvent(QDragMoveEvent* event) +{ + if (event->mimeData()->hasFormat(ScreenSetupModel::mimeType())) + { + // where does the event come from? myself or someone else? + if (event->source() == this) + { + // myself is ok, but then it must be a move action, never a copy + event->setDropAction(Qt::MoveAction); + event->accept(); + } + else + { + int col = columnAt(event->pos().x()); + int row = rowAt(event->pos().y()); + + // a drop from outside is not allowed if there's a screen already there. + if (!model()->screen(col, row).isNull()) + event->ignore(); + else + event->acceptProposedAction(); + } + } + else + event->ignore(); +} + +// this is reimplemented from QAbstractItemView::startDrag() +void ScreenSetupView::startDrag(Qt::DropActions) +{ + QModelIndexList indexes = selectedIndexes(); + + if (indexes.count() != 1) + return; + + QMimeData* pData = model()->mimeData(indexes); + if (pData == NULL) + return; + + QPixmap pixmap = *model()->screen(indexes[0]).pixmap(); + QDrag* pDrag = new QDrag(this); + pDrag->setPixmap(pixmap); + pDrag->setMimeData(pData); + pDrag->setHotSpot(QPoint(pixmap.width() / 2, pixmap.height() / 2)); + + if (pDrag->exec(Qt::MoveAction, Qt::MoveAction) == Qt::MoveAction) + { + selectionModel()->clear(); + + // make sure to only delete the drag source if screens weren't swapped + // see ScreenSetupModel::dropMimeData + if (!model()->screen(indexes[0]).swapped()) + model()->screen(indexes[0]) = Screen(); + else + model()->screen(indexes[0]).setSwapped(false); + } +} + +QStyleOptionViewItem ScreenSetupView::viewOptions() const +{ + QStyleOptionViewItem option = QTableView::viewOptions(); + option.showDecorationSelected = true; + option.decorationPosition = QStyleOptionViewItem::Top; + option.displayAlignment = Qt::AlignCenter; + option.textElideMode = Qt::ElideMiddle; + return option; +} + diff --git a/src/gui/src/ScreenSetupView.h b/src/gui/src/ScreenSetupView.h new file mode 100644 index 00000000..b6e4d156 --- /dev/null +++ b/src/gui/src/ScreenSetupView.h @@ -0,0 +1,56 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(SCREENSETUPVIEW__H) + +#define SCREENSETUPVIEW__H + +#include +#include + +#include "Screen.h" + +class QWidget; +class QMouseEvent; +class QResizeEvent; +class QDragEnterEvent; +class ScreenSetupModel; + +class ScreenSetupView : public QTableView +{ + Q_OBJECT + + public: + ScreenSetupView(QWidget* parent); + + public: + void setModel(ScreenSetupModel* model); + ScreenSetupModel* model() const; + + protected: + void mouseDoubleClickEvent(QMouseEvent*); + void setTableSize(); + void resizeEvent(QResizeEvent*); + void dragEnterEvent(QDragEnterEvent* event); + void dragMoveEvent(QDragMoveEvent* event); + void startDrag(Qt::DropActions supportedActions); + QStyleOptionViewItem viewOptions() const; + void scrollTo(const QModelIndex&, ScrollHint) {} +}; + +#endif + diff --git a/src/gui/src/ServerConfig.cpp b/src/gui/src/ServerConfig.cpp new file mode 100644 index 00000000..386d447d --- /dev/null +++ b/src/gui/src/ServerConfig.cpp @@ -0,0 +1,264 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "ServerConfig.h" +#include "Hotkey.h" + +#include + +static const struct +{ + int x; + int y; + const char* name; +} neighbourDirs[] = +{ + { 0, -1, "up" }, + { 1, 0, "right" }, + { 0, 1, "down" }, + { -1, 0, "left" }, +}; + + +ServerConfig::ServerConfig(QSettings* settings, int numColumns, int numRows) : + m_pSettings(settings), + m_Screens(), + m_NumColumns(numColumns), + m_NumRows(numRows) +{ + Q_ASSERT(m_pSettings); + + loadSettings(); +} + +ServerConfig::~ServerConfig() +{ + saveSettings(); +} + +bool ServerConfig::save(const QString& fileName) const +{ + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) + return false; + + save(file); + file.close(); + + return true; +} + +void ServerConfig::save(QFile& file) const +{ + QTextStream outStream(&file); + outStream << *this; +} + +void ServerConfig::init() +{ + switchCorners().clear(); + screens().clear(); + + // m_NumSwitchCorners is used as a fixed size array. See Screen::init() + for (int i = 0; i < NumSwitchCorners; i++) + switchCorners() << false; + + // There must always be screen objects for each cell in the screens QList. Unused screens + // are identified by having an empty name. + for (int i = 0; i < numColumns() * numRows(); i++) + addScreen(Screen()); +} + +void ServerConfig::saveSettings() +{ + settings().beginGroup("internalConfig"); + settings().remove(""); + + settings().setValue("numColumns", numColumns()); + settings().setValue("numRows", numRows()); + + settings().setValue("hasHeartbeat", hasHeartbeat()); + settings().setValue("heartbeat", heartbeat()); + settings().setValue("relativeMouseMoves", relativeMouseMoves()); + settings().setValue("screenSaverSync", screenSaverSync()); + settings().setValue("win32KeepForeground", win32KeepForeground()); + settings().setValue("hasSwitchDelay", hasSwitchDelay()); + settings().setValue("switchDelay", switchDelay()); + settings().setValue("hasSwitchDoubleTap", hasSwitchDoubleTap()); + settings().setValue("switchDoubleTap", switchDoubleTap()); + settings().setValue("switchCornerSize", switchCornerSize()); + + writeSettings(settings(), switchCorners(), "switchCorner"); + + settings().beginWriteArray("screens"); + for (int i = 0; i < screens().size(); i++) + { + settings().setArrayIndex(i); + screens()[i].saveSettings(settings()); + } + settings().endArray(); + + settings().beginWriteArray("hotkeys"); + for (int i = 0; i < hotkeys().size(); i++) + { + settings().setArrayIndex(i); + hotkeys()[i].saveSettings(settings()); + } + settings().endArray(); + + settings().endGroup(); +} + +void ServerConfig::loadSettings() +{ + settings().beginGroup("internalConfig"); + + setNumColumns(settings().value("numColumns", 5).toInt()); + setNumRows(settings().value("numRows", 3).toInt()); + + // we need to know the number of columns and rows before we can set up ourselves + init(); + + haveHeartbeat(settings().value("hasHeartbeat", false).toBool()); + setHeartbeat(settings().value("heartbeat", 5000).toInt()); + setRelativeMouseMoves(settings().value("relativeMouseMoves", false).toBool()); + setScreenSaverSync(settings().value("screenSaverSync", true).toBool()); + setWin32KeepForeground(settings().value("win32KeepForeground", false).toBool()); + haveSwitchDelay(settings().value("hasSwitchDelay", false).toBool()); + setSwitchDelay(settings().value("switchDelay", 250).toInt()); + haveSwitchDoubleTap(settings().value("hasSwitchDoubleTap", false).toBool()); + setSwitchDoubleTap(settings().value("switchDoubleTap", 250).toInt()); + setSwitchCornerSize(settings().value("switchCornerSize").toInt()); + + readSettings(settings(), switchCorners(), "switchCorner", false, NumSwitchCorners); + + int numScreens = settings().beginReadArray("screens"); + Q_ASSERT(numScreens <= screens().size()); + for (int i = 0; i < numScreens; i++) + { + settings().setArrayIndex(i); + screens()[i].loadSettings(settings()); + } + settings().endArray(); + + int numHotkeys = settings().beginReadArray("hotkeys"); + for (int i = 0; i < numHotkeys; i++) + { + settings().setArrayIndex(i); + Hotkey h; + h.loadSettings(settings()); + hotkeys().append(h); + } + settings().endArray(); + + settings().endGroup(); +} + +int ServerConfig::adjacentScreenIndex(int idx, int deltaColumn, int deltaRow) const +{ + if (screens()[idx].isNull()) + return -1; + + // if we're at the left or right end of the table, don't find results going further left or right + if ((deltaColumn > 0 && (idx+1) % numColumns() == 0) + || (deltaColumn < 0 && idx % numColumns() == 0)) + return -1; + + int arrayPos = idx + deltaColumn + deltaRow * numColumns(); + + if (arrayPos >= screens().size() || arrayPos < 0) + return -1; + + return arrayPos; +} + +QTextStream& operator<<(QTextStream& outStream, const ServerConfig& config) +{ + outStream << "section: screens" << endl; + + foreach (const Screen& s, config.screens()) + if (!s.isNull()) + s.writeScreensSection(outStream); + + outStream << "end" << endl << endl; + + outStream << "section: aliases" << endl; + + foreach (const Screen& s, config.screens()) + if (!s.isNull()) + s.writeAliasesSection(outStream); + + outStream << "end" << endl << endl; + + outStream << "section: links" << endl; + + for (int i = 0; i < config.screens().size(); i++) + if (!config.screens()[i].isNull()) + { + outStream << "\t" << config.screens()[i].name() << ":" << endl; + + for (unsigned int j = 0; j < sizeof(neighbourDirs) / sizeof(neighbourDirs[0]); j++) + { + int idx = config.adjacentScreenIndex(i, neighbourDirs[j].x, neighbourDirs[j].y); + if (idx != -1 && !config.screens()[idx].isNull()) + outStream << "\t\t" << neighbourDirs[j].name << " = " << config.screens()[idx].name() << endl; + } + } + + outStream << "end" << endl << endl; + + outStream << "section: options" << endl; + + if (config.hasHeartbeat()) + outStream << "\t" << "heartbeat = " << config.heartbeat() << endl; + + outStream << "\t" << "relativeMouseMoves = " << (config.relativeMouseMoves() ? "true" : "false") << endl; + outStream << "\t" << "screenSaverSync = " << (config.screenSaverSync() ? "true" : "false") << endl; + outStream << "\t" << "win32KeepForeground = " << (config.win32KeepForeground() ? "true" : "false") << endl; + + if (config.hasSwitchDelay()) + outStream << "\t" << "switchDelay = " << config.switchDelay() << endl; + + if (config.hasSwitchDoubleTap()) + outStream << "\t" << "switchDoubleTap = " << config.switchDoubleTap() << endl; + + outStream << "\t" << "switchCorners = none "; + for (int i = 0; i < config.switchCorners().size(); i++) + if (config.switchCorners()[i]) + outStream << "+" << config.switchCornerName(i) << " "; + outStream << endl; + + outStream << "\t" << "switchCornerSize = " << config.switchCornerSize() << endl; + + foreach(const Hotkey& hotkey, config.hotkeys()) + outStream << hotkey; + + outStream << "end" << endl << endl; + + return outStream; +} + +int ServerConfig::numScreens() const +{ + int rval = 0; + + foreach(const Screen& s, screens()) + if (!s.isNull()) + rval++; + + return rval; +} diff --git a/src/gui/src/ServerConfig.h b/src/gui/src/ServerConfig.h new file mode 100644 index 00000000..327b5556 --- /dev/null +++ b/src/gui/src/ServerConfig.h @@ -0,0 +1,113 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(SERVERCONFIG__H) + +#define SERVERCONFIG__H + +#include + +#include "Screen.h" +#include "BaseConfig.h" +#include "Hotkey.h" + +class QTextStream; +class QSettings; +class QString; +class QFile; +class ServerConfigDialog; + +class ServerConfig : public BaseConfig +{ + friend class ServerConfigDialog; + friend QTextStream& operator<<(QTextStream& outStream, const ServerConfig& config); + + public: + ServerConfig(QSettings* settings, int numColumns, int numRows); + ~ServerConfig(); + + public: + const ScreenList& screens() const { return m_Screens; } + int numColumns() const { return m_NumColumns; } + int numRows() const { return m_NumRows; } + bool hasHeartbeat() const { return m_HasHeartbeat; } + int heartbeat() const { return m_Heartbeat; } + bool relativeMouseMoves() const { return m_RelativeMouseMoves; } + bool screenSaverSync() const { return m_ScreenSaverSync; } + bool win32KeepForeground() const { return m_Win32KeepForeground; } + bool hasSwitchDelay() const { return m_HasSwitchDelay; } + int switchDelay() const { return m_SwitchDelay; } + bool hasSwitchDoubleTap() const { return m_HasSwitchDoubleTap; } + int switchDoubleTap() const { return m_SwitchDoubleTap; } + bool switchCorner(int c) const { return m_SwitchCorners[c]; } + int switchCornerSize() const { return m_SwitchCornerSize; } + const QList& switchCorners() const { return m_SwitchCorners; } + const HotkeyList& hotkeys() const { return m_Hotkeys; } + + void saveSettings(); + void loadSettings(); + bool save(const QString& fileName) const; + void save(QFile& file) const; + int numScreens() const; + + protected: + QSettings& settings() { return *m_pSettings; } + ScreenList& screens() { return m_Screens; } + void setScreens(const ScreenList& screens) { m_Screens = screens; } + void addScreen(const Screen& screen) { m_Screens.append(screen); } + void setNumColumns(int n) { m_NumColumns = n; } + void setNumRows(int n) { m_NumRows = n; } + void haveHeartbeat(bool on) { m_HasHeartbeat = on; } + void setHeartbeat(int val) { m_Heartbeat = val; } + void setRelativeMouseMoves(bool on) { m_RelativeMouseMoves = on; } + void setScreenSaverSync(bool on) { m_ScreenSaverSync = on; } + void setWin32KeepForeground(bool on) { m_Win32KeepForeground = on; } + void haveSwitchDelay(bool on) { m_HasSwitchDelay = on; } + void setSwitchDelay(int val) { m_SwitchDelay = val; } + void haveSwitchDoubleTap(bool on) { m_HasSwitchDoubleTap = on; } + void setSwitchDoubleTap(int val) { m_SwitchDoubleTap = val; } + void setSwitchCorner(int c, bool on) { m_SwitchCorners[c] = on; } + void setSwitchCornerSize(int val) { m_SwitchCornerSize = val; } + QList& switchCorners() { return m_SwitchCorners; } + HotkeyList& hotkeys() { return m_Hotkeys; } + + void init(); + int adjacentScreenIndex(int idx, int deltaColumn, int deltaRow) const; + + private: + QSettings* m_pSettings; + ScreenList m_Screens; + int m_NumColumns; + int m_NumRows; + bool m_HasHeartbeat; + int m_Heartbeat; + bool m_RelativeMouseMoves; + bool m_ScreenSaverSync; + bool m_Win32KeepForeground; + bool m_HasSwitchDelay; + int m_SwitchDelay; + bool m_HasSwitchDoubleTap; + int m_SwitchDoubleTap; + int m_SwitchCornerSize; + QList m_SwitchCorners; + HotkeyList m_Hotkeys; +}; + +QTextStream& operator<<(QTextStream& outStream, const ServerConfig& config); + +#endif + diff --git a/src/gui/src/ServerConfigDialog.cpp b/src/gui/src/ServerConfigDialog.cpp new file mode 100644 index 00000000..aea75278 --- /dev/null +++ b/src/gui/src/ServerConfigDialog.cpp @@ -0,0 +1,196 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "ServerConfigDialog.h" +#include "ServerConfig.h" +#include "HotkeyDialog.h" +#include "ActionDialog.h" + +#include +#include + +ServerConfigDialog::ServerConfigDialog(QWidget* parent, ServerConfig& config, const QString& defaultScreenName) : + QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint), + Ui::ServerConfigDialogBase(), + m_OrigServerConfig(config), + m_ServerConfig(config), + m_ScreenSetupModel(serverConfig().screens(), serverConfig().numColumns(), serverConfig().numRows()) +{ + setupUi(this); + + m_pCheckBoxHeartbeat->setChecked(serverConfig().hasHeartbeat()); + m_pSpinBoxHeartbeat->setValue(serverConfig().heartbeat()); + + m_pCheckBoxRelativeMouseMoves->setChecked(serverConfig().relativeMouseMoves()); + m_pCheckBoxScreenSaverSync->setChecked(serverConfig().screenSaverSync()); + m_pCheckBoxWin32KeepForeground->setChecked(serverConfig().win32KeepForeground()); + + m_pCheckBoxSwitchDelay->setChecked(serverConfig().hasSwitchDelay()); + m_pSpinBoxSwitchDelay->setValue(serverConfig().switchDelay()); + + m_pCheckBoxSwitchDoubleTap->setChecked(serverConfig().hasSwitchDoubleTap()); + m_pSpinBoxSwitchDoubleTap->setValue(serverConfig().switchDoubleTap()); + + m_pCheckBoxCornerTopLeft->setChecked(serverConfig().switchCorner(BaseConfig::TopLeft)); + m_pCheckBoxCornerTopRight->setChecked(serverConfig().switchCorner(BaseConfig::TopRight)); + m_pCheckBoxCornerBottomLeft->setChecked(serverConfig().switchCorner(BaseConfig::BottomLeft)); + m_pCheckBoxCornerBottomRight->setChecked(serverConfig().switchCorner(BaseConfig::BottomRight)); + m_pSpinBoxSwitchCornerSize->setValue(serverConfig().switchCornerSize()); + + foreach(const Hotkey& hotkey, serverConfig().hotkeys()) + m_pListHotkeys->addItem(hotkey.text()); + + m_pScreenSetupView->setModel(&m_ScreenSetupModel); + + if (serverConfig().numScreens() == 0) + model().screen(serverConfig().numColumns() / 2, serverConfig().numRows() / 2) = Screen(defaultScreenName); +} + +void ServerConfigDialog::accept() +{ + serverConfig().haveHeartbeat(m_pCheckBoxHeartbeat->isChecked()); + serverConfig().setHeartbeat(m_pSpinBoxHeartbeat->value()); + + serverConfig().setRelativeMouseMoves(m_pCheckBoxRelativeMouseMoves->isChecked()); + serverConfig().setScreenSaverSync(m_pCheckBoxScreenSaverSync->isChecked()); + serverConfig().setWin32KeepForeground(m_pCheckBoxWin32KeepForeground->isChecked()); + + serverConfig().haveSwitchDelay(m_pCheckBoxSwitchDelay->isChecked()); + serverConfig().setSwitchDelay(m_pSpinBoxSwitchDelay->value()); + + serverConfig().haveSwitchDoubleTap(m_pCheckBoxSwitchDoubleTap->isChecked()); + serverConfig().setSwitchDoubleTap(m_pSpinBoxSwitchDoubleTap->value()); + + serverConfig().setSwitchCorner(BaseConfig::TopLeft, m_pCheckBoxCornerTopLeft->isChecked()); + serverConfig().setSwitchCorner(BaseConfig::TopRight, m_pCheckBoxCornerTopRight->isChecked()); + serverConfig().setSwitchCorner(BaseConfig::BottomLeft, m_pCheckBoxCornerBottomLeft->isChecked()); + serverConfig().setSwitchCorner(BaseConfig::BottomRight, m_pCheckBoxCornerBottomRight->isChecked()); + serverConfig().setSwitchCornerSize(m_pSpinBoxSwitchCornerSize->value()); + + // now that the dialog has been accepted, copy the new server config to the original one, + // which is a reference to the one in MainWindow. + setOrigServerConfig(serverConfig()); + + QDialog::accept(); +} + +void ServerConfigDialog::on_m_pButtonNewHotkey_clicked() +{ + Hotkey hotkey; + HotkeyDialog dlg(this, hotkey); + if (dlg.exec() == QDialog::Accepted) + { + serverConfig().hotkeys().append(hotkey); + m_pListHotkeys->addItem(hotkey.text()); + } +} + +void ServerConfigDialog::on_m_pButtonEditHotkey_clicked() +{ + int idx = m_pListHotkeys->currentRow(); + Q_ASSERT(idx >= 0 && idx < serverConfig().hotkeys().size()); + Hotkey& hotkey = serverConfig().hotkeys()[idx]; + HotkeyDialog dlg(this, hotkey); + if (dlg.exec() == QDialog::Accepted) + m_pListHotkeys->currentItem()->setText(hotkey.text()); +} + +void ServerConfigDialog::on_m_pButtonRemoveHotkey_clicked() +{ + int idx = m_pListHotkeys->currentRow(); + Q_ASSERT(idx >= 0 && idx < serverConfig().hotkeys().size()); + serverConfig().hotkeys().removeAt(idx); + m_pListActions->clear(); + delete m_pListHotkeys->item(idx); +} + +void ServerConfigDialog::on_m_pListHotkeys_itemSelectionChanged() +{ + bool itemsSelected = !m_pListHotkeys->selectedItems().isEmpty(); + m_pButtonEditHotkey->setEnabled(itemsSelected); + m_pButtonRemoveHotkey->setEnabled(itemsSelected); + m_pButtonNewAction->setEnabled(itemsSelected); + + if (itemsSelected && serverConfig().hotkeys().size() > 0) + { + m_pListActions->clear(); + + int idx = m_pListHotkeys->row(m_pListHotkeys->selectedItems()[0]); + + // There's a bug somewhere around here: We get idx == 1 right after we deleted the next to last item, so idx can + // only possibly be 0. GDB shows we got called indirectly from the delete line in + // on_m_pButtonRemoveHotkey_clicked() above, but the delete is of course necessary and seems correct. + // The while() is a generalized workaround for all that and shouldn't be required. + while (idx >= 0 && idx >= serverConfig().hotkeys().size()) + idx--; + + Q_ASSERT(idx >= 0 && idx < serverConfig().hotkeys().size()); + + const Hotkey& hotkey = serverConfig().hotkeys()[idx]; + foreach(const Action& action, hotkey.actions()) + m_pListActions->addItem(action.text()); + } +} + +void ServerConfigDialog::on_m_pButtonNewAction_clicked() +{ + int idx = m_pListHotkeys->currentRow(); + Q_ASSERT(idx >= 0 && idx < serverConfig().hotkeys().size()); + Hotkey& hotkey = serverConfig().hotkeys()[idx]; + + Action action; + ActionDialog dlg(this, serverConfig(), hotkey, action); + if (dlg.exec() == QDialog::Accepted) + { + hotkey.actions().append(action); + m_pListActions->addItem(action.text()); + } +} + +void ServerConfigDialog::on_m_pButtonEditAction_clicked() +{ + int idxHotkey = m_pListHotkeys->currentRow(); + Q_ASSERT(idxHotkey >= 0 && idxHotkey < serverConfig().hotkeys().size()); + Hotkey& hotkey = serverConfig().hotkeys()[idxHotkey]; + + int idxAction = m_pListActions->currentRow(); + Q_ASSERT(idxAction >= 0 && idxAction < hotkey.actions().size()); + Action& action = hotkey.actions()[idxAction]; + + ActionDialog dlg(this, serverConfig(), hotkey, action); + if (dlg.exec() == QDialog::Accepted) + m_pListActions->currentItem()->setText(action.text()); +} + +void ServerConfigDialog::on_m_pButtonRemoveAction_clicked() +{ + int idxHotkey = m_pListHotkeys->currentRow(); + Q_ASSERT(idxHotkey >= 0 && idxHotkey < serverConfig().hotkeys().size()); + Hotkey& hotkey = serverConfig().hotkeys()[idxHotkey]; + + int idxAction = m_pListActions->currentRow(); + Q_ASSERT(idxAction >= 0 && idxAction < hotkey.actions().size()); + + hotkey.actions().removeAt(idxAction); + delete m_pListActions->currentItem(); +} + +void ServerConfigDialog::on_m_pListActions_itemSelectionChanged() +{ + m_pButtonEditAction->setEnabled(!m_pListActions->selectedItems().isEmpty()); + m_pButtonRemoveAction->setEnabled(!m_pListActions->selectedItems().isEmpty()); +} diff --git a/src/gui/src/ServerConfigDialog.h b/src/gui/src/ServerConfigDialog.h new file mode 100644 index 00000000..9fb34b16 --- /dev/null +++ b/src/gui/src/ServerConfigDialog.h @@ -0,0 +1,62 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(SERVERCONFIGDIALOG__H) + +#define SERVERCONFIGDIALOG__H + +#include "ScreenSetupModel.h" +#include "ServerConfig.h" + +#include "ui_ServerConfigDialogBase.h" + +#include + +class ServerConfigDialog : public QDialog, public Ui::ServerConfigDialogBase +{ + Q_OBJECT + + public: + ServerConfigDialog(QWidget* parent, ServerConfig& config, const QString& defaultScreenName); + + public slots: + void accept(); + + protected slots: + void on_m_pButtonNewHotkey_clicked(); + void on_m_pListHotkeys_itemSelectionChanged(); + void on_m_pButtonEditHotkey_clicked(); + void on_m_pButtonRemoveHotkey_clicked(); + + void on_m_pButtonNewAction_clicked(); + void on_m_pListActions_itemSelectionChanged(); + void on_m_pButtonEditAction_clicked(); + void on_m_pButtonRemoveAction_clicked(); + + protected: + ServerConfig& serverConfig() { return m_ServerConfig; } + void setOrigServerConfig(const ServerConfig& s) { m_OrigServerConfig = s; } + ScreenSetupModel& model() { return m_ScreenSetupModel; } + + private: + ServerConfig& m_OrigServerConfig; + ServerConfig m_ServerConfig; + ScreenSetupModel m_ScreenSetupModel; +}; + +#endif + diff --git a/src/gui/src/SettingsDialog.cpp b/src/gui/src/SettingsDialog.cpp new file mode 100644 index 00000000..cf1cc517 --- /dev/null +++ b/src/gui/src/SettingsDialog.cpp @@ -0,0 +1,89 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "SettingsDialog.h" + +#include +#include + +#include "AppConfig.h" + +SettingsDialog::SettingsDialog(QWidget* parent, AppConfig& config) : + QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint), + Ui::SettingsDialogBase(), + m_AppConfig(config) +{ + setupUi(this); + + m_pCheckBoxAutoConnect->setChecked(appConfig().autoConnect()); + m_pLineEditScreenName->setText(appConfig().screenName()); + m_pSpinBoxPort->setValue(appConfig().port()); + m_pLineEditInterface->setText(appConfig().interface()); + m_pComboLogLevel->setCurrentIndex(appConfig().logLevel()); + m_pCheckBoxLogToFile->setChecked(appConfig().logToFile()); + m_pLineEditLogFilename->setText(appConfig().logFilename()); + m_pCheckBoxAutoStart->setChecked(appConfig().autoStart()); + m_pCheckBoxAutoHide->setChecked(appConfig().autoHide()); + +#ifdef Q_OS_WIN + m_pComboBoxGameDevice->setCurrentIndex(appConfig().gameModeIndex()); + m_pRadioButtonGamePollDynamic->setChecked(appConfig().gamePollingDynamic()); + m_pRadioButtonGamePollStatic->setChecked(!appConfig().gamePollingDynamic()); + m_pSpinBoxGamePoll->setValue(appConfig().gamePollingFrequency()); +#else + m_pGroupGameDevices->setEnabled(false); +#endif +} + +void SettingsDialog::accept() +{ + appConfig().setAutoConnect(m_pCheckBoxAutoConnect->isChecked()); + appConfig().setScreenName(m_pLineEditScreenName->text()); + appConfig().setPort(m_pSpinBoxPort->value()); + appConfig().setInterface(m_pLineEditInterface->text()); + appConfig().setLogLevel(m_pComboLogLevel->currentIndex()); + appConfig().setLogToFile(m_pCheckBoxLogToFile->isChecked()); + appConfig().setLogFilename(m_pLineEditLogFilename->text()); + appConfig().setAutoStart(m_pCheckBoxAutoStart->isChecked()); + appConfig().setAutoHide(m_pCheckBoxAutoHide->isChecked()); + appConfig().setGameModeIndex(m_pComboBoxGameDevice->currentIndex()); + appConfig().setGamePollingDynamic(m_pRadioButtonGamePollDynamic->isChecked()); + appConfig().setGamePollingFrequency(m_pSpinBoxGamePoll->value()); + + QDialog::accept(); +} + +void SettingsDialog::on_m_pCheckBoxLogToFile_stateChanged(int i) +{ + bool checked = i == 2; + + m_pLineEditLogFilename->setEnabled(checked); + m_pButtonBrowseLog->setEnabled(checked); +} + +void SettingsDialog::on_m_pButtonBrowseLog_clicked() +{ + QString fileName = QFileDialog::getSaveFileName( + this, tr("Save log file to..."), + m_pLineEditLogFilename->text(), + "Logs (*.log *.txt)"); + + if (!fileName.isEmpty()) + { + m_pLineEditLogFilename->setText(fileName); + } +} diff --git a/src/gui/src/SettingsDialog.h b/src/gui/src/SettingsDialog.h new file mode 100644 index 00000000..25f3ec7e --- /dev/null +++ b/src/gui/src/SettingsDialog.h @@ -0,0 +1,48 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(SETTINGSDIALOG_H) + +#define SETTINGSDIALOG_H + +#include +#include "ui_SettingsDialogBase.h" + +class AppConfig; + +class SettingsDialog : public QDialog, public Ui::SettingsDialogBase +{ + Q_OBJECT + + public: + SettingsDialog(QWidget* parent, AppConfig& config); + static QString browseForSynergyc(QWidget* parent, const QString& programDir, const QString& synergycName); + static QString browseForSynergys(QWidget* parent, const QString& programDir, const QString& synergysName); + + protected: + void accept(); + AppConfig& appConfig() { return m_AppConfig; } + + private: + AppConfig& m_AppConfig; + + private slots: + void on_m_pCheckBoxLogToFile_stateChanged(int ); + void on_m_pButtonBrowseLog_clicked(); +}; + +#endif diff --git a/src/gui/src/SetupWizard.cpp b/src/gui/src/SetupWizard.cpp new file mode 100644 index 00000000..180d8939 --- /dev/null +++ b/src/gui/src/SetupWizard.cpp @@ -0,0 +1,120 @@ +#include "SetupWizard.h" +#include "MainWindow.h" + +#include +#include + +SetupWizard::SetupWizard(MainWindow& mainWindow, bool startMain) : + m_MainWindow(mainWindow), + m_StartMain(startMain) +{ + setupUi(this); + +#if defined(Q_OS_MAC) + + // the mac style needs a little more room because of the + // graphic on the left. + resize(600, 500); + setMinimumSize(size()); + +#elif defined(Q_OS_WIN) + + // when areo is disabled on windows, the next/back buttons + // are hidden (must be a qt bug) -- resizing the window + // to +1 of the original height seems to fix this. + // NOTE: calling setMinimumSize after this will break + // it again, so don't do that. + resize(size().width(), size().height() + 1); + +#endif + +#if !defined(Q_OS_WIN) + m_pServiceRadioButton->setEnabled(false); + m_pServiceRadioButton->setText(tr("Service (Windows only)")); + m_pServiceLabel->setEnabled(false); + m_pDesktopRadioButton->setChecked(true); +#endif + + connect(this, SIGNAL(finished(int)), this, SLOT(handlefinished())); + connect(m_pServerRadioButton, SIGNAL(toggled(bool)), m_MainWindow.m_pGroupServer, SLOT(setChecked(bool))); + connect(m_pClientRadioButton, SIGNAL(toggled(bool)), m_MainWindow.m_pGroupClient, SLOT(setChecked(bool))); +} + +SetupWizard::~SetupWizard() +{ +} + +bool SetupWizard::validateCurrentPage() +{ + QMessageBox message; + message.setWindowTitle(tr("Setup Synergy")); + message.setIcon(QMessageBox::Information); + + bool result = false; + if (currentPage() == m_pNodePage) + { + result = m_pClientRadioButton->isChecked() || + m_pServerRadioButton->isChecked(); + + if (!result) + { + message.setText(tr("Please select an option.")); + message.exec(); + } + } + else if(currentPage() == m_pStartupPage) + { + result = m_pServiceRadioButton->isChecked() || + m_pDesktopRadioButton->isChecked() || + m_pNoneRadioButton->isChecked(); + + if (!result) + { + message.setText(tr("Please select an option.")); + message.exec(); + } + } + return result; +} + +void SetupWizard::handlefinished() +{ + close(); + + AppConfig& appConfig = m_MainWindow.appConfig(); + if (m_pServiceRadioButton->isChecked()) + { + appConfig.setProcessMode(Service); + } + else if (m_pDesktopRadioButton->isChecked()) + { + appConfig.setProcessMode(Desktop); + appConfig.setAutoStart(true); + appConfig.setAutoConnect(true); + } + else if (m_pNoneRadioButton->isChecked()) + { + // still use desktop mode, but don't auto start. + appConfig.setProcessMode(Desktop); + } + appConfig.setWizardHasRun(true); + appConfig.saveSettings(); + + QSettings& settings = m_MainWindow.settings(); + if (m_pServerRadioButton->isChecked()) + { + settings.setValue("groupServerChecked", true); + settings.setValue("groupClientChecked", false); + } + if (m_pClientRadioButton->isChecked()) + { + settings.setValue("groupClientChecked", true); + settings.setValue("groupServerChecked", false); + } + settings.sync(); + + if (m_StartMain) + { + m_MainWindow.start(true); + } +} diff --git a/src/gui/src/SetupWizard.h b/src/gui/src/SetupWizard.h new file mode 100644 index 00000000..8a760974 --- /dev/null +++ b/src/gui/src/SetupWizard.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include "ui_SetupWizardBase.h" + +class MainWindow; + +class SetupWizard : public QWizard, public Ui::SetupWizardBase +{ + Q_OBJECT +public: + SetupWizard(MainWindow& mainWindow, bool startMain); + virtual ~SetupWizard(); + bool validateCurrentPage(); +protected slots: + void handlefinished(); +private: + MainWindow& m_MainWindow; + bool m_StartMain; +}; diff --git a/src/gui/src/TrashScreenWidget.cpp b/src/gui/src/TrashScreenWidget.cpp new file mode 100644 index 00000000..1be9aabc --- /dev/null +++ b/src/gui/src/TrashScreenWidget.cpp @@ -0,0 +1,42 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 "TrashScreenWidget.h" +#include "ScreenSetupModel.h" + +#include +#include + +void TrashScreenWidget::dragEnterEvent(QDragEnterEvent* event) +{ + if (event->mimeData()->hasFormat(ScreenSetupModel::mimeType())) + { + event->setDropAction(Qt::MoveAction); + event->accept(); + } + else + event->ignore(); +} + +void TrashScreenWidget::dropEvent(QDropEvent* event) +{ + if (event->mimeData()->hasFormat(ScreenSetupModel::mimeType())) + event->acceptProposedAction(); + else + event->ignore(); +} + diff --git a/src/gui/src/TrashScreenWidget.h b/src/gui/src/TrashScreenWidget.h new file mode 100644 index 00000000..34a5608e --- /dev/null +++ b/src/gui/src/TrashScreenWidget.h @@ -0,0 +1,41 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#if !defined(TRASHSCREENWIDGET__H) + +#define TRASHSCREENWIDGET__H + +#include + +class QWidget; +class QDragEnterEvent; +class QDropEvent; + +class TrashScreenWidget : public QLabel +{ + Q_OBJECT + + public: + TrashScreenWidget(QWidget* parent) : QLabel(parent) {} + + public: + void dragEnterEvent(QDragEnterEvent* event); + void dropEvent(QDropEvent* event); +}; + +#endif + diff --git a/src/gui/src/UpdateCheck.cpp b/src/gui/src/UpdateCheck.cpp new file mode 100644 index 00000000..e69de29b diff --git a/src/gui/src/VersionChecker.cpp b/src/gui/src/VersionChecker.cpp new file mode 100644 index 00000000..76a64600 --- /dev/null +++ b/src/gui/src/VersionChecker.cpp @@ -0,0 +1,102 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "VersionChecker.h" + +#include +#include +#include +#include +#include + +#define VERSION_REGEX "(\\d\\.\\d\\.\\d)" +#define VERSION_URL "http://synergy-plus.googlecode.com/svn/web/version.txt" + +VersionChecker::VersionChecker() +{ + m_manager = new QNetworkAccessManager(this); + + connect(m_manager, SIGNAL(finished(QNetworkReply*)), + this, SLOT(replyFinished(QNetworkReply*))); +} + +VersionChecker::~VersionChecker() +{ + delete m_manager; +} + +void VersionChecker::checkLatest() +{ + m_manager->get(QNetworkRequest(QUrl(VERSION_URL))); +} + +void VersionChecker::replyFinished(QNetworkReply* reply) +{ + QString newestVersion = QString(reply->readAll()); + if (!newestVersion.isEmpty()) + { + QString currentVersion = getVersion(); + if (compareVersions(currentVersion, newestVersion) > 0) + emit updateFound(newestVersion); + } +} + +int VersionChecker::compareVersions(const QString& left, const QString& right) +{ + if (left.compare(right) == 0) + return 0; // versions are same. + + QStringList leftSplit = left.split(QRegExp("\\.")); + if (leftSplit.size() != 3) + return 1; // assume right wins. + + QStringList rightSplit = right.split(QRegExp("\\.")); + if (rightSplit.size() != 3) + return -1; // assume left wins. + + int leftMajor = leftSplit.at(0).toInt(); + int leftMinor = leftSplit.at(1).toInt(); + int leftRev = leftSplit.at(2).toInt(); + + int rightMajor = rightSplit.at(0).toInt(); + int rightMinor = rightSplit.at(1).toInt(); + int rightRev = rightSplit.at(2).toInt(); + + bool rightWins = + (rightMajor > leftMajor) || + ((rightMajor >= leftMajor) && (rightMinor > leftMinor)) || + ((rightMajor >= leftMajor) && (rightMinor >= leftMinor) && (rightRev > leftRev)); + + return rightWins ? 1 : -1; +} + +QString VersionChecker::getVersion() +{ + QProcess process; + process.start(m_app, QStringList() << "--version"); + + process.setReadChannel(QProcess::StandardOutput); + if (process.waitForStarted() && process.waitForFinished()) + { + QRegExp rx(VERSION_REGEX); + QString text = process.readLine(); + if (rx.indexIn(text) != -1) + return rx.cap(1); + } + + return tr("Unknown"); +} diff --git a/src/gui/src/VersionChecker.h b/src/gui/src/VersionChecker.h new file mode 100644 index 00000000..88924a4d --- /dev/null +++ b/src/gui/src/VersionChecker.h @@ -0,0 +1,43 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 +#include + +class QNetworkAccessManager; +class QNetworkReply; + +class VersionChecker : public QObject +{ + Q_OBJECT +public: + VersionChecker(); + virtual ~VersionChecker(); + void checkLatest(); + QString getVersion(); + void setApp(const QString& app) { m_app = app; } + int compareVersions(const QString& left, const QString& right); +public slots: + void replyFinished(QNetworkReply* reply); +signals: + void updateFound(const QString& version); +private: + QNetworkAccessManager* m_manager; + QString m_app; +}; diff --git a/src/gui/src/main.cpp b/src/gui/src/main.cpp new file mode 100644 index 00000000..e81193ff --- /dev/null +++ b/src/gui/src/main.cpp @@ -0,0 +1,103 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * + * 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 COPYING 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 . + */ + +#define TRAY_RETRY_COUNT 10 +#define TRAY_RETRY_WAIT 2000 + +#include "QSynergyApplication.h" +#include "MainWindow.h" +#include "AppConfig.h" +#include "SetupWizard.h" + +#include +#include +#include + +class QThreadImpl : public QThread +{ +public: + static void msleep(unsigned long msecs) + { + QThread::msleep(msecs); + } +}; + +int waitForTray(); + +int main(int argc, char* argv[]) +{ + QCoreApplication::setOrganizationName("Synergy"); + QCoreApplication::setOrganizationDomain("http://synergy-foss.org/"); + QCoreApplication::setApplicationName("Synergy"); + + QSynergyApplication app(argc, argv); + + if (!waitForTray()) + return -1; + + QApplication::setQuitOnLastWindowClosed(false); + + QTranslator qtTranslator; + qtTranslator.load("qt_" + QLocale::system().name(), + QLibraryInfo::location(QLibraryInfo::TranslationsPath)); + app.installTranslator(&qtTranslator); + + QTranslator synergyTranslator; + synergyTranslator.load("res/lang/" + QLocale::system().name()); + app.installTranslator(&synergyTranslator); + + QSettings settings; + AppConfig appConfig(&settings); + MainWindow mainWindow(settings, appConfig); + SetupWizard setupWizard(mainWindow, true); + + if (appConfig.wizardHasRun()) + { + mainWindow.start(false); + } + else + { + setupWizard.show(); + } + + return app.exec(); +} + +int waitForTray() +{ + // on linux, the system tray may not be available immediately after logging in, + // so keep retrying but give up after a short time. + int trayAttempts = 0; + while (true) + { + if (QSystemTrayIcon::isSystemTrayAvailable()) + { + break; + } + + if (++trayAttempts > TRAY_RETRY_COUNT) + { + QMessageBox::critical(NULL, "Synergy", + QObject::tr("System tray is unavailable, quitting.")); + return false; + } + + QThreadImpl::msleep(TRAY_RETRY_WAIT); + } + return true; +} + diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt new file mode 100644 index 00000000..4cbb3c78 --- /dev/null +++ b/src/lib/CMakeLists.txt @@ -0,0 +1,25 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +add_subdirectory(arch) +add_subdirectory(base) +add_subdirectory(client) +add_subdirectory(common) +add_subdirectory(io) +add_subdirectory(mt) +add_subdirectory(net) +add_subdirectory(platform) +add_subdirectory(server) +add_subdirectory(synergy) diff --git a/src/lib/arch/CArch.cpp b/src/lib/arch/CArch.cpp new file mode 100644 index 00000000..0ff78cd6 --- /dev/null +++ b/src/lib/arch/CArch.cpp @@ -0,0 +1,54 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArch.h" + +// +// CArch +// + +CArch* CArch::s_instance = NULL; + +CArch::CArch() +{ +} + +CArch::~CArch() +{ +} + +void +CArch::init() +{ + // initialization that requires ARCH is done here. + ARCH_NETWORK::init(); +#if SYSAPI_WIN32 + ARCH_TASKBAR::init(); + CArchMiscWindows::init(); +#endif +} + +CArch* +CArch::getInstance() +{ + if (s_instance == NULL) { + s_instance = new CArch(); + s_instance->init(); + } + + return s_instance; +} diff --git a/src/lib/arch/CArch.h b/src/lib/arch/CArch.h new file mode 100644 index 00000000..a6efe54c --- /dev/null +++ b/src/lib/arch/CArch.h @@ -0,0 +1,143 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +// TODO: consider whether or not to use either encapsulation (as below) +// or inheritance (as it is now) for the ARCH stuff. +// +// case for encapsulation: +// pros: +// - compiler errors for missing pv implementations are not absolutely bonkers. +// - function names don't have to be so verbose. +// - easier to understand and debug. +// - ctors in IArch implementations can call other implementations. +// cons: +// - slightly more code for calls to ARCH. +// - you'll have to modify each ARCH call. +// +// also, we may want to consider making each encapsulated +// class lazy-loaded so that apps like the daemon don't load +// stuff when they don't need it. + +#ifndef CARCH_H +#define CARCH_H + +#include "common.h" + +#if SYSAPI_WIN32 +# include "CArchConsoleWindows.h" +# include "CArchDaemonWindows.h" +# include "CArchFileWindows.h" +# include "CArchLogWindows.h" +# include "CArchIpcLogWindows.h" +# include "CArchMiscWindows.h" +# include "CArchMultithreadWindows.h" +# include "CArchNetworkWinsock.h" +# include "CArchSleepWindows.h" +# include "CArchStringWindows.h" +# include "CArchSystemWindows.h" +# include "CArchTaskBarWindows.h" +# include "CArchTimeWindows.h" +# include "CArchPluginWindows.h" +#elif SYSAPI_UNIX +# include "CArchConsoleUnix.h" +# include "CArchDaemonUnix.h" +# include "CArchFileUnix.h" +# include "CArchLogUnix.h" +# include "CArchIpcLogUnix.h" +# if HAVE_PTHREAD +# include "CArchMultithreadPosix.h" +# endif +# include "CArchNetworkBSD.h" +# include "CArchSleepUnix.h" +# include "CArchStringUnix.h" +# include "CArchSystemUnix.h" +# include "CArchTaskBarXWindows.h" +# include "CArchTimeUnix.h" +# include "CArchPluginUnix.h" +#endif + +/*! +\def ARCH +This macro evaluates to the singleton CArch object. +*/ +#define ARCH (CArch::getInstance()) + +//! Delegating implementation of architecture dependent interfaces +/*! +This class is a centralized interface to all architecture dependent +interface implementations (except miscellaneous functions). It +instantiates an implementation of each interface and delegates calls +to each method to those implementations. Clients should use the +\c ARCH macro to access this object. Clients must also instantiate +exactly one of these objects before attempting to call any method, +typically at the beginning of \c main(). +*/ +class CArch : public ARCH_CONSOLE, + public ARCH_DAEMON, + public ARCH_FILE, + public ARCH_LOG, + public ARCH_MULTITHREAD, + public ARCH_NETWORK, + public ARCH_SLEEP, + public ARCH_STRING, + public ARCH_SYSTEM, + public ARCH_TASKBAR, + public ARCH_TIME { +public: + ~CArch(); + + // + // accessors + // + + //! Return the singleton instance + /*! + The client must have instantiated exactly once CArch object before + calling this function. + */ + static CArch* getInstance(); + + ARCH_IPC_LOG& ipcLog() const { return (ARCH_IPC_LOG&)m_ipcLog; } + ARCH_PLUGIN& plugin() const { return (ARCH_PLUGIN&)m_plugin; } + +private: + CArch(); + void init(); + +private: + static CArch* s_instance; + ARCH_IPC_LOG m_ipcLog; + ARCH_PLUGIN m_plugin; +}; + +//! Convenience object to lock/unlock an arch mutex +class CArchMutexLock { +public: + CArchMutexLock(CArchMutex mutex) : m_mutex(mutex) + { + ARCH->lockMutex(m_mutex); + } + ~CArchMutexLock() + { + ARCH->unlockMutex(m_mutex); + } + +private: + CArchMutex m_mutex; +}; + +#endif diff --git a/src/lib/arch/CArchConsoleStd.cpp b/src/lib/arch/CArchConsoleStd.cpp new file mode 100644 index 00000000..b579bc0a --- /dev/null +++ b/src/lib/arch/CArchConsoleStd.cpp @@ -0,0 +1,31 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchConsoleStd.h" +#include "CLog.h" +#include + +void +CArchConsoleStd::writeConsole(ELevel level, const char* str) +{ + if ((level >= kFATAL) && (level <= kWARNING)) + std::cerr << str << std::endl; + else + std::cout << str << std::endl; + + std::cout.flush(); +} \ No newline at end of file diff --git a/src/lib/arch/CArchConsoleStd.h b/src/lib/arch/CArchConsoleStd.h new file mode 100644 index 00000000..1863d2a3 --- /dev/null +++ b/src/lib/arch/CArchConsoleStd.h @@ -0,0 +1,33 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IArchConsole.h" + +//! Cross platform implementation of IArchConsole +class CArchConsoleStd : public IArchConsole { +public: + CArchConsoleStd() { } + virtual ~CArchConsoleStd() { } + + // IArchConsole overrides + virtual void openConsole(const char* title) { } + virtual void closeConsole() { } + virtual void showConsole(bool) { } + virtual void writeConsole(ELevel level, const char*); +}; diff --git a/src/lib/arch/CArchConsoleUnix.cpp b/src/lib/arch/CArchConsoleUnix.cpp new file mode 100644 index 00000000..4f934f74 --- /dev/null +++ b/src/lib/arch/CArchConsoleUnix.cpp @@ -0,0 +1,22 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchConsoleUnix.h" + +CArchConsoleUnix::CArchConsoleUnix() { } + +CArchConsoleUnix::~CArchConsoleUnix() { } diff --git a/src/lib/arch/CArchConsoleUnix.h b/src/lib/arch/CArchConsoleUnix.h new file mode 100644 index 00000000..18141d07 --- /dev/null +++ b/src/lib/arch/CArchConsoleUnix.h @@ -0,0 +1,28 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchConsoleStd.h" + +#define ARCH_CONSOLE CArchConsoleUnix + +class CArchConsoleUnix : public CArchConsoleStd { +public: + CArchConsoleUnix(); + virtual ~CArchConsoleUnix(); +}; diff --git a/src/lib/arch/CArchConsoleWindows.cpp b/src/lib/arch/CArchConsoleWindows.cpp new file mode 100644 index 00000000..33315573 --- /dev/null +++ b/src/lib/arch/CArchConsoleWindows.cpp @@ -0,0 +1,22 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchConsoleWindows.h" + +CArchConsoleWindows::CArchConsoleWindows() { } + +CArchConsoleWindows::~CArchConsoleWindows() { } diff --git a/src/lib/arch/CArchConsoleWindows.h b/src/lib/arch/CArchConsoleWindows.h new file mode 100644 index 00000000..f86f7501 --- /dev/null +++ b/src/lib/arch/CArchConsoleWindows.h @@ -0,0 +1,28 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchConsoleStd.h" + +#define ARCH_CONSOLE CArchConsoleWindows + +class CArchConsoleWindows : public CArchConsoleStd { +public: + CArchConsoleWindows(); + virtual ~CArchConsoleWindows(); +}; diff --git a/src/lib/arch/CArchDaemonNone.cpp b/src/lib/arch/CArchDaemonNone.cpp new file mode 100644 index 00000000..65d375f6 --- /dev/null +++ b/src/lib/arch/CArchDaemonNone.cpp @@ -0,0 +1,85 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchDaemonNone.h" + +// +// CArchDaemonNone +// + +CArchDaemonNone::CArchDaemonNone() +{ + // do nothing +} + +CArchDaemonNone::~CArchDaemonNone() +{ + // do nothing +} + +void +CArchDaemonNone::installDaemon(const char*, + const char*, + const char*, + const char*, + const char*, + bool) +{ + // do nothing +} + +void +CArchDaemonNone::uninstallDaemon(const char*, bool) +{ + // do nothing +} + +int +CArchDaemonNone::daemonize(const char* name, DaemonFunc func) +{ + // simply forward the call to func. obviously, this doesn't + // do any daemonizing. + return func(1, &name); +} + +bool +CArchDaemonNone::canInstallDaemon(const char*, bool) +{ + return false; +} + +bool +CArchDaemonNone::isDaemonInstalled(const char*, bool) +{ + return false; +} + +void +CArchDaemonNone::installDaemon() +{ +} + +void +CArchDaemonNone::uninstallDaemon() +{ +} + +std::string +CArchDaemonNone::commandLine() const +{ + return ""; +} diff --git a/src/lib/arch/CArchDaemonNone.h b/src/lib/arch/CArchDaemonNone.h new file mode 100644 index 00000000..8e942b0f --- /dev/null +++ b/src/lib/arch/CArchDaemonNone.h @@ -0,0 +1,53 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHDAEMONNONE_H +#define CARCHDAEMONNONE_H + +#include "IArchDaemon.h" + +#define ARCH_DAEMON CArchDaemonNone + +//! Dummy implementation of IArchDaemon +/*! +This class implements IArchDaemon for a platform that does not have +daemons. The install and uninstall functions do nothing, the query +functions return false, and \c daemonize() simply calls the passed +function and returns its result. +*/ +class CArchDaemonNone : public IArchDaemon { +public: + CArchDaemonNone(); + virtual ~CArchDaemonNone(); + + // IArchDaemon overrides + virtual void installDaemon(const char* name, + const char* description, + const char* pathname, + const char* commandLine, + const char* dependencies, + bool allUsers); + virtual void uninstallDaemon(const char* name, bool allUsers); + virtual int daemonize(const char* name, DaemonFunc func); + virtual bool canInstallDaemon(const char* name, bool allUsers); + virtual bool isDaemonInstalled(const char* name, bool allUsers); + virtual void installDaemon(); + virtual void uninstallDaemon(); + virtual std::string commandLine() const; +}; + +#endif diff --git a/src/lib/arch/CArchDaemonUnix.cpp b/src/lib/arch/CArchDaemonUnix.cpp new file mode 100644 index 00000000..cc6c986a --- /dev/null +++ b/src/lib/arch/CArchDaemonUnix.cpp @@ -0,0 +1,127 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchDaemonUnix.h" +#include "XArchUnix.h" +#include +#include +#include +#include +#include +#include +#include "CLog.h" + +// +// CArchDaemonUnix +// + +CArchDaemonUnix::CArchDaemonUnix() +{ + // do nothing +} + +CArchDaemonUnix::~CArchDaemonUnix() +{ + // do nothing +} + + +#ifdef __APPLE__ + +// In Mac OS X, fork()'d child processes can't use most APIs (the frameworks +// that Synergy uses in fact prevent it and make the process just up and die), +// so need to exec a copy of the program that doesn't fork so isn't limited. +int +execSelfNonDaemonized() +{ + extern char** NXArgv; + char** selfArgv = NXArgv; + + setenv("_SYNERGY_DAEMONIZED", "", 1); + + execvp(selfArgv[0], selfArgv); + return 0; +} + +bool alreadyDaemonized() { + return getenv("_SYNERGY_DAEMONIZED") != NULL; +} + +#endif + +int +CArchDaemonUnix::daemonize(const char* name, DaemonFunc func) +{ +#ifdef __APPLE__ + if (alreadyDaemonized()) + return func(1, &name); +#endif + + // fork so shell thinks we're done and so we're not a process + // group leader + switch (fork()) { + case -1: + // failed + throw XArchDaemonFailed(new XArchEvalUnix(errno)); + + case 0: + // child + break; + + default: + // parent exits + exit(0); + } + + // become leader of a new session + setsid(); + +#ifndef __APPLE__ + // NB: don't run chdir on apple; causes strange behaviour. + // chdir to root so we don't keep mounted filesystems points busy + // TODO: this is a bit of a hack - can we find a better solution? + int chdirErr = chdir("/"); + if (chdirErr) + // NB: file logging actually isn't working at this point! + LOG((CLOG_ERR "chdir error: %i", chdirErr)); +#endif + + // mask off permissions for any but owner + umask(077); + + // close open files. we only expect stdin, stdout, stderr to be open. + close(0); + close(1); + close(2); + + // attach file descriptors 0, 1, 2 to /dev/null so inadvertent use + // of standard I/O safely goes in the bit bucket. + open("/dev/null", O_RDONLY); + open("/dev/null", O_RDWR); + + int dupErr = dup(1); + if (dupErr) + // NB: file logging actually isn't working at this point! + LOG((CLOG_ERR "dup error: %i", dupErr)); + +#ifdef __APPLE__ + return execSelfNonDaemonized(); +#endif + + // invoke function + return func(1, &name); +} diff --git a/src/lib/arch/CArchDaemonUnix.h b/src/lib/arch/CArchDaemonUnix.h new file mode 100644 index 00000000..8c2b9946 --- /dev/null +++ b/src/lib/arch/CArchDaemonUnix.h @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHDAEMONUNIX_H +#define CARCHDAEMONUNIX_H + +#include "CArchDaemonNone.h" + +#undef ARCH_DAEMON +#define ARCH_DAEMON CArchDaemonUnix + +//! Unix implementation of IArchDaemon +class CArchDaemonUnix : public CArchDaemonNone { +public: + CArchDaemonUnix(); + virtual ~CArchDaemonUnix(); + + // IArchDaemon overrides + virtual int daemonize(const char* name, DaemonFunc func); +}; + +#define CONFIG_FILE "/etc/synergy/synergyd.conf" + +#endif diff --git a/src/lib/arch/CArchDaemonWindows.cpp b/src/lib/arch/CArchDaemonWindows.cpp new file mode 100644 index 00000000..04cb3084 --- /dev/null +++ b/src/lib/arch/CArchDaemonWindows.cpp @@ -0,0 +1,841 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchDaemonWindows.h" +#include "CArch.h" +#include "CArchMiscWindows.h" +#include "XArchWindows.h" +#include "stdvector.h" + +// +// CArchDaemonWindows +// + +CArchDaemonWindows* CArchDaemonWindows::s_daemon = NULL; + +CArchDaemonWindows::CArchDaemonWindows() : +m_daemonThreadID(0) +{ + m_quitMessage = RegisterWindowMessage("SynergyDaemonExit"); +} + +CArchDaemonWindows::~CArchDaemonWindows() +{ + // do nothing +} + +int +CArchDaemonWindows::runDaemon(RunFunc runFunc) +{ + assert(s_daemon != NULL); + + return s_daemon->doRunDaemon(runFunc); +} + +void +CArchDaemonWindows::daemonRunning(bool running) +{ + // if s_daemon is NULL we assume we're running on the windows + // 95 family and we just ignore this call so the caller doesn't + // have to go through the trouble of not calling it on the + // windows 95 family. + if (s_daemon != NULL) { + s_daemon->doDaemonRunning(running); + } +} + +UINT +CArchDaemonWindows::getDaemonQuitMessage() +{ + if (s_daemon != NULL) { + return s_daemon->doGetDaemonQuitMessage(); + } + else { + return 0; + } +} + +void +CArchDaemonWindows::daemonFailed(int result) +{ + // if s_daemon is NULL we assume we're running on the windows + // 95 family and we just ignore this call so the caller doesn't + // have to go through the trouble of not calling it on the + // windows 95 family. + if (s_daemon != NULL) { + throw XArchDaemonRunFailed(result); + } +} + +void +CArchDaemonWindows::installDaemon(const char* name, + const char* description, + const char* pathname, + const char* commandLine, + const char* dependencies, + bool allUsers) +{ + // if not for all users then use the user's autostart registry. + // key. if windows 95 family then use windows 95 services key. + if (!allUsers || CArchMiscWindows::isWindows95Family()) { + // open registry + HKEY key = (allUsers && CArchMiscWindows::isWindows95Family()) ? + open95ServicesKey() : openUserStartupKey(); + if (key == NULL) { + // can't open key + throw XArchDaemonInstallFailed(new XArchEvalWindows); + } + + // construct entry + std::string value; + value += "\""; + value += pathname; + value += "\" "; + value += commandLine; + + // install entry + CArchMiscWindows::setValue(key, name, value); + + // clean up + CArchMiscWindows::closeKey(key); + } + + // windows NT family services + else { + // open service manager + SC_HANDLE mgr = OpenSCManager(NULL, NULL, GENERIC_WRITE); + if (mgr == NULL) { + // can't open service manager + throw XArchDaemonInstallFailed(new XArchEvalWindows); + } + + // create the service + SC_HANDLE service = CreateService(mgr, + name, + name, + 0, + SERVICE_WIN32_OWN_PROCESS | + SERVICE_INTERACTIVE_PROCESS, + SERVICE_AUTO_START, + SERVICE_ERROR_NORMAL, + pathname, + NULL, + NULL, + dependencies, + NULL, + NULL); + if (service == NULL) { + // can't create service + DWORD err = GetLastError(); + if (err != ERROR_SERVICE_EXISTS) { + CloseServiceHandle(mgr); + throw XArchDaemonInstallFailed(new XArchEvalWindows(err)); + } + } + else { + // done with service (but only try to close if not null) + CloseServiceHandle(service); + } + + // done with manager + CloseServiceHandle(mgr); + + // open the registry key for this service + HKEY key = openNTServicesKey(); + key = CArchMiscWindows::addKey(key, name); + if (key == NULL) { + // can't open key + DWORD err = GetLastError(); + try { + uninstallDaemon(name, allUsers); + } + catch (...) { + // ignore + } + throw XArchDaemonInstallFailed(new XArchEvalWindows(err)); + } + + // set the description + CArchMiscWindows::setValue(key, _T("Description"), description); + + // set command line + key = CArchMiscWindows::addKey(key, _T("Parameters")); + if (key == NULL) { + // can't open key + DWORD err = GetLastError(); + CArchMiscWindows::closeKey(key); + try { + uninstallDaemon(name, allUsers); + } + catch (...) { + // ignore + } + throw XArchDaemonInstallFailed(new XArchEvalWindows(err)); + } + CArchMiscWindows::setValue(key, _T("CommandLine"), commandLine); + + // done with registry + CArchMiscWindows::closeKey(key); + } +} + +void +CArchDaemonWindows::uninstallDaemon(const char* name, bool allUsers) +{ + // if not for all users then use the user's autostart registry. + // key. if windows 95 family then use windows 95 services key. + if (!allUsers || CArchMiscWindows::isWindows95Family()) { + // open registry + HKEY key = (allUsers && CArchMiscWindows::isWindows95Family()) ? + open95ServicesKey() : openUserStartupKey(); + if (key == NULL) { + // can't open key. daemon is probably not installed. + throw XArchDaemonUninstallNotInstalled(new XArchEvalWindows); + } + + // remove entry + CArchMiscWindows::deleteValue(key, name); + + // clean up + CArchMiscWindows::closeKey(key); + } + + // windows NT family services + else { + // remove parameters for this service. ignore failures. + HKEY key = openNTServicesKey(); + key = CArchMiscWindows::openKey(key, name); + if (key != NULL) { + CArchMiscWindows::deleteKey(key, _T("Parameters")); + CArchMiscWindows::closeKey(key); + } + + // open service manager + SC_HANDLE mgr = OpenSCManager(NULL, NULL, GENERIC_WRITE); + if (mgr == NULL) { + // can't open service manager + throw XArchDaemonUninstallFailed(new XArchEvalWindows); + } + + // open the service. oddly, you must open a service to delete it. + SC_HANDLE service = OpenService(mgr, name, DELETE | SERVICE_STOP); + if (service == NULL) { + DWORD err = GetLastError(); + CloseServiceHandle(mgr); + if (err != ERROR_SERVICE_DOES_NOT_EXIST) { + throw XArchDaemonUninstallFailed(new XArchEvalWindows(err)); + } + throw XArchDaemonUninstallNotInstalled(new XArchEvalWindows(err)); + } + + // stop the service. we don't care if we fail. + SERVICE_STATUS status; + ControlService(service, SERVICE_CONTROL_STOP, &status); + + // delete the service + const bool okay = (DeleteService(service) == 0); + const DWORD err = GetLastError(); + + // clean up + CloseServiceHandle(service); + CloseServiceHandle(mgr); + + // give windows a chance to remove the service before + // we check if it still exists. + ARCH->sleep(1); + + // handle failure. ignore error if service isn't installed anymore. + if (!okay && isDaemonInstalled(name, allUsers)) { + if (err == ERROR_SUCCESS) { + // this seems to occur even though the uninstall was successful. + // it could be a timing issue, i.e., isDaemonInstalled is + // called too soon. i've added a sleep to try and stop this. + return; + } + if (err == ERROR_IO_PENDING) { + // this seems to be a spurious error + return; + } + if (err != ERROR_SERVICE_MARKED_FOR_DELETE) { + throw XArchDaemonUninstallFailed(new XArchEvalWindows(err)); + } + throw XArchDaemonUninstallNotInstalled(new XArchEvalWindows(err)); + } + } +} + +int +CArchDaemonWindows::daemonize(const char* name, DaemonFunc func) +{ + assert(name != NULL); + assert(func != NULL); + + // windows 95 family services + if (CArchMiscWindows::isWindows95Family()) { + typedef DWORD (WINAPI *RegisterServiceProcessT)(DWORD, DWORD); + + // mark this process as a service so it's not killed when the + // user logs off. + HINSTANCE kernel = LoadLibrary("kernel32.dll"); + if (kernel == NULL) { + throw XArchDaemonFailed(new XArchEvalWindows); + } + RegisterServiceProcessT RegisterServiceProcess = + reinterpret_cast( + GetProcAddress(kernel, + "RegisterServiceProcess")); + if (RegisterServiceProcess == NULL) { + // missing RegisterServiceProcess function + DWORD err = GetLastError(); + FreeLibrary(kernel); + throw XArchDaemonFailed(new XArchEvalWindows(err)); + } + if (RegisterServiceProcess(0, 1) == 0) { + // RegisterServiceProcess failed + DWORD err = GetLastError(); + FreeLibrary(kernel); + throw XArchDaemonFailed(new XArchEvalWindows(err)); + } + FreeLibrary(kernel); + + // now simply call the daemon function + return func(1, &name); + } + + // windows NT family services + else { + // save daemon function + m_daemonFunc = func; + + // construct the service entry + SERVICE_TABLE_ENTRY entry[2]; + entry[0].lpServiceName = const_cast(name); + entry[0].lpServiceProc = &CArchDaemonWindows::serviceMainEntry; + entry[1].lpServiceName = NULL; + entry[1].lpServiceProc = NULL; + + // hook us up to the service control manager. this won't return + // (if successful) until the processes have terminated. + s_daemon = this; + if (StartServiceCtrlDispatcher(entry) == 0) { + // StartServiceCtrlDispatcher failed + s_daemon = NULL; + throw XArchDaemonFailed(new XArchEvalWindows); + } + + s_daemon = NULL; + return m_daemonResult; + } +} + +bool +CArchDaemonWindows::canInstallDaemon(const char* /*name*/, bool allUsers) +{ + // if not for all users then use the user's autostart registry. + // key. if windows 95 family then use windows 95 services key. + if (!allUsers || CArchMiscWindows::isWindows95Family()) { + // check if we can open the registry key + HKEY key = (allUsers && CArchMiscWindows::isWindows95Family()) ? + open95ServicesKey() : openUserStartupKey(); + CArchMiscWindows::closeKey(key); + return (key != NULL); + } + + // windows NT family services + else { + // check if we can open service manager for write + SC_HANDLE mgr = OpenSCManager(NULL, NULL, GENERIC_WRITE); + if (mgr == NULL) { + return false; + } + CloseServiceHandle(mgr); + + // check if we can open the registry key + HKEY key = openNTServicesKey(); +// key = CArchMiscWindows::addKey(key, name); +// key = CArchMiscWindows::addKey(key, _T("Parameters")); + CArchMiscWindows::closeKey(key); + + return (key != NULL); + } +} + +bool +CArchDaemonWindows::isDaemonInstalled(const char* name, bool allUsers) +{ + // open service manager + SC_HANDLE mgr = OpenSCManager(NULL, NULL, GENERIC_READ); + if (mgr == NULL) { + return false; + } + + // open the service + SC_HANDLE service = OpenService(mgr, name, GENERIC_READ); + + // clean up + if (service != NULL) { + CloseServiceHandle(service); + } + CloseServiceHandle(mgr); + + return (service != NULL); +} + +HKEY +CArchDaemonWindows::openNTServicesKey() +{ + static const char* s_keyNames[] = { + _T("SYSTEM"), + _T("CurrentControlSet"), + _T("Services"), + NULL + }; + + return CArchMiscWindows::addKey(HKEY_LOCAL_MACHINE, s_keyNames); +} + +HKEY +CArchDaemonWindows::open95ServicesKey() +{ + static const char* s_keyNames[] = { + _T("Software"), + _T("Microsoft"), + _T("Windows"), + _T("CurrentVersion"), + _T("RunServices"), + NULL + }; + + return CArchMiscWindows::addKey(HKEY_LOCAL_MACHINE, s_keyNames); +} + +HKEY +CArchDaemonWindows::openUserStartupKey() +{ + static const char* s_keyNames[] = { + _T("Software"), + _T("Microsoft"), + _T("Windows"), + _T("CurrentVersion"), + _T("Run"), + NULL + }; + + return CArchMiscWindows::addKey(HKEY_CURRENT_USER, s_keyNames); +} + +bool +CArchDaemonWindows::isRunState(DWORD state) +{ + switch (state) { + case SERVICE_START_PENDING: + case SERVICE_CONTINUE_PENDING: + case SERVICE_RUNNING: + return true; + + default: + return false; + } +} + +int +CArchDaemonWindows::doRunDaemon(RunFunc run) +{ + // should only be called from DaemonFunc + assert(m_serviceMutex != NULL); + assert(run != NULL); + + // create message queue for this thread + MSG dummy; + PeekMessage(&dummy, NULL, 0, 0, PM_NOREMOVE); + + int result = 0; + ARCH->lockMutex(m_serviceMutex); + m_daemonThreadID = GetCurrentThreadId(); + while (m_serviceState != SERVICE_STOPPED) { + // wait until we're told to start + while (!isRunState(m_serviceState) && + m_serviceState != SERVICE_STOP_PENDING) { + ARCH->waitCondVar(m_serviceCondVar, m_serviceMutex, -1.0); + } + + // run unless told to stop + if (m_serviceState != SERVICE_STOP_PENDING) { + ARCH->unlockMutex(m_serviceMutex); + try { + result = run(); + } + catch (...) { + ARCH->lockMutex(m_serviceMutex); + setStatusError(0); + m_serviceState = SERVICE_STOPPED; + setStatus(m_serviceState); + ARCH->broadcastCondVar(m_serviceCondVar); + ARCH->unlockMutex(m_serviceMutex); + throw; + } + ARCH->lockMutex(m_serviceMutex); + } + + // notify of new state + if (m_serviceState == SERVICE_PAUSE_PENDING) { + m_serviceState = SERVICE_PAUSED; + } + else { + m_serviceState = SERVICE_STOPPED; + } + setStatus(m_serviceState); + ARCH->broadcastCondVar(m_serviceCondVar); + } + ARCH->unlockMutex(m_serviceMutex); + return result; +} + +void +CArchDaemonWindows::doDaemonRunning(bool running) +{ + ARCH->lockMutex(m_serviceMutex); + if (running) { + m_serviceState = SERVICE_RUNNING; + setStatus(m_serviceState); + ARCH->broadcastCondVar(m_serviceCondVar); + } + ARCH->unlockMutex(m_serviceMutex); +} + +UINT +CArchDaemonWindows::doGetDaemonQuitMessage() +{ + return m_quitMessage; +} + +void +CArchDaemonWindows::setStatus(DWORD state) +{ + setStatus(state, 0, 0); +} + +void +CArchDaemonWindows::setStatus(DWORD state, DWORD step, DWORD waitHint) +{ + assert(s_daemon != NULL); + + SERVICE_STATUS status; + status.dwServiceType = SERVICE_WIN32_OWN_PROCESS | + SERVICE_INTERACTIVE_PROCESS; + status.dwCurrentState = state; + status.dwControlsAccepted = SERVICE_ACCEPT_STOP | + SERVICE_ACCEPT_PAUSE_CONTINUE | + SERVICE_ACCEPT_SHUTDOWN; + status.dwWin32ExitCode = NO_ERROR; + status.dwServiceSpecificExitCode = 0; + status.dwCheckPoint = step; + status.dwWaitHint = waitHint; + SetServiceStatus(s_daemon->m_statusHandle, &status); +} + +void +CArchDaemonWindows::setStatusError(DWORD error) +{ + assert(s_daemon != NULL); + + SERVICE_STATUS status; + status.dwServiceType = SERVICE_WIN32_OWN_PROCESS | + SERVICE_INTERACTIVE_PROCESS; + status.dwCurrentState = SERVICE_STOPPED; + status.dwControlsAccepted = SERVICE_ACCEPT_STOP | + SERVICE_ACCEPT_PAUSE_CONTINUE | + SERVICE_ACCEPT_SHUTDOWN; + status.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR; + status.dwServiceSpecificExitCode = error; + status.dwCheckPoint = 0; + status.dwWaitHint = 0; + SetServiceStatus(s_daemon->m_statusHandle, &status); +} + +void +CArchDaemonWindows::serviceMain(DWORD argc, LPTSTR* argvIn) +{ + typedef std::vector ArgList; + typedef std::vector Arguments; + const char** argv = const_cast(argvIn); + + // create synchronization objects + m_serviceMutex = ARCH->newMutex(); + m_serviceCondVar = ARCH->newCondVar(); + + // register our service handler function + m_statusHandle = RegisterServiceCtrlHandler(argv[0], + &CArchDaemonWindows::serviceHandlerEntry); + if (m_statusHandle == 0) { + // cannot start as service + m_daemonResult = -1; + ARCH->closeCondVar(m_serviceCondVar); + ARCH->closeMutex(m_serviceMutex); + return; + } + + // tell service control manager that we're starting + m_serviceState = SERVICE_START_PENDING; + setStatus(m_serviceState, 0, 10000); + + std::string commandLine; + + // if no arguments supplied then try getting them from the registry. + // the first argument doesn't count because it's the service name. + Arguments args; + ArgList myArgv; + if (argc <= 1) { + // read command line + HKEY key = openNTServicesKey(); + key = CArchMiscWindows::openKey(key, argvIn[0]); + key = CArchMiscWindows::openKey(key, _T("Parameters")); + if (key != NULL) { + commandLine = CArchMiscWindows::readValueString(key, + _T("CommandLine")); + } + + // if the command line isn't empty then parse and use it + if (!commandLine.empty()) { + // parse, honoring double quoted substrings + std::string::size_type i = commandLine.find_first_not_of(" \t"); + while (i != std::string::npos && i != commandLine.size()) { + // find end of string + std::string::size_type e; + if (commandLine[i] == '\"') { + // quoted. find closing quote. + ++i; + e = commandLine.find("\"", i); + + // whitespace must follow closing quote + if (e == std::string::npos || + (e + 1 != commandLine.size() && + commandLine[e + 1] != ' ' && + commandLine[e + 1] != '\t')) { + args.clear(); + break; + } + + // extract + args.push_back(commandLine.substr(i, e - i)); + i = e + 1; + } + else { + // unquoted. find next whitespace. + e = commandLine.find_first_of(" \t", i); + if (e == std::string::npos) { + e = commandLine.size(); + } + + // extract + args.push_back(commandLine.substr(i, e - i)); + i = e + 1; + } + + // next argument + i = commandLine.find_first_not_of(" \t", i); + } + + // service name goes first + myArgv.push_back(argv[0]); + + // get pointers + for (size_t j = 0; j < args.size(); ++j) { + myArgv.push_back(args[j].c_str()); + } + + // adjust argc/argv + argc = (DWORD)myArgv.size(); + argv = &myArgv[0]; + } + } + + m_commandLine = commandLine; + + try { + // invoke daemon function + m_daemonResult = m_daemonFunc(static_cast(argc), argv); + } + catch (XArchDaemonRunFailed& e) { + setStatusError(e.m_result); + m_daemonResult = -1; + } + catch (...) { + setStatusError(1); + m_daemonResult = -1; + } + + // clean up + ARCH->closeCondVar(m_serviceCondVar); + ARCH->closeMutex(m_serviceMutex); + + // we're going to exit now, so set status to stopped + m_serviceState = SERVICE_STOPPED; + setStatus(m_serviceState, 0, 10000); +} + +void WINAPI +CArchDaemonWindows::serviceMainEntry(DWORD argc, LPTSTR* argv) +{ + s_daemon->serviceMain(argc, argv); +} + +void +CArchDaemonWindows::serviceHandler(DWORD ctrl) +{ + assert(m_serviceMutex != NULL); + assert(m_serviceCondVar != NULL); + + ARCH->lockMutex(m_serviceMutex); + + // ignore request if service is already stopped + if (s_daemon == NULL || m_serviceState == SERVICE_STOPPED) { + if (s_daemon != NULL) { + setStatus(m_serviceState); + } + ARCH->unlockMutex(m_serviceMutex); + return; + } + + switch (ctrl) { + case SERVICE_CONTROL_PAUSE: + m_serviceState = SERVICE_PAUSE_PENDING; + setStatus(m_serviceState, 0, 5000); + PostThreadMessage(m_daemonThreadID, m_quitMessage, 0, 0); + while (isRunState(m_serviceState)) { + ARCH->waitCondVar(m_serviceCondVar, m_serviceMutex, -1.0); + } + break; + + case SERVICE_CONTROL_CONTINUE: + // FIXME -- maybe should flush quit messages from queue + m_serviceState = SERVICE_CONTINUE_PENDING; + setStatus(m_serviceState, 0, 5000); + ARCH->broadcastCondVar(m_serviceCondVar); + break; + + case SERVICE_CONTROL_STOP: + case SERVICE_CONTROL_SHUTDOWN: + m_serviceState = SERVICE_STOP_PENDING; + setStatus(m_serviceState, 0, 5000); + PostThreadMessage(m_daemonThreadID, m_quitMessage, 0, 0); + ARCH->broadcastCondVar(m_serviceCondVar); + while (isRunState(m_serviceState)) { + ARCH->waitCondVar(m_serviceCondVar, m_serviceMutex, -1.0); + } + break; + + default: + // unknown service command + // fall through + + case SERVICE_CONTROL_INTERROGATE: + setStatus(m_serviceState); + break; + } + + ARCH->unlockMutex(m_serviceMutex); +} + +void WINAPI +CArchDaemonWindows::serviceHandlerEntry(DWORD ctrl) +{ + s_daemon->serviceHandler(ctrl); +} + +void +CArchDaemonWindows::start(const char* name) +{ + // open service manager + SC_HANDLE mgr = OpenSCManager(NULL, NULL, GENERIC_READ); + if (mgr == NULL) { + throw XArchDaemonFailed(new XArchEvalWindows()); + } + + // open the service + SC_HANDLE service = OpenService( + mgr, name, SERVICE_START); + + if (service == NULL) { + CloseServiceHandle(mgr); + throw XArchDaemonFailed(new XArchEvalWindows()); + } + + // start the service + if (!StartService(service, 0, NULL)) { + throw XArchDaemonFailed(new XArchEvalWindows()); + } +} + +void +CArchDaemonWindows::stop(const char* name) +{ + // open service manager + SC_HANDLE mgr = OpenSCManager(NULL, NULL, GENERIC_READ); + if (mgr == NULL) { + throw XArchDaemonFailed(new XArchEvalWindows()); + } + + // open the service + SC_HANDLE service = OpenService( + mgr, name, + SERVICE_STOP | SERVICE_QUERY_STATUS); + + if (service == NULL) { + CloseServiceHandle(mgr); + throw XArchDaemonFailed(new XArchEvalWindows()); + } + + // ask the service to stop, asynchronously + SERVICE_STATUS ss; + if (!ControlService(service, SERVICE_CONTROL_STOP, &ss)) { + DWORD dwErrCode = GetLastError(); + if (dwErrCode != ERROR_SERVICE_NOT_ACTIVE) { + throw XArchDaemonFailed(new XArchEvalWindows()); + } + } +} + +void +CArchDaemonWindows::installDaemon() +{ + // install default daemon if not already installed. + if (!isDaemonInstalled(DEFAULT_DAEMON_NAME, true)) { + char path[MAX_PATH]; + GetModuleFileName(CArchMiscWindows::instanceWin32(), path, MAX_PATH); + installDaemon(DEFAULT_DAEMON_NAME, DEFAULT_DAEMON_INFO, path, "", "", true); + } + + start(DEFAULT_DAEMON_NAME); +} + +void +CArchDaemonWindows::uninstallDaemon() +{ + // remove legacy services if installed. + if (isDaemonInstalled(LEGACY_SERVER_DAEMON_NAME, true)) { + uninstallDaemon(LEGACY_SERVER_DAEMON_NAME, true); + } + if (isDaemonInstalled(LEGACY_CLIENT_DAEMON_NAME, true)) { + uninstallDaemon(LEGACY_CLIENT_DAEMON_NAME, true); + } + + // remove new service if installed. + if (isDaemonInstalled(DEFAULT_DAEMON_NAME, true)) { + uninstallDaemon(DEFAULT_DAEMON_NAME, true); + } +} diff --git a/src/lib/arch/CArchDaemonWindows.h b/src/lib/arch/CArchDaemonWindows.h new file mode 100644 index 00000000..dc699ac8 --- /dev/null +++ b/src/lib/arch/CArchDaemonWindows.h @@ -0,0 +1,159 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHDAEMONWINDOWS_H +#define CARCHDAEMONWINDOWS_H + +#define WIN32_LEAN_AND_MEAN + +#include "IArchDaemon.h" +#include "IArchMultithread.h" +#include "stdstring.h" +#include +#include + +#define ARCH_DAEMON CArchDaemonWindows + +//! Win32 implementation of IArchDaemon +class CArchDaemonWindows : public IArchDaemon { +public: + typedef int (*RunFunc)(void); + + CArchDaemonWindows(); + virtual ~CArchDaemonWindows(); + + //! Run the daemon + /*! + When the client calls \c daemonize(), the \c DaemonFunc should call this + function after initialization and argument parsing to perform the + daemon processing. The \c runFunc should perform the daemon's + main loop, calling \c daemonRunning(true) when it enters the main loop + (i.e. after initialization) and \c daemonRunning(false) when it leaves + the main loop. The \c runFunc is called in a new thread and when the + daemon must exit the main loop due to some external control the + getDaemonQuitMessage() is posted to the thread. This function returns + what \c runFunc returns. \c runFunc should call \c daemonFailed() if + the daemon fails. + */ + static int runDaemon(RunFunc runFunc); + + //! Indicate daemon is in main loop + /*! + The \c runFunc passed to \c runDaemon() should call this function + to indicate when it has entered (\c running is \c true) or exited + (\c running is \c false) the main loop. + */ + static void daemonRunning(bool running); + + //! Indicate failure of running daemon + /*! + The \c runFunc passed to \c runDaemon() should call this function + to indicate failure. \c result is returned by \c daemonize(). + */ + static void daemonFailed(int result); + + //! Get daemon quit message + /*! + The windows NT daemon tells daemon thread to exit by posting this + message to it. The thread must, of course, have a message queue + for this to work. + */ + static UINT getDaemonQuitMessage(); + + // IArchDaemon overrides + virtual void installDaemon(const char* name, + const char* description, + const char* pathname, + const char* commandLine, + const char* dependencies, + bool allUsers); + virtual void uninstallDaemon(const char* name, bool allUsers); + virtual void installDaemon(); + virtual void uninstallDaemon(); + virtual int daemonize(const char* name, DaemonFunc func); + virtual bool canInstallDaemon(const char* name, bool allUsers); + virtual bool isDaemonInstalled(const char* name, bool allUsers); + std::string commandLine() const { return m_commandLine; } + +private: + static HKEY openNTServicesKey(); + static HKEY open95ServicesKey(); + static HKEY openUserStartupKey(); + + int doRunDaemon(RunFunc runFunc); + void doDaemonRunning(bool running); + UINT doGetDaemonQuitMessage(); + + static void setStatus(DWORD state); + static void setStatus(DWORD state, DWORD step, DWORD waitHint); + static void setStatusError(DWORD error); + + static bool isRunState(DWORD state); + + void serviceMain(DWORD, LPTSTR*); + static void WINAPI serviceMainEntry(DWORD, LPTSTR*); + + void serviceHandler(DWORD ctrl); + static void WINAPI serviceHandlerEntry(DWORD ctrl); + + void start(const char* name); + void stop(const char* name); + +private: + class XArchDaemonRunFailed { + public: + XArchDaemonRunFailed(int result) : m_result(result) { } + + public: + int m_result; + }; + +private: + static CArchDaemonWindows* s_daemon; + + CArchMutex m_serviceMutex; + CArchCond m_serviceCondVar; + DWORD m_serviceState; + bool m_serviceHandlerWaiting; + bool m_serviceRunning; + + DWORD m_daemonThreadID; + DaemonFunc m_daemonFunc; + int m_daemonResult; + + SERVICE_STATUS_HANDLE m_statusHandle; + + UINT m_quitMessage; + + std::string m_commandLine; +}; + +#define DEFAULT_DAEMON_NAME _T("Synergy") +#define DEFAULT_DAEMON_INFO _T("Manages the Synergy foreground processes.") + +#define LEGACY_SERVER_DAEMON_NAME _T("Synergy Server") +#define LEGACY_CLIENT_DAEMON_NAME _T("Synergy Client") + +static const TCHAR* const g_daemonKeyPath[] = { + _T("SOFTWARE"), + _T("The Synergy Project"), + _T("Synergy"), + _T("Service"), + NULL +}; + +#endif diff --git a/src/lib/arch/CArchFileUnix.cpp b/src/lib/arch/CArchFileUnix.cpp new file mode 100644 index 00000000..526811b9 --- /dev/null +++ b/src/lib/arch/CArchFileUnix.cpp @@ -0,0 +1,101 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchFileUnix.h" +#include +#include +#include +#include +#include + +// +// CArchFileUnix +// + +CArchFileUnix::CArchFileUnix() +{ + // do nothing +} + +CArchFileUnix::~CArchFileUnix() +{ + // do nothing +} + +const char* +CArchFileUnix::getBasename(const char* pathname) +{ + if (pathname == NULL) { + return NULL; + } + + const char* basename = strrchr(pathname, '/'); + if (basename != NULL) { + return basename + 1; + } + else { + return pathname; + } +} + +std::string +CArchFileUnix::getUserDirectory() +{ + char* buffer = NULL; + std::string dir; +#if HAVE_GETPWUID_R + struct passwd pwent; + struct passwd* pwentp; +#if defined(_SC_GETPW_R_SIZE_MAX) + long size = sysconf(_SC_GETPW_R_SIZE_MAX); + if (size == -1) { + size = BUFSIZ; + } +#else + long size = BUFSIZ; +#endif + buffer = new char[size]; + getpwuid_r(getuid(), &pwent, buffer, size, &pwentp); +#else + struct passwd* pwentp = getpwuid(getuid()); +#endif + if (pwentp != NULL && pwentp->pw_dir != NULL) { + dir = pwentp->pw_dir; + } + delete[] buffer; + return dir; +} + +std::string +CArchFileUnix::getSystemDirectory() +{ + return "/etc"; +} + +std::string +CArchFileUnix::concatPath(const std::string& prefix, + const std::string& suffix) +{ + std::string path; + path.reserve(prefix.size() + 1 + suffix.size()); + path += prefix; + if (path.size() == 0 || path[path.size() - 1] != '/') { + path += '/'; + } + path += suffix; + return path; +} diff --git a/src/lib/arch/CArchFileUnix.h b/src/lib/arch/CArchFileUnix.h new file mode 100644 index 00000000..0c4e9254 --- /dev/null +++ b/src/lib/arch/CArchFileUnix.h @@ -0,0 +1,39 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHFILEUNIX_H +#define CARCHFILEUNIX_H + +#include "IArchFile.h" + +#define ARCH_FILE CArchFileUnix + +//! Unix implementation of IArchFile +class CArchFileUnix : public IArchFile { +public: + CArchFileUnix(); + virtual ~CArchFileUnix(); + + // IArchFile overrides + virtual const char* getBasename(const char* pathname); + virtual std::string getUserDirectory(); + virtual std::string getSystemDirectory(); + virtual std::string concatPath(const std::string& prefix, + const std::string& suffix); +}; + +#endif diff --git a/src/lib/arch/CArchFileWindows.cpp b/src/lib/arch/CArchFileWindows.cpp new file mode 100644 index 00000000..c75752c5 --- /dev/null +++ b/src/lib/arch/CArchFileWindows.cpp @@ -0,0 +1,135 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchFileWindows.h" +#include +#include +#include +#include + +// +// CArchFileWindows +// + +CArchFileWindows::CArchFileWindows() +{ + // do nothing +} + +CArchFileWindows::~CArchFileWindows() +{ + // do nothing +} + +const char* +CArchFileWindows::getBasename(const char* pathname) +{ + if (pathname == NULL) { + return NULL; + } + + // check for last slash + const char* basename = strrchr(pathname, '/'); + if (basename != NULL) { + ++basename; + } + else { + basename = pathname; + } + + // check for last backslash + const char* basename2 = strrchr(pathname, '\\'); + if (basename2 != NULL && basename2 > basename) { + basename = basename2 + 1; + } + + return basename; +} + +std::string +CArchFileWindows::getUserDirectory() +{ + // try %HOMEPATH% + TCHAR dir[MAX_PATH]; + DWORD size = sizeof(dir) / sizeof(TCHAR); + DWORD result = GetEnvironmentVariable(_T("HOMEPATH"), dir, size); + if (result != 0 && result <= size) { + // sanity check -- if dir doesn't appear to start with a + // drive letter and isn't a UNC name then don't use it + // FIXME -- allow UNC names + if (dir[0] != '\0' && (dir[1] == ':' || + ((dir[0] == '\\' || dir[0] == '/') && + (dir[1] == '\\' || dir[1] == '/')))) { + return dir; + } + } + + // get the location of the personal files. that's as close to + // a home directory as we're likely to find. + ITEMIDLIST* idl; + if (SUCCEEDED(SHGetSpecialFolderLocation(NULL, CSIDL_PERSONAL, &idl))) { + TCHAR* path = NULL; + if (SHGetPathFromIDList(idl, dir)) { + DWORD attr = GetFileAttributes(dir); + if (attr != 0xffffffff && (attr & FILE_ATTRIBUTE_DIRECTORY) != 0) + path = dir; + } + + IMalloc* shalloc; + if (SUCCEEDED(SHGetMalloc(&shalloc))) { + shalloc->Free(idl); + shalloc->Release(); + } + + if (path != NULL) { + return path; + } + } + + // use root of C drive as a default + return "C:"; +} + +std::string +CArchFileWindows::getSystemDirectory() +{ + // get windows directory + char dir[MAX_PATH]; + if (GetWindowsDirectory(dir, sizeof(dir)) != 0) { + return dir; + } + else { + // can't get it. use C:\ as a default. + return "C:"; + } +} + +std::string +CArchFileWindows::concatPath(const std::string& prefix, + const std::string& suffix) +{ + std::string path; + path.reserve(prefix.size() + 1 + suffix.size()); + path += prefix; + if (path.size() == 0 || + (path[path.size() - 1] != '\\' && + path[path.size() - 1] != '/')) { + path += '\\'; + } + path += suffix; + return path; +} diff --git a/src/lib/arch/CArchFileWindows.h b/src/lib/arch/CArchFileWindows.h new file mode 100644 index 00000000..a3cf5075 --- /dev/null +++ b/src/lib/arch/CArchFileWindows.h @@ -0,0 +1,39 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHFILEWINDOWS_H +#define CARCHFILEWINDOWS_H + +#include "IArchFile.h" + +#define ARCH_FILE CArchFileWindows + +//! Win32 implementation of IArchFile +class CArchFileWindows : public IArchFile { +public: + CArchFileWindows(); + virtual ~CArchFileWindows(); + + // IArchFile overrides + virtual const char* getBasename(const char* pathname); + virtual std::string getUserDirectory(); + virtual std::string getSystemDirectory(); + virtual std::string concatPath(const std::string& prefix, + const std::string& suffix); +}; + +#endif diff --git a/src/lib/arch/CArchIpcLogUnix.cpp b/src/lib/arch/CArchIpcLogUnix.cpp new file mode 100644 index 00000000..14a44e0e --- /dev/null +++ b/src/lib/arch/CArchIpcLogUnix.cpp @@ -0,0 +1,46 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CArchIpcLogUnix.h" + +CArchIpcLogUnix::CArchIpcLogUnix() +{ +} + +CArchIpcLogUnix::~CArchIpcLogUnix() +{ +} + +void +CArchIpcLogUnix::openLog(const char* name) +{ +} + +void +CArchIpcLogUnix::closeLog() +{ +} + +void +CArchIpcLogUnix::showLog(bool showIfEmpty) +{ +} + +void +CArchIpcLogUnix::writeLog(ELevel, const char*) +{ +} diff --git a/src/lib/arch/CArchIpcLogUnix.h b/src/lib/arch/CArchIpcLogUnix.h new file mode 100644 index 00000000..2f11d02e --- /dev/null +++ b/src/lib/arch/CArchIpcLogUnix.h @@ -0,0 +1,34 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "IArchLog.h" + +#define ARCH_IPC_LOG CArchIpcLogUnix + +class CArchIpcLogUnix : public IArchLog { +public: + CArchIpcLogUnix(); + virtual ~CArchIpcLogUnix(); + + // IArchLog overrides + virtual void openLog(const char* name); + virtual void closeLog(); + virtual void showLog(bool showIfEmpty); + virtual void writeLog(ELevel, const char*); +}; diff --git a/src/lib/arch/CArchIpcLogWindows.cpp b/src/lib/arch/CArchIpcLogWindows.cpp new file mode 100644 index 00000000..8cf02104 --- /dev/null +++ b/src/lib/arch/CArchIpcLogWindows.cpp @@ -0,0 +1,111 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchIpcLogWindows.h" +#include "CArchMiscWindows.h" +#include "XArch.h" +#include "CThread.h" +#include "TMethodJob.h" +#include "CArch.h" + +#define WIN32_LEAN_AND_MEAN +#include + +// +// CArchIpcLogWindows +// + +CArchIpcLogWindows::CArchIpcLogWindows() : + m_pipe(NULL), + m_listenThread(NULL), + m_connected(false) +{ +} + +CArchIpcLogWindows::~CArchIpcLogWindows() +{ + if (m_listenThread != NULL) + delete m_listenThread; +} + +void +CArchIpcLogWindows::openLog(const char* name) +{ + // grant access to everyone. + SECURITY_DESCRIPTOR sd; + InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION); + SetSecurityDescriptorDacl(&sd, TRUE, static_cast(0), FALSE); + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.lpSecurityDescriptor = &sd; + + HANDLE pipe = CreateNamedPipe( + TEXT("\\\\.\\pipe\\SynergyLog"), + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, + PIPE_UNLIMITED_INSTANCES, + 1024, 1024, 0, &sa); + + if (pipe == INVALID_HANDLE_VALUE) + XArch("could not create named pipe."); + + m_pipe = pipe; + + m_listenThread = new CThread(new TMethodJob( + this, &CArchIpcLogWindows::connectThread, nullptr)); +} + +void +CArchIpcLogWindows::connectThread(void*) +{ + // HACK: this seems like a hacky pile of bollocks. it will continuously call + // ConnectNamedPipe every second. if there is no client, it will block, + // but if there is a client it will return FALSE and GetLastError() will + // be ERROR_PIPE_CONNECTED. in any other case, the client has gone away + // and ConnectNamedPipe will go back to blocking (waiting for the client + // to reconnect). + while (true) { + BOOL result = ConnectNamedPipe(m_pipe, NULL); + if ((result == TRUE) || (GetLastError() == ERROR_PIPE_CONNECTED)) { + m_connected = true; + ARCH->sleep(1); + } else { + DisconnectNamedPipe(m_pipe); + } + } +} + +void +CArchIpcLogWindows::closeLog() +{ +} + +void +CArchIpcLogWindows::showLog(bool) +{ +} + +void +CArchIpcLogWindows::writeLog(ELevel level, const char* data) +{ + if (!m_connected) + return; + + DWORD bytesWritten; + WriteFile(m_pipe, data, (DWORD)strlen(data), &bytesWritten, NULL); +} diff --git a/src/lib/arch/CArchIpcLogWindows.h b/src/lib/arch/CArchIpcLogWindows.h new file mode 100644 index 00000000..85158a0e --- /dev/null +++ b/src/lib/arch/CArchIpcLogWindows.h @@ -0,0 +1,48 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 + +#define WIN32_LEAN_AND_MEAN + +#include "IArchLog.h" +#include + +#define ARCH_IPC_LOG CArchIpcLogWindows + +class CThread; + +//! Win32 implementation of IArchLog (IPC version) +class CArchIpcLogWindows : public IArchLog { +public: + CArchIpcLogWindows(); + virtual ~CArchIpcLogWindows(); + + // IArchLog overrides + virtual void openLog(const char* name); + virtual void closeLog(); + virtual void showLog(bool showIfEmpty); + virtual void writeLog(ELevel, const char*); + +private: + void connectThread(void*); + +private: + HANDLE m_pipe; + CThread* m_listenThread; + bool m_connected; +}; diff --git a/src/lib/arch/CArchLogUnix.cpp b/src/lib/arch/CArchLogUnix.cpp new file mode 100644 index 00000000..7e66bb76 --- /dev/null +++ b/src/lib/arch/CArchLogUnix.cpp @@ -0,0 +1,82 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchLogUnix.h" +#include + +// +// CArchLogUnix +// + +CArchLogUnix::CArchLogUnix() +{ + // do nothing +} + +CArchLogUnix::~CArchLogUnix() +{ + // do nothing +} + +void +CArchLogUnix::openLog(const char* name) +{ + openlog(name, 0, LOG_DAEMON); +} + +void +CArchLogUnix::closeLog() +{ + closelog(); +} + +void +CArchLogUnix::showLog(bool) +{ + // do nothing +} + +void +CArchLogUnix::writeLog(ELevel level, const char* msg) +{ + // convert level + int priority; + switch (level) { + case kERROR: + priority = LOG_ERR; + break; + + case kWARNING: + priority = LOG_WARNING; + break; + + case kNOTE: + priority = LOG_NOTICE; + break; + + case kINFO: + priority = LOG_INFO; + break; + + default: + priority = LOG_DEBUG; + break; + } + + // log it + syslog(priority, "%s", msg); +} diff --git a/src/lib/arch/CArchLogUnix.h b/src/lib/arch/CArchLogUnix.h new file mode 100644 index 00000000..800526c5 --- /dev/null +++ b/src/lib/arch/CArchLogUnix.h @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHLOGUNIX_H +#define CARCHLOGUNIX_H + +#include "IArchLog.h" + +#define ARCH_LOG CArchLogUnix + +//! Unix implementation of IArchLog +class CArchLogUnix : public IArchLog { +public: + CArchLogUnix(); + virtual ~CArchLogUnix(); + + // IArchLog overrides + virtual void openLog(const char* name); + virtual void closeLog(); + virtual void showLog(bool); + virtual void writeLog(ELevel, const char*); +}; + +#endif diff --git a/src/lib/arch/CArchLogWindows.cpp b/src/lib/arch/CArchLogWindows.cpp new file mode 100644 index 00000000..fad39e2e --- /dev/null +++ b/src/lib/arch/CArchLogWindows.cpp @@ -0,0 +1,93 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchLogWindows.h" +#include "CArchMiscWindows.h" +#include + +// +// CArchLogWindows +// + +CArchLogWindows::CArchLogWindows() : m_eventLog(NULL) +{ + // do nothing +} + +CArchLogWindows::~CArchLogWindows() +{ + // do nothing +} + +void +CArchLogWindows::openLog(const char* name) +{ + if (m_eventLog == NULL && !CArchMiscWindows::isWindows95Family()) { + m_eventLog = RegisterEventSource(NULL, name); + } +} + +void +CArchLogWindows::closeLog() +{ + if (m_eventLog != NULL) { + DeregisterEventSource(m_eventLog); + m_eventLog = NULL; + } +} + +void +CArchLogWindows::showLog(bool) +{ + // do nothing +} + +void +CArchLogWindows::writeLog(ELevel level, const char* msg) +{ + if (m_eventLog != NULL) { + // convert priority + WORD type; + switch (level) { + case kERROR: + type = EVENTLOG_ERROR_TYPE; + break; + + case kWARNING: + type = EVENTLOG_WARNING_TYPE; + break; + + default: + type = EVENTLOG_INFORMATION_TYPE; + break; + } + + // log it + // FIXME -- win32 wants to use a message table to look up event + // strings. log messages aren't organized that way so we'll + // just dump our string into the raw data section of the event + // so users can at least see the message. note that we use our + // level as the event category. + ReportEvent(m_eventLog, type, static_cast(level), + 0, // event ID + NULL, + 0, + (DWORD)strlen(msg) + 1, // raw data size + NULL, + const_cast(msg));// raw data + } +} diff --git a/src/lib/arch/CArchLogWindows.h b/src/lib/arch/CArchLogWindows.h new file mode 100644 index 00000000..38ab2931 --- /dev/null +++ b/src/lib/arch/CArchLogWindows.h @@ -0,0 +1,44 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHLOGWINDOWS_H +#define CARCHLOGWINDOWS_H + +#define WIN32_LEAN_AND_MEAN + +#include "IArchLog.h" +#include + +#define ARCH_LOG CArchLogWindows + +//! Win32 implementation of IArchLog +class CArchLogWindows : public IArchLog { +public: + CArchLogWindows(); + virtual ~CArchLogWindows(); + + // IArchLog overrides + virtual void openLog(const char* name); + virtual void closeLog(); + virtual void showLog(bool showIfEmpty); + virtual void writeLog(ELevel, const char*); + +private: + HANDLE m_eventLog; +}; + +#endif diff --git a/src/lib/arch/CArchMiscWindows.cpp b/src/lib/arch/CArchMiscWindows.cpp new file mode 100644 index 00000000..1420fe25 --- /dev/null +++ b/src/lib/arch/CArchMiscWindows.cpp @@ -0,0 +1,554 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchMiscWindows.h" +#include "CArchDaemonWindows.h" +#include "CLog.h" + +#include +#pragma warning(disable: 4099) +#include +#pragma warning(default: 4099) +#include "Version.h" + +// parent process name for services in Vista +#define SERVICE_LAUNCHER "services.exe" + +#ifndef ES_SYSTEM_REQUIRED +#define ES_SYSTEM_REQUIRED ((DWORD)0x00000001) +#endif +#ifndef ES_DISPLAY_REQUIRED +#define ES_DISPLAY_REQUIRED ((DWORD)0x00000002) +#endif +#ifndef ES_CONTINUOUS +#define ES_CONTINUOUS ((DWORD)0x80000000) +#endif +typedef DWORD EXECUTION_STATE; + +// +// CArchMiscWindows +// + +CArchMiscWindows::CDialogs* CArchMiscWindows::s_dialogs = NULL; +DWORD CArchMiscWindows::s_busyState = 0; +CArchMiscWindows::STES_t CArchMiscWindows::s_stes = NULL; +HICON CArchMiscWindows::s_largeIcon = NULL; +HICON CArchMiscWindows::s_smallIcon = NULL; +HINSTANCE CArchMiscWindows::s_instanceWin32 = NULL; + +void +CArchMiscWindows::init() +{ + s_dialogs = new CDialogs; + isWindows95Family(); +} + +bool +CArchMiscWindows::isWindows95Family() +{ + static bool init = false; + static bool result = false; + + if (!init) { + OSVERSIONINFO version; + version.dwOSVersionInfoSize = sizeof(version); + if (GetVersionEx(&version) == 0) { + // cannot determine OS; assume windows 95 family + result = true; + } + else { + result = (version.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS); + } + init = true; + } + return result; +} + +bool +CArchMiscWindows::isWindowsModern() +{ + static bool init = false; + static bool result = false; + + if (!init) { + OSVERSIONINFO version; + version.dwOSVersionInfoSize = sizeof(version); + if (GetVersionEx(&version) == 0) { + // cannot determine OS; assume not modern + result = false; + } + else { + result = ((version.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS && + version.dwMajorVersion == 4 && + version.dwMinorVersion > 0) || + (version.dwPlatformId == VER_PLATFORM_WIN32_NT && + version.dwMajorVersion > 4)); + } + init = true; + } + return result; +} + +void +CArchMiscWindows::setIcons(HICON largeIcon, HICON smallIcon) +{ + s_largeIcon = largeIcon; + s_smallIcon = smallIcon; +} + +void +CArchMiscWindows::getIcons(HICON& largeIcon, HICON& smallIcon) +{ + largeIcon = s_largeIcon; + smallIcon = s_smallIcon; +} + +int +CArchMiscWindows::runDaemon(RunFunc runFunc) +{ + return CArchDaemonWindows::runDaemon(runFunc); +} + +void +CArchMiscWindows::daemonRunning(bool running) +{ + CArchDaemonWindows::daemonRunning(running); +} + +void +CArchMiscWindows::daemonFailed(int result) +{ + CArchDaemonWindows::daemonFailed(result); +} + +UINT +CArchMiscWindows::getDaemonQuitMessage() +{ + return CArchDaemonWindows::getDaemonQuitMessage(); +} + +HKEY +CArchMiscWindows::openKey(HKEY key, const TCHAR* keyName) +{ + return openKey(key, keyName, false); +} + +HKEY +CArchMiscWindows::openKey(HKEY key, const TCHAR* const* keyNames) +{ + return openKey(key, keyNames, false); +} + +HKEY +CArchMiscWindows::addKey(HKEY key, const TCHAR* keyName) +{ + return openKey(key, keyName, true); +} + +HKEY +CArchMiscWindows::addKey(HKEY key, const TCHAR* const* keyNames) +{ + return openKey(key, keyNames, true); +} + +HKEY +CArchMiscWindows::openKey(HKEY key, const TCHAR* keyName, bool create) +{ + // ignore if parent is NULL + if (key == NULL) { + return NULL; + } + + // open next key + HKEY newKey; + LONG result = RegOpenKeyEx(key, keyName, 0, + KEY_WRITE | KEY_QUERY_VALUE, &newKey); + if (result != ERROR_SUCCESS && create) { + DWORD disp; + result = RegCreateKeyEx(key, keyName, 0, TEXT(""), + 0, KEY_WRITE | KEY_QUERY_VALUE, + NULL, &newKey, &disp); + } + if (result != ERROR_SUCCESS) { + RegCloseKey(key); + return NULL; + } + + // switch to new key + RegCloseKey(key); + return newKey; +} + +HKEY +CArchMiscWindows::openKey(HKEY key, const TCHAR* const* keyNames, bool create) +{ + for (size_t i = 0; key != NULL && keyNames[i] != NULL; ++i) { + // open next key + key = openKey(key, keyNames[i], create); + } + return key; +} + +void +CArchMiscWindows::closeKey(HKEY key) +{ + assert(key != NULL); + if (key==NULL) return; + RegCloseKey(key); +} + +void +CArchMiscWindows::deleteKey(HKEY key, const TCHAR* name) +{ + assert(key != NULL); + assert(name != NULL); + if (key==NULL || name==NULL) return; + RegDeleteKey(key, name); +} + +void +CArchMiscWindows::deleteValue(HKEY key, const TCHAR* name) +{ + assert(key != NULL); + assert(name != NULL); + if (key==NULL || name==NULL) return; + RegDeleteValue(key, name); +} + +bool +CArchMiscWindows::hasValue(HKEY key, const TCHAR* name) +{ + DWORD type; + LONG result = RegQueryValueEx(key, name, 0, &type, NULL, NULL); + return (result == ERROR_SUCCESS && + (type == REG_DWORD || type == REG_SZ)); +} + +CArchMiscWindows::EValueType +CArchMiscWindows::typeOfValue(HKEY key, const TCHAR* name) +{ + DWORD type; + LONG result = RegQueryValueEx(key, name, 0, &type, NULL, NULL); + if (result != ERROR_SUCCESS) { + return kNO_VALUE; + } + switch (type) { + case REG_DWORD: + return kUINT; + + case REG_SZ: + return kSTRING; + + case REG_BINARY: + return kBINARY; + + default: + return kUNKNOWN; + } +} + +void +CArchMiscWindows::setValue(HKEY key, + const TCHAR* name, const std::string& value) +{ + assert(key != NULL); + assert(name != NULL); + if(key ==NULL || name==NULL) return; // TODO: throw exception + RegSetValueEx(key, name, 0, REG_SZ, + reinterpret_cast(value.c_str()), + (DWORD)value.size() + 1); +} + +void +CArchMiscWindows::setValue(HKEY key, const TCHAR* name, DWORD value) +{ + assert(key != NULL); + assert(name != NULL); + if(key ==NULL || name==NULL) return; // TODO: throw exception + RegSetValueEx(key, name, 0, REG_DWORD, + reinterpret_cast(&value), + sizeof(DWORD)); +} + +void +CArchMiscWindows::setValueBinary(HKEY key, + const TCHAR* name, const std::string& value) +{ + assert(key != NULL); + assert(name != NULL); + if(key ==NULL || name==NULL) return; // TODO: throw exception + RegSetValueEx(key, name, 0, REG_BINARY, + reinterpret_cast(value.data()), + (DWORD)value.size()); +} + +std::string +CArchMiscWindows::readBinaryOrString(HKEY key, const TCHAR* name, DWORD type) +{ + // get the size of the string + DWORD actualType; + DWORD size = 0; + LONG result = RegQueryValueEx(key, name, 0, &actualType, NULL, &size); + if (result != ERROR_SUCCESS || actualType != type) { + return std::string(); + } + + // if zero size then return empty string + if (size == 0) { + return std::string(); + } + + // allocate space + char* buffer = new char[size]; + + // read it + result = RegQueryValueEx(key, name, 0, &actualType, + reinterpret_cast(buffer), &size); + if (result != ERROR_SUCCESS || actualType != type) { + delete[] buffer; + return std::string(); + } + + // clean up and return value + if (type == REG_SZ && buffer[size - 1] == '\0') { + // don't include terminating nul; std::string will add one. + --size; + } + std::string value(buffer, size); + delete[] buffer; + return value; +} + +std::string +CArchMiscWindows::readValueString(HKEY key, const TCHAR* name) +{ + return readBinaryOrString(key, name, REG_SZ); +} + +std::string +CArchMiscWindows::readValueBinary(HKEY key, const TCHAR* name) +{ + return readBinaryOrString(key, name, REG_BINARY); +} + +DWORD +CArchMiscWindows::readValueInt(HKEY key, const TCHAR* name) +{ + DWORD type; + DWORD value; + DWORD size = sizeof(value); + LONG result = RegQueryValueEx(key, name, 0, &type, + reinterpret_cast(&value), &size); + if (result != ERROR_SUCCESS || type != REG_DWORD) { + return 0; + } + return value; +} + +void +CArchMiscWindows::addDialog(HWND hwnd) +{ + s_dialogs->insert(hwnd); +} + +void +CArchMiscWindows::removeDialog(HWND hwnd) +{ + s_dialogs->erase(hwnd); +} + +bool +CArchMiscWindows::processDialog(MSG* msg) +{ + for (CDialogs::const_iterator index = s_dialogs->begin(); + index != s_dialogs->end(); ++index) { + if (IsDialogMessage(*index, msg)) { + return true; + } + } + return false; +} + +void +CArchMiscWindows::addBusyState(DWORD busyModes) +{ + s_busyState |= busyModes; + setThreadExecutionState(s_busyState); +} + +void +CArchMiscWindows::removeBusyState(DWORD busyModes) +{ + s_busyState &= ~busyModes; + setThreadExecutionState(s_busyState); +} + +void +CArchMiscWindows::setThreadExecutionState(DWORD busyModes) +{ + // look up function dynamically so we work on older systems + if (s_stes == NULL) { + HINSTANCE kernel = LoadLibrary("kernel32.dll"); + if (kernel != NULL) { + s_stes = reinterpret_cast(GetProcAddress(kernel, + "SetThreadExecutionState")); + } + if (s_stes == NULL) { + s_stes = &CArchMiscWindows::dummySetThreadExecutionState; + } + } + + // convert to STES form + EXECUTION_STATE state = 0; + if ((busyModes & kSYSTEM) != 0) { + state |= ES_SYSTEM_REQUIRED; + } + if ((busyModes & kDISPLAY) != 0) { + state |= ES_DISPLAY_REQUIRED; + } + if (state != 0) { + state |= ES_CONTINUOUS; + } + + // do it + s_stes(state); +} + +DWORD +CArchMiscWindows::dummySetThreadExecutionState(DWORD) +{ + // do nothing + return 0; +} + +void +CArchMiscWindows::wakeupDisplay() +{ + // We can't use ::setThreadExecutionState here because it sets + // ES_CONTINUOUS, which we don't want. + + if (s_stes == NULL) { + HINSTANCE kernel = LoadLibrary("kernel32.dll"); + if (kernel != NULL) { + s_stes = reinterpret_cast(GetProcAddress(kernel, + "SetThreadExecutionState")); + } + if (s_stes == NULL) { + s_stes = &CArchMiscWindows::dummySetThreadExecutionState; + } + } + + s_stes(ES_DISPLAY_REQUIRED); + + // restore the original execution states + setThreadExecutionState(s_busyState); +} + +bool +CArchMiscWindows::wasLaunchedAsService() +{ + CString name; + if (!getParentProcessName(name)) { + LOG((CLOG_ERR "cannot determine if process was launched as service")); + return false; + } + + return (name == SERVICE_LAUNCHER); +} + +bool +CArchMiscWindows::getParentProcessName(CString &name) +{ + PROCESSENTRY32 parentEntry; + if (!getParentProcessEntry(parentEntry)){ + LOG((CLOG_ERR "could not get entry for parent process")); + return false; + } + + name = parentEntry.szExeFile; + return true; +} + +BOOL WINAPI +CArchMiscWindows::getSelfProcessEntry(PROCESSENTRY32& entry) +{ + // get entry from current PID + return getProcessEntry(entry, GetCurrentProcessId()); +} + +BOOL WINAPI +CArchMiscWindows::getParentProcessEntry(PROCESSENTRY32& entry) +{ + // get the current process, so we can get parent PID + PROCESSENTRY32 selfEntry; + if (!getSelfProcessEntry(selfEntry)) { + return FALSE; + } + + // get entry from parent PID + return getProcessEntry(entry, selfEntry.th32ParentProcessID); +} + +BOOL WINAPI +CArchMiscWindows::getProcessEntry(PROCESSENTRY32& entry, DWORD processID) +{ + // first we need to take a snapshot of the running processes + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) { + LOG((CLOG_ERR "could not get process snapshot (error: %i)", + GetLastError())); + return FALSE; + } + + entry.dwSize = sizeof(PROCESSENTRY32); + + // get the first process, and if we can't do that then it's + // unlikely we can go any further + BOOL gotEntry = Process32First(snapshot, &entry); + if (!gotEntry) { + LOG((CLOG_ERR "could not get first process entry (error: %i)", + GetLastError())); + return FALSE; + } + + while(gotEntry) { + + if (entry.th32ProcessID == processID) { + // found current process + return TRUE; + } + + // now move on to the next entry (when we reach end, loop will stop) + gotEntry = Process32Next(snapshot, &entry); + } + + return FALSE; +} + +HINSTANCE +CArchMiscWindows::instanceWin32() +{ + assert(s_instanceWin32 != NULL); + return s_instanceWin32; +} + +void +CArchMiscWindows::setInstanceWin32(HINSTANCE instance) +{ + assert(instance != NULL); + s_instanceWin32 = instance; +} \ No newline at end of file diff --git a/src/lib/arch/CArchMiscWindows.h b/src/lib/arch/CArchMiscWindows.h new file mode 100644 index 00000000..3abad463 --- /dev/null +++ b/src/lib/arch/CArchMiscWindows.h @@ -0,0 +1,214 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHMISCWINDOWS_H +#define CARCHMISCWINDOWS_H + +#define WIN32_LEAN_AND_MEAN + +#include "common.h" +#include "stdstring.h" +#include "stdset.h" +#include +#include +#include "CString.h" + +//! Miscellaneous win32 functions. +class CArchMiscWindows { +public: + enum EValueType { + kUNKNOWN, + kNO_VALUE, + kUINT, + kSTRING, + kBINARY + }; + enum EBusyModes { + kIDLE = 0x0000, + kSYSTEM = 0x0001, + kDISPLAY = 0x0002 + }; + + typedef int (*RunFunc)(void); + + //! Initialize + static void init(); + + //! Test if windows 95, et al. + /*! + Returns true iff the platform is win95/98/me. + */ + static bool isWindows95Family(); + + //! Test if windows 95, et al. + /*! + Returns true iff the platform is win98 or win2k or higher (i.e. + not windows 95 or windows NT). + */ + static bool isWindowsModern(); + + //! Set the application icons + /*! + Set the application icons. + */ + static void setIcons(HICON largeIcon, HICON smallIcon); + + //! Get the application icons + /*! + Get the application icons. + */ + static void getIcons(HICON& largeIcon, HICON& smallIcon); + + //! Run the daemon + /*! + Delegates to CArchDaemonWindows. + */ + static int runDaemon(RunFunc runFunc); + + //! Indicate daemon is in main loop + /*! + Delegates to CArchDaemonWindows. + */ + static void daemonRunning(bool running); + + //! Indicate failure of running daemon + /*! + Delegates to CArchDaemonWindows. + */ + static void daemonFailed(int result); + + //! Get daemon quit message + /*! + Delegates to CArchDaemonWindows. + */ + static UINT getDaemonQuitMessage(); + + //! Open and return a registry key, closing the parent key + static HKEY openKey(HKEY parent, const TCHAR* child); + + //! Open and return a registry key, closing the parent key + static HKEY openKey(HKEY parent, const TCHAR* const* keyPath); + + //! Open/create and return a registry key, closing the parent key + static HKEY addKey(HKEY parent, const TCHAR* child); + + //! Open/create and return a registry key, closing the parent key + static HKEY addKey(HKEY parent, const TCHAR* const* keyPath); + + //! Close a key + static void closeKey(HKEY); + + //! Delete a key (which should have no subkeys) + static void deleteKey(HKEY parent, const TCHAR* name); + + //! Delete a value + static void deleteValue(HKEY parent, const TCHAR* name); + + //! Test if a value exists + static bool hasValue(HKEY key, const TCHAR* name); + + //! Get type of value + static EValueType typeOfValue(HKEY key, const TCHAR* name); + + //! Set a string value in the registry + static void setValue(HKEY key, const TCHAR* name, + const std::string& value); + + //! Set a DWORD value in the registry + static void setValue(HKEY key, const TCHAR* name, DWORD value); + + //! Set a BINARY value in the registry + /*! + Sets the \p name value of \p key to \p value.data(). + */ + static void setValueBinary(HKEY key, const TCHAR* name, + const std::string& value); + + //! Read a string value from the registry + static std::string readValueString(HKEY, const TCHAR* name); + + //! Read a DWORD value from the registry + static DWORD readValueInt(HKEY, const TCHAR* name); + + //! Read a BINARY value from the registry + static std::string readValueBinary(HKEY, const TCHAR* name); + + //! Add a dialog + static void addDialog(HWND); + + //! Remove a dialog + static void removeDialog(HWND); + + //! Process dialog message + /*! + Checks if the message is destined for a dialog. If so the message + is passed to the dialog and returns true, otherwise returns false. + */ + static bool processDialog(MSG*); + + //! Disable power saving + static void addBusyState(DWORD busyModes); + + //! Enable power saving + static void removeBusyState(DWORD busyModes); + + //! Briefly interrupt power saving + static void wakeupDisplay(); + + //! Returns true if this process was launched via NT service host. + static bool wasLaunchedAsService(); + + //! Returns true if we got the parent process name. + static bool getParentProcessName(CString &name); + + static HINSTANCE instanceWin32(); + + static void setInstanceWin32(HINSTANCE instance); + + static BOOL WINAPI getProcessEntry(PROCESSENTRY32& entry, DWORD processID); + static BOOL WINAPI getSelfProcessEntry(PROCESSENTRY32& entry); + static BOOL WINAPI getParentProcessEntry(PROCESSENTRY32& entry); + +private: + //! Open and return a registry key, closing the parent key + static HKEY openKey(HKEY parent, const TCHAR* child, bool create); + + //! Open and return a registry key, closing the parent key + static HKEY openKey(HKEY parent, const TCHAR* const* keyPath, + bool create); + + //! Read a string value from the registry + static std::string readBinaryOrString(HKEY, const TCHAR* name, DWORD type); + + //! Set thread busy state + static void setThreadExecutionState(DWORD); + + static DWORD WINAPI dummySetThreadExecutionState(DWORD); + +private: + typedef std::set CDialogs; + typedef DWORD (WINAPI *STES_t)(DWORD); + + static CDialogs* s_dialogs; + static DWORD s_busyState; + static STES_t s_stes; + static HICON s_largeIcon; + static HICON s_smallIcon; + static HINSTANCE s_instanceWin32; +}; + +#endif diff --git a/src/lib/arch/CArchMultithreadPosix.cpp b/src/lib/arch/CArchMultithreadPosix.cpp new file mode 100644 index 00000000..1c5f0789 --- /dev/null +++ b/src/lib/arch/CArchMultithreadPosix.cpp @@ -0,0 +1,809 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchMultithreadPosix.h" +#include "CArch.h" +#include "XArch.h" +#include +#if TIME_WITH_SYS_TIME +# include +# include +#else +# if HAVE_SYS_TIME_H +# include +# else +# include +# endif +#endif +#include + +#define SIGWAKEUP SIGUSR1 + +#if !HAVE_PTHREAD_SIGNAL + // boy, is this platform broken. forget about pthread signal + // handling and let signals through to every process. synergy + // will not terminate cleanly when it gets SIGTERM or SIGINT. +# define pthread_sigmask sigprocmask +# define pthread_kill(tid_, sig_) kill(0, (sig_)) +# define sigwait(set_, sig_) +# undef HAVE_POSIX_SIGWAIT +# define HAVE_POSIX_SIGWAIT 1 +#endif + +static +void +setSignalSet(sigset_t* sigset) +{ + sigemptyset(sigset); + sigaddset(sigset, SIGHUP); + sigaddset(sigset, SIGINT); + sigaddset(sigset, SIGTERM); + sigaddset(sigset, SIGUSR2); +} + +// +// CArchThreadImpl +// + +class CArchThreadImpl { +public: + CArchThreadImpl(); + +public: + int m_refCount; + IArchMultithread::ThreadID m_id; + pthread_t m_thread; + IArchMultithread::ThreadFunc m_func; + void* m_userData; + bool m_cancel; + bool m_cancelling; + bool m_exited; + void* m_result; + void* m_networkData; +}; + +CArchThreadImpl::CArchThreadImpl() : + m_refCount(1), + m_id(0), + m_func(NULL), + m_userData(NULL), + m_cancel(false), + m_cancelling(false), + m_exited(false), + m_result(NULL), + m_networkData(NULL) +{ + // do nothing +} + + +// +// CArchMultithreadPosix +// + +CArchMultithreadPosix* CArchMultithreadPosix::s_instance = NULL; + +CArchMultithreadPosix::CArchMultithreadPosix() : + m_newThreadCalled(false), + m_nextID(0) +{ + assert(s_instance == NULL); + + s_instance = this; + + // no signal handlers + for (size_t i = 0; i < kNUM_SIGNALS; ++i) { + m_signalFunc[i] = NULL; + m_signalUserData[i] = NULL; + } + + // create mutex for thread list + m_threadMutex = newMutex(); + + // create thread for calling (main) thread and add it to our + // list. no need to lock the mutex since we're the only thread. + m_mainThread = new CArchThreadImpl; + m_mainThread->m_thread = pthread_self(); + insert(m_mainThread); + + // install SIGWAKEUP handler. this causes SIGWAKEUP to interrupt + // system calls. we use that when cancelling a thread to force it + // to wake up immediately if it's blocked in a system call. we + // won't need this until another thread is created but it's fine + // to install it now. + struct sigaction act; + sigemptyset(&act.sa_mask); +# if defined(SA_INTERRUPT) + act.sa_flags = SA_INTERRUPT; +# else + act.sa_flags = 0; +# endif + act.sa_handler = &threadCancel; + sigaction(SIGWAKEUP, &act, NULL); + + // set desired signal dispositions. let SIGWAKEUP through but + // ignore SIGPIPE (we'll handle EPIPE). + sigset_t sigset; + sigemptyset(&sigset); + sigaddset(&sigset, SIGWAKEUP); + pthread_sigmask(SIG_UNBLOCK, &sigset, NULL); + sigemptyset(&sigset); + sigaddset(&sigset, SIGPIPE); + pthread_sigmask(SIG_BLOCK, &sigset, NULL); +} + +CArchMultithreadPosix::~CArchMultithreadPosix() +{ + assert(s_instance != NULL); + + closeMutex(m_threadMutex); + s_instance = NULL; +} + +void +CArchMultithreadPosix::setNetworkDataForCurrentThread(void* data) +{ + lockMutex(m_threadMutex); + CArchThreadImpl* thread = find(pthread_self()); + thread->m_networkData = data; + unlockMutex(m_threadMutex); +} + +void* +CArchMultithreadPosix::getNetworkDataForThread(CArchThread thread) +{ + lockMutex(m_threadMutex); + void* data = thread->m_networkData; + unlockMutex(m_threadMutex); + return data; +} + +CArchMultithreadPosix* +CArchMultithreadPosix::getInstance() +{ + return s_instance; +} + +CArchCond +CArchMultithreadPosix::newCondVar() +{ + CArchCondImpl* cond = new CArchCondImpl; + int status = pthread_cond_init(&cond->m_cond, NULL); + (void)status; + assert(status == 0); + return cond; +} + +void +CArchMultithreadPosix::closeCondVar(CArchCond cond) +{ + int status = pthread_cond_destroy(&cond->m_cond); + (void)status; + assert(status == 0); + delete cond; +} + +void +CArchMultithreadPosix::signalCondVar(CArchCond cond) +{ + int status = pthread_cond_signal(&cond->m_cond); + (void)status; + assert(status == 0); +} + +void +CArchMultithreadPosix::broadcastCondVar(CArchCond cond) +{ + int status = pthread_cond_broadcast(&cond->m_cond); + (void)status; + assert(status == 0); +} + +bool +CArchMultithreadPosix::waitCondVar(CArchCond cond, + CArchMutex mutex, double timeout) +{ + // we can't wait on a condition variable and also wake it up for + // cancellation since we don't use posix cancellation. so we + // must wake up periodically to check for cancellation. we + // can't simply go back to waiting after the check since the + // condition may have changed and we'll have lost the signal. + // so we have to return to the caller. since the caller will + // always check for spurious wakeups the only drawback here is + // performance: we're waking up a lot more than desired. + static const double maxCancellationLatency = 0.1; + if (timeout < 0.0 || timeout > maxCancellationLatency) { + timeout = maxCancellationLatency; + } + + // see if we should cancel this thread + testCancelThread(); + + // get final time + struct timeval now; + gettimeofday(&now, NULL); + struct timespec finalTime; + finalTime.tv_sec = now.tv_sec; + finalTime.tv_nsec = now.tv_usec * 1000; + long timeout_sec = (long)timeout; + long timeout_nsec = (long)(1.0e+9 * (timeout - timeout_sec)); + finalTime.tv_sec += timeout_sec; + finalTime.tv_nsec += timeout_nsec; + if (finalTime.tv_nsec >= 1000000000) { + finalTime.tv_nsec -= 1000000000; + finalTime.tv_sec += 1; + } + + // wait + int status = pthread_cond_timedwait(&cond->m_cond, + &mutex->m_mutex, &finalTime); + + // check for cancel again + testCancelThread(); + + switch (status) { + case 0: + // success + return true; + + case ETIMEDOUT: + return false; + + default: + assert(0 && "condition variable wait error"); + return false; + } +} + +CArchMutex +CArchMultithreadPosix::newMutex() +{ + pthread_mutexattr_t attr; + int status = pthread_mutexattr_init(&attr); + assert(status == 0); + CArchMutexImpl* mutex = new CArchMutexImpl; + status = pthread_mutex_init(&mutex->m_mutex, &attr); + assert(status == 0); + return mutex; +} + +void +CArchMultithreadPosix::closeMutex(CArchMutex mutex) +{ + int status = pthread_mutex_destroy(&mutex->m_mutex); + (void)status; + assert(status == 0); + delete mutex; +} + +void +CArchMultithreadPosix::lockMutex(CArchMutex mutex) +{ + int status = pthread_mutex_lock(&mutex->m_mutex); + + switch (status) { + case 0: + // success + return; + + case EDEADLK: + assert(0 && "lock already owned"); + break; + + case EAGAIN: + assert(0 && "too many recursive locks"); + break; + + default: + assert(0 && "unexpected error"); + break; + } +} + +void +CArchMultithreadPosix::unlockMutex(CArchMutex mutex) +{ + int status = pthread_mutex_unlock(&mutex->m_mutex); + + switch (status) { + case 0: + // success + return; + + case EPERM: + assert(0 && "thread doesn't own a lock"); + break; + + default: + assert(0 && "unexpected error"); + break; + } +} + +CArchThread +CArchMultithreadPosix::newThread(ThreadFunc func, void* data) +{ + assert(func != NULL); + + // initialize signal handler. we do this here instead of the + // constructor so we can avoid daemonizing (using fork()) + // when there are multiple threads. clients can safely + // use condition variables and mutexes before creating a + // new thread and they can safely use the only thread + // they have access to, the main thread, so they really + // can't tell the difference. + if (!m_newThreadCalled) { + m_newThreadCalled = true; +#if HAVE_PTHREAD_SIGNAL + startSignalHandler(); +#endif + } + + lockMutex(m_threadMutex); + + // create thread impl for new thread + CArchThreadImpl* thread = new CArchThreadImpl; + thread->m_func = func; + thread->m_userData = data; + + // create the thread. pthread_create() on RedHat 7.2 smp fails + // if passed a NULL attr so use a default attr. + pthread_attr_t attr; + int status = pthread_attr_init(&attr); + if (status == 0) { + status = pthread_create(&thread->m_thread, &attr, + &CArchMultithreadPosix::threadFunc, thread); + pthread_attr_destroy(&attr); + } + + // check if thread was started + if (status != 0) { + // failed to start thread so clean up + delete thread; + thread = NULL; + } + else { + // add thread to list + insert(thread); + + // increment ref count to account for the thread itself + refThread(thread); + } + + // note that the child thread will wait until we release this mutex + unlockMutex(m_threadMutex); + + return thread; +} + +CArchThread +CArchMultithreadPosix::newCurrentThread() +{ + lockMutex(m_threadMutex); + CArchThreadImpl* thread = find(pthread_self()); + unlockMutex(m_threadMutex); + assert(thread != NULL); + return thread; +} + +void +CArchMultithreadPosix::closeThread(CArchThread thread) +{ + assert(thread != NULL); + + // decrement ref count and clean up thread if no more references + if (--thread->m_refCount == 0) { + // detach from thread (unless it's the main thread) + if (thread->m_func != NULL) { + pthread_detach(thread->m_thread); + } + + // remove thread from list + lockMutex(m_threadMutex); + assert(findNoRef(thread->m_thread) == thread); + erase(thread); + unlockMutex(m_threadMutex); + + // done with thread + delete thread; + } +} + +CArchThread +CArchMultithreadPosix::copyThread(CArchThread thread) +{ + refThread(thread); + return thread; +} + +void +CArchMultithreadPosix::cancelThread(CArchThread thread) +{ + assert(thread != NULL); + + // set cancel and wakeup flags if thread can be cancelled + bool wakeup = false; + lockMutex(m_threadMutex); + if (!thread->m_exited && !thread->m_cancelling) { + thread->m_cancel = true; + wakeup = true; + } + unlockMutex(m_threadMutex); + + // force thread to exit system calls if wakeup is true + if (wakeup) { + pthread_kill(thread->m_thread, SIGWAKEUP); + } +} + +void +CArchMultithreadPosix::setPriorityOfThread(CArchThread thread, int /*n*/) +{ + assert(thread != NULL); + + // FIXME +} + +void +CArchMultithreadPosix::testCancelThread() +{ + // find current thread + lockMutex(m_threadMutex); + CArchThreadImpl* thread = findNoRef(pthread_self()); + unlockMutex(m_threadMutex); + + // test cancel on thread + testCancelThreadImpl(thread); +} + +bool +CArchMultithreadPosix::wait(CArchThread target, double timeout) +{ + assert(target != NULL); + + lockMutex(m_threadMutex); + + // find current thread + CArchThreadImpl* self = findNoRef(pthread_self()); + + // ignore wait if trying to wait on ourself + if (target == self) { + unlockMutex(m_threadMutex); + return false; + } + + // ref the target so it can't go away while we're watching it + refThread(target); + + unlockMutex(m_threadMutex); + + try { + // do first test regardless of timeout + testCancelThreadImpl(self); + if (isExitedThread(target)) { + closeThread(target); + return true; + } + + // wait and repeat test if there's a timeout + if (timeout != 0.0) { + const double start = ARCH->time(); + do { + // wait a little + ARCH->sleep(0.05); + + // repeat test + testCancelThreadImpl(self); + if (isExitedThread(target)) { + closeThread(target); + return true; + } + + // repeat wait and test until timed out + } while (timeout < 0.0 || (ARCH->time() - start) <= timeout); + } + + closeThread(target); + return false; + } + catch (...) { + closeThread(target); + throw; + } +} + +bool +CArchMultithreadPosix::isSameThread(CArchThread thread1, CArchThread thread2) +{ + return (thread1 == thread2); +} + +bool +CArchMultithreadPosix::isExitedThread(CArchThread thread) +{ + lockMutex(m_threadMutex); + bool exited = thread->m_exited; + unlockMutex(m_threadMutex); + return exited; +} + +void* +CArchMultithreadPosix::getResultOfThread(CArchThread thread) +{ + lockMutex(m_threadMutex); + void* result = thread->m_result; + unlockMutex(m_threadMutex); + return result; +} + +IArchMultithread::ThreadID +CArchMultithreadPosix::getIDOfThread(CArchThread thread) +{ + return thread->m_id; +} + +void +CArchMultithreadPosix::setSignalHandler( + ESignal signal, SignalFunc func, void* userData) +{ + lockMutex(m_threadMutex); + m_signalFunc[signal] = func; + m_signalUserData[signal] = userData; + unlockMutex(m_threadMutex); +} + +void +CArchMultithreadPosix::raiseSignal(ESignal signal) +{ + lockMutex(m_threadMutex); + if (m_signalFunc[signal] != NULL) { + m_signalFunc[signal](signal, m_signalUserData[signal]); + pthread_kill(m_mainThread->m_thread, SIGWAKEUP); + } + else if (signal == kINTERRUPT || signal == kTERMINATE) { + ARCH->cancelThread(m_mainThread); + } + unlockMutex(m_threadMutex); +} + +void +CArchMultithreadPosix::startSignalHandler() +{ + // set signal mask. the main thread blocks these signals and + // the signal handler thread will listen for them. + sigset_t sigset, oldsigset; + setSignalSet(&sigset); + pthread_sigmask(SIG_BLOCK, &sigset, &oldsigset); + + // fire up the INT and TERM signal handler thread. we could + // instead arrange to catch and handle these signals but + // we'd be unable to cancel the main thread since no pthread + // calls are allowed in a signal handler. + pthread_attr_t attr; + int status = pthread_attr_init(&attr); + if (status == 0) { + status = pthread_create(&m_signalThread, &attr, + &CArchMultithreadPosix::threadSignalHandler, + NULL); + pthread_attr_destroy(&attr); + } + if (status != 0) { + // can't create thread to wait for signal so don't block + // the signals. + pthread_sigmask(SIG_UNBLOCK, &oldsigset, NULL); + } +} + +CArchThreadImpl* +CArchMultithreadPosix::find(pthread_t thread) +{ + CArchThreadImpl* impl = findNoRef(thread); + if (impl != NULL) { + refThread(impl); + } + return impl; +} + +CArchThreadImpl* +CArchMultithreadPosix::findNoRef(pthread_t thread) +{ + // linear search + for (CThreadList::const_iterator index = m_threadList.begin(); + index != m_threadList.end(); ++index) { + if ((*index)->m_thread == thread) { + return *index; + } + } + return NULL; +} + +void +CArchMultithreadPosix::insert(CArchThreadImpl* thread) +{ + assert(thread != NULL); + + // thread shouldn't already be on the list + assert(findNoRef(thread->m_thread) == NULL); + + // set thread id. note that we don't worry about m_nextID + // wrapping back to 0 and duplicating thread ID's since the + // likelihood of synergy running that long is vanishingly + // small. + thread->m_id = ++m_nextID; + + // append to list + m_threadList.push_back(thread); +} + +void +CArchMultithreadPosix::erase(CArchThreadImpl* thread) +{ + for (CThreadList::iterator index = m_threadList.begin(); + index != m_threadList.end(); ++index) { + if (*index == thread) { + m_threadList.erase(index); + break; + } + } +} + +void +CArchMultithreadPosix::refThread(CArchThreadImpl* thread) +{ + assert(thread != NULL); + assert(findNoRef(thread->m_thread) != NULL); + ++thread->m_refCount; +} + +void +CArchMultithreadPosix::testCancelThreadImpl(CArchThreadImpl* thread) +{ + assert(thread != NULL); + + // update cancel state + lockMutex(m_threadMutex); + bool cancel = false; + if (thread->m_cancel && !thread->m_cancelling) { + thread->m_cancelling = true; + thread->m_cancel = false; + cancel = true; + } + unlockMutex(m_threadMutex); + + // unwind thread's stack if cancelling + if (cancel) { + throw XThreadCancel(); + } +} + +void* +CArchMultithreadPosix::threadFunc(void* vrep) +{ + // get the thread + CArchThreadImpl* thread = reinterpret_cast(vrep); + + // setup pthreads + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); + + // run thread + s_instance->doThreadFunc(thread); + + // terminate the thread + return NULL; +} + +void +CArchMultithreadPosix::doThreadFunc(CArchThread thread) +{ + // default priority is slightly below normal + setPriorityOfThread(thread, 1); + + // wait for parent to initialize this object + lockMutex(m_threadMutex); + unlockMutex(m_threadMutex); + + void* result = NULL; + try { + // go + result = (*thread->m_func)(thread->m_userData); + } + + catch (XThreadCancel&) { + // client called cancel() + } + catch (...) { + // note -- don't catch (...) to avoid masking bugs + lockMutex(m_threadMutex); + thread->m_exited = true; + unlockMutex(m_threadMutex); + closeThread(thread); + throw; + } + + // thread has exited + lockMutex(m_threadMutex); + thread->m_result = result; + thread->m_exited = true; + unlockMutex(m_threadMutex); + + // done with thread + closeThread(thread); +} + +void +CArchMultithreadPosix::threadCancel(int) +{ + // do nothing +} + +void* +CArchMultithreadPosix::threadSignalHandler(void*) +{ + // detach + pthread_detach(pthread_self()); + + // add signal to mask + sigset_t sigset; + setSignalSet(&sigset); + + // also wait on SIGABRT. on linux (others?) this thread (process) + // will persist after all the other threads evaporate due to an + // assert unless we wait on SIGABRT. that means our resources (like + // the socket we're listening on) are not released and never will be + // until the lingering thread is killed. i don't know why sigwait() + // should protect the thread from being killed. note that sigwait() + // doesn't actually return if we receive SIGABRT and, for some + // reason, we don't have to block SIGABRT. + sigaddset(&sigset, SIGABRT); + + // we exit the loop via thread cancellation in sigwait() + for (;;) { + // wait +#if HAVE_POSIX_SIGWAIT + int signal = 0; + sigwait(&sigset, &signal); +#else + sigwait(&sigset); +#endif + + // if we get here then the signal was raised + switch (signal) { + case SIGINT: + ARCH->raiseSignal(kINTERRUPT); + break; + + case SIGTERM: + ARCH->raiseSignal(kTERMINATE); + break; + + case SIGHUP: + ARCH->raiseSignal(kHANGUP); + break; + + case SIGUSR2: + ARCH->raiseSignal(kUSER); + break; + + default: + // ignore + break; + } + } + + return NULL; +} diff --git a/src/lib/arch/CArchMultithreadPosix.h b/src/lib/arch/CArchMultithreadPosix.h new file mode 100644 index 00000000..031adcd7 --- /dev/null +++ b/src/lib/arch/CArchMultithreadPosix.h @@ -0,0 +1,116 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHMULTITHREADPOSIX_H +#define CARCHMULTITHREADPOSIX_H + +#include "IArchMultithread.h" +#include "stdlist.h" +#include + +#define ARCH_MULTITHREAD CArchMultithreadPosix + +class CArchCondImpl { +public: + pthread_cond_t m_cond; +}; + +class CArchMutexImpl { +public: + pthread_mutex_t m_mutex; +}; + +//! Posix implementation of IArchMultithread +class CArchMultithreadPosix : public IArchMultithread { +public: + CArchMultithreadPosix(); + virtual ~CArchMultithreadPosix(); + + //! @name manipulators + //@{ + + void setNetworkDataForCurrentThread(void*); + + //@} + //! @name accessors + //@{ + + void* getNetworkDataForThread(CArchThread); + + static CArchMultithreadPosix* getInstance(); + + //@} + + // IArchMultithread overrides + virtual CArchCond newCondVar(); + virtual void closeCondVar(CArchCond); + virtual void signalCondVar(CArchCond); + virtual void broadcastCondVar(CArchCond); + virtual bool waitCondVar(CArchCond, CArchMutex, double timeout); + virtual CArchMutex newMutex(); + virtual void closeMutex(CArchMutex); + virtual void lockMutex(CArchMutex); + virtual void unlockMutex(CArchMutex); + virtual CArchThread newThread(ThreadFunc, void*); + virtual CArchThread newCurrentThread(); + virtual CArchThread copyThread(CArchThread); + virtual void closeThread(CArchThread); + virtual void cancelThread(CArchThread); + virtual void setPriorityOfThread(CArchThread, int n); + virtual void testCancelThread(); + virtual bool wait(CArchThread, double timeout); + virtual bool isSameThread(CArchThread, CArchThread); + virtual bool isExitedThread(CArchThread); + virtual void* getResultOfThread(CArchThread); + virtual ThreadID getIDOfThread(CArchThread); + virtual void setSignalHandler(ESignal, SignalFunc, void*); + virtual void raiseSignal(ESignal); + +private: + void startSignalHandler(); + + CArchThreadImpl* find(pthread_t thread); + CArchThreadImpl* findNoRef(pthread_t thread); + void insert(CArchThreadImpl* thread); + void erase(CArchThreadImpl* thread); + + void refThread(CArchThreadImpl* rep); + void testCancelThreadImpl(CArchThreadImpl* rep); + + void doThreadFunc(CArchThread thread); + static void* threadFunc(void* vrep); + static void threadCancel(int); + static void* threadSignalHandler(void* vrep); + +private: + typedef std::list CThreadList; + + static CArchMultithreadPosix* s_instance; + + bool m_newThreadCalled; + + CArchMutex m_threadMutex; + CArchThread m_mainThread; + CThreadList m_threadList; + ThreadID m_nextID; + + pthread_t m_signalThread; + SignalFunc m_signalFunc[kNUM_SIGNALS]; + void* m_signalUserData[kNUM_SIGNALS]; +}; + +#endif diff --git a/src/lib/arch/CArchMultithreadWindows.cpp b/src/lib/arch/CArchMultithreadWindows.cpp new file mode 100644 index 00000000..d7a6101b --- /dev/null +++ b/src/lib/arch/CArchMultithreadWindows.cpp @@ -0,0 +1,702 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#if defined(_MSC_VER) && !defined(_MT) +# error multithreading compile option is required +#endif + +#include "CArchMultithreadWindows.h" +#include "CArch.h" +#include "XArch.h" +#include + +// +// note -- implementation of condition variable taken from: +// http://www.cs.wustl.edu/~schmidt/win32-cv-1.html +// titled "Strategies for Implementing POSIX Condition Variables +// on Win32." it also provides an implementation that doesn't +// suffer from the incorrectness problem described in our +// corresponding header but it is slower, still unfair, and +// can cause busy waiting. +// + +// +// CArchThreadImpl +// + +class CArchThreadImpl { +public: + CArchThreadImpl(); + ~CArchThreadImpl(); + +public: + int m_refCount; + HANDLE m_thread; + DWORD m_id; + IArchMultithread::ThreadFunc m_func; + void* m_userData; + HANDLE m_cancel; + bool m_cancelling; + HANDLE m_exit; + void* m_result; + void* m_networkData; +}; + +CArchThreadImpl::CArchThreadImpl() : + m_refCount(1), + m_thread(NULL), + m_id(0), + m_func(NULL), + m_userData(NULL), + m_cancelling(false), + m_result(NULL), + m_networkData(NULL) +{ + m_exit = CreateEvent(NULL, TRUE, FALSE, NULL); + m_cancel = CreateEvent(NULL, TRUE, FALSE, NULL); +} + +CArchThreadImpl::~CArchThreadImpl() +{ + CloseHandle(m_exit); + CloseHandle(m_cancel); +} + + +// +// CArchMultithreadWindows +// + +CArchMultithreadWindows* CArchMultithreadWindows::s_instance = NULL; + +CArchMultithreadWindows::CArchMultithreadWindows() +{ + assert(s_instance == NULL); + s_instance = this; + + // no signal handlers + for (size_t i = 0; i < kNUM_SIGNALS; ++i) { + m_signalFunc[i] = NULL; + m_signalUserData[i] = NULL; + } + + // create mutex for thread list + m_threadMutex = newMutex(); + + // create thread for calling (main) thread and add it to our + // list. no need to lock the mutex since we're the only thread. + m_mainThread = new CArchThreadImpl; + m_mainThread->m_thread = NULL; + m_mainThread->m_id = GetCurrentThreadId(); + insert(m_mainThread); +} + +CArchMultithreadWindows::~CArchMultithreadWindows() +{ + s_instance = NULL; + + // clean up thread list + for (CThreadList::iterator index = m_threadList.begin(); + index != m_threadList.end(); ++index) { + delete *index; + } + + // done with mutex + delete m_threadMutex; +} + +void +CArchMultithreadWindows::setNetworkDataForCurrentThread(void* data) +{ + lockMutex(m_threadMutex); + CArchThreadImpl* thread = findNoRef(GetCurrentThreadId()); + thread->m_networkData = data; + unlockMutex(m_threadMutex); +} + +void* +CArchMultithreadWindows::getNetworkDataForThread(CArchThread thread) +{ + lockMutex(m_threadMutex); + void* data = thread->m_networkData; + unlockMutex(m_threadMutex); + return data; +} + +HANDLE +CArchMultithreadWindows::getCancelEventForCurrentThread() +{ + lockMutex(m_threadMutex); + CArchThreadImpl* thread = findNoRef(GetCurrentThreadId()); + unlockMutex(m_threadMutex); + return thread->m_cancel; +} + +CArchMultithreadWindows* +CArchMultithreadWindows::getInstance() +{ + return s_instance; +} + +CArchCond +CArchMultithreadWindows::newCondVar() +{ + CArchCondImpl* cond = new CArchCondImpl; + cond->m_events[CArchCondImpl::kSignal] = CreateEvent(NULL, + FALSE, FALSE, NULL); + cond->m_events[CArchCondImpl::kBroadcast] = CreateEvent(NULL, + TRUE, FALSE, NULL); + cond->m_waitCountMutex = newMutex(); + cond->m_waitCount = 0; + return cond; +} + +void +CArchMultithreadWindows::closeCondVar(CArchCond cond) +{ + CloseHandle(cond->m_events[CArchCondImpl::kSignal]); + CloseHandle(cond->m_events[CArchCondImpl::kBroadcast]); + closeMutex(cond->m_waitCountMutex); + delete cond; +} + +void +CArchMultithreadWindows::signalCondVar(CArchCond cond) +{ + // is anybody waiting? + lockMutex(cond->m_waitCountMutex); + const bool hasWaiter = (cond->m_waitCount > 0); + unlockMutex(cond->m_waitCountMutex); + + // wake one thread if anybody is waiting + if (hasWaiter) { + SetEvent(cond->m_events[CArchCondImpl::kSignal]); + } +} + +void +CArchMultithreadWindows::broadcastCondVar(CArchCond cond) +{ + // is anybody waiting? + lockMutex(cond->m_waitCountMutex); + const bool hasWaiter = (cond->m_waitCount > 0); + unlockMutex(cond->m_waitCountMutex); + + // wake all threads if anybody is waiting + if (hasWaiter) { + SetEvent(cond->m_events[CArchCondImpl::kBroadcast]); + } +} + +bool +CArchMultithreadWindows::waitCondVar(CArchCond cond, + CArchMutex mutex, double timeout) +{ + // prepare to wait + const DWORD winTimeout = (timeout < 0.0) ? INFINITE : + static_cast(1000.0 * timeout); + + // make a list of the condition variable events and the cancel event + // for the current thread. + HANDLE handles[4]; + handles[0] = cond->m_events[CArchCondImpl::kSignal]; + handles[1] = cond->m_events[CArchCondImpl::kBroadcast]; + handles[2] = getCancelEventForCurrentThread(); + + // update waiter count + lockMutex(cond->m_waitCountMutex); + ++cond->m_waitCount; + unlockMutex(cond->m_waitCountMutex); + + // release mutex. this should be atomic with the wait so that it's + // impossible for another thread to signal us between the unlock and + // the wait, which would lead to a lost signal on broadcasts. + // however, we're using a manual reset event for broadcasts which + // stays set until we reset it, so we don't lose the broadcast. + unlockMutex(mutex); + + // wait for a signal or broadcast + DWORD result = WaitForMultipleObjects(3, handles, FALSE, winTimeout); + + // cancel takes priority + if (result != WAIT_OBJECT_0 + 2 && + WaitForSingleObject(handles[2], 0) == WAIT_OBJECT_0) { + result = WAIT_OBJECT_0 + 2; + } + + // update the waiter count and check if we're the last waiter + lockMutex(cond->m_waitCountMutex); + --cond->m_waitCount; + const bool last = (result == WAIT_OBJECT_0 + 1 && cond->m_waitCount == 0); + unlockMutex(cond->m_waitCountMutex); + + // reset the broadcast event if we're the last waiter + if (last) { + ResetEvent(cond->m_events[CArchCondImpl::kBroadcast]); + } + + // reacquire the mutex + lockMutex(mutex); + + // cancel thread if necessary + if (result == WAIT_OBJECT_0 + 2) { + ARCH->testCancelThread(); + } + + // return success or failure + return (result == WAIT_OBJECT_0 + 0 || + result == WAIT_OBJECT_0 + 1); +} + +CArchMutex +CArchMultithreadWindows::newMutex() +{ + CArchMutexImpl* mutex = new CArchMutexImpl; + InitializeCriticalSection(&mutex->m_mutex); + return mutex; +} + +void +CArchMultithreadWindows::closeMutex(CArchMutex mutex) +{ + DeleteCriticalSection(&mutex->m_mutex); + delete mutex; +} + +void +CArchMultithreadWindows::lockMutex(CArchMutex mutex) +{ + EnterCriticalSection(&mutex->m_mutex); +} + +void +CArchMultithreadWindows::unlockMutex(CArchMutex mutex) +{ + LeaveCriticalSection(&mutex->m_mutex); +} + +CArchThread +CArchMultithreadWindows::newThread(ThreadFunc func, void* data) +{ + lockMutex(m_threadMutex); + + // create thread impl for new thread + CArchThreadImpl* thread = new CArchThreadImpl; + thread->m_func = func; + thread->m_userData = data; + + // create thread + unsigned int id = 0; + thread->m_thread = reinterpret_cast(_beginthreadex(NULL, 0, + threadFunc, (void*)thread, 0, &id)); + thread->m_id = static_cast(id); + + // check if thread was started + if (thread->m_thread == 0) { + // failed to start thread so clean up + delete thread; + thread = NULL; + } + else { + // add thread to list + insert(thread); + + // increment ref count to account for the thread itself + refThread(thread); + } + + // note that the child thread will wait until we release this mutex + unlockMutex(m_threadMutex); + + return thread; +} + +CArchThread +CArchMultithreadWindows::newCurrentThread() +{ + lockMutex(m_threadMutex); + CArchThreadImpl* thread = find(GetCurrentThreadId()); + unlockMutex(m_threadMutex); + assert(thread != NULL); + return thread; +} + +void +CArchMultithreadWindows::closeThread(CArchThread thread) +{ + assert(thread != NULL); + + // decrement ref count and clean up thread if no more references + if (--thread->m_refCount == 0) { + // close the handle (main thread has a NULL handle) + if (thread->m_thread != NULL) { + CloseHandle(thread->m_thread); + } + + // remove thread from list + lockMutex(m_threadMutex); + assert(findNoRefOrCreate(thread->m_id) == thread); + erase(thread); + unlockMutex(m_threadMutex); + + // done with thread + delete thread; + } +} + +CArchThread +CArchMultithreadWindows::copyThread(CArchThread thread) +{ + refThread(thread); + return thread; +} + +void +CArchMultithreadWindows::cancelThread(CArchThread thread) +{ + assert(thread != NULL); + + // set cancel flag + SetEvent(thread->m_cancel); +} + +void +CArchMultithreadWindows::setPriorityOfThread(CArchThread thread, int n) +{ + struct CPriorityInfo { + public: + DWORD m_class; + int m_level; + }; + static const CPriorityInfo s_pClass[] = { + { IDLE_PRIORITY_CLASS, THREAD_PRIORITY_IDLE }, + { IDLE_PRIORITY_CLASS, THREAD_PRIORITY_LOWEST }, + { IDLE_PRIORITY_CLASS, THREAD_PRIORITY_BELOW_NORMAL }, + { IDLE_PRIORITY_CLASS, THREAD_PRIORITY_NORMAL }, + { IDLE_PRIORITY_CLASS, THREAD_PRIORITY_ABOVE_NORMAL }, + { IDLE_PRIORITY_CLASS, THREAD_PRIORITY_HIGHEST }, + { NORMAL_PRIORITY_CLASS, THREAD_PRIORITY_LOWEST }, + { NORMAL_PRIORITY_CLASS, THREAD_PRIORITY_BELOW_NORMAL }, + { NORMAL_PRIORITY_CLASS, THREAD_PRIORITY_NORMAL }, + { NORMAL_PRIORITY_CLASS, THREAD_PRIORITY_ABOVE_NORMAL }, + { NORMAL_PRIORITY_CLASS, THREAD_PRIORITY_HIGHEST }, + { HIGH_PRIORITY_CLASS, THREAD_PRIORITY_LOWEST }, + { HIGH_PRIORITY_CLASS, THREAD_PRIORITY_BELOW_NORMAL }, + { HIGH_PRIORITY_CLASS, THREAD_PRIORITY_NORMAL }, + { HIGH_PRIORITY_CLASS, THREAD_PRIORITY_ABOVE_NORMAL }, + { HIGH_PRIORITY_CLASS, THREAD_PRIORITY_HIGHEST }, + { REALTIME_PRIORITY_CLASS, THREAD_PRIORITY_IDLE }, + { REALTIME_PRIORITY_CLASS, THREAD_PRIORITY_LOWEST }, + { REALTIME_PRIORITY_CLASS, THREAD_PRIORITY_BELOW_NORMAL }, + { REALTIME_PRIORITY_CLASS, THREAD_PRIORITY_NORMAL }, + { REALTIME_PRIORITY_CLASS, THREAD_PRIORITY_ABOVE_NORMAL }, + { REALTIME_PRIORITY_CLASS, THREAD_PRIORITY_HIGHEST }, + { REALTIME_PRIORITY_CLASS, THREAD_PRIORITY_TIME_CRITICAL} + }; +#if defined(_DEBUG) + // don't use really high priorities when debugging + static const size_t s_pMax = 13; +#else + static const size_t s_pMax = sizeof(s_pClass) / sizeof(s_pClass[0]) - 1; +#endif + static const size_t s_pBase = 8; // index of normal priority + + assert(thread != NULL); + + size_t index; + if (n > 0 && s_pBase < (size_t)n) { + // lowest priority + index = 0; + } + else { + index = (size_t)((int)s_pBase - n); + if (index > s_pMax) { + // highest priority + index = s_pMax; + } + } + SetPriorityClass(GetCurrentProcess(), s_pClass[index].m_class); + SetThreadPriority(thread->m_thread, s_pClass[index].m_level); +} + +void +CArchMultithreadWindows::testCancelThread() +{ + // find current thread + lockMutex(m_threadMutex); + CArchThreadImpl* thread = findNoRef(GetCurrentThreadId()); + unlockMutex(m_threadMutex); + + // test cancel on thread + testCancelThreadImpl(thread); +} + +bool +CArchMultithreadWindows::wait(CArchThread target, double timeout) +{ + assert(target != NULL); + + lockMutex(m_threadMutex); + + // find current thread + CArchThreadImpl* self = findNoRef(GetCurrentThreadId()); + + // ignore wait if trying to wait on ourself + if (target == self) { + unlockMutex(m_threadMutex); + return false; + } + + // ref the target so it can't go away while we're watching it + refThread(target); + + unlockMutex(m_threadMutex); + + // convert timeout + DWORD t; + if (timeout < 0.0) { + t = INFINITE; + } + else { + t = (DWORD)(1000.0 * timeout); + } + + // wait for this thread to be cancelled or woken up or for the + // target thread to terminate. + HANDLE handles[2]; + handles[0] = target->m_exit; + handles[1] = self->m_cancel; + DWORD result = WaitForMultipleObjects(2, handles, FALSE, t); + + // cancel takes priority + if (result != WAIT_OBJECT_0 + 1 && + WaitForSingleObject(handles[1], 0) == WAIT_OBJECT_0) { + result = WAIT_OBJECT_0 + 1; + } + + // release target + closeThread(target); + + // handle result + switch (result) { + case WAIT_OBJECT_0 + 0: + // target thread terminated + return true; + + case WAIT_OBJECT_0 + 1: + // this thread was cancelled. does not return. + testCancelThreadImpl(self); + + default: + // timeout or error + return false; + } +} + +bool +CArchMultithreadWindows::isSameThread(CArchThread thread1, CArchThread thread2) +{ + return (thread1 == thread2); +} + +bool +CArchMultithreadWindows::isExitedThread(CArchThread thread) +{ + // poll exit event + return (WaitForSingleObject(thread->m_exit, 0) == WAIT_OBJECT_0); +} + +void* +CArchMultithreadWindows::getResultOfThread(CArchThread thread) +{ + lockMutex(m_threadMutex); + void* result = thread->m_result; + unlockMutex(m_threadMutex); + return result; +} + +IArchMultithread::ThreadID +CArchMultithreadWindows::getIDOfThread(CArchThread thread) +{ + return static_cast(thread->m_id); +} + +void +CArchMultithreadWindows::setSignalHandler( + ESignal signal, SignalFunc func, void* userData) +{ + lockMutex(m_threadMutex); + m_signalFunc[signal] = func; + m_signalUserData[signal] = userData; + unlockMutex(m_threadMutex); +} + +void +CArchMultithreadWindows::raiseSignal(ESignal signal) +{ + lockMutex(m_threadMutex); + if (m_signalFunc[signal] != NULL) { + m_signalFunc[signal](signal, m_signalUserData[signal]); + ARCH->unblockPollSocket(m_mainThread); + } + else if (signal == kINTERRUPT || signal == kTERMINATE) { + ARCH->cancelThread(m_mainThread); + } + unlockMutex(m_threadMutex); +} + +CArchThreadImpl* +CArchMultithreadWindows::find(DWORD id) +{ + CArchThreadImpl* impl = findNoRef(id); + if (impl != NULL) { + refThread(impl); + } + return impl; +} + +CArchThreadImpl* +CArchMultithreadWindows::findNoRef(DWORD id) +{ + CArchThreadImpl* impl = findNoRefOrCreate(id); + if (impl == NULL) { + // create thread for calling thread which isn't in our list and + // add it to the list. this won't normally happen but it can if + // the system calls us under a new thread, like it does when we + // run as a service. + impl = new CArchThreadImpl; + impl->m_thread = NULL; + impl->m_id = GetCurrentThreadId(); + insert(impl); + } + return impl; +} + +CArchThreadImpl* +CArchMultithreadWindows::findNoRefOrCreate(DWORD id) +{ + // linear search + for (CThreadList::const_iterator index = m_threadList.begin(); + index != m_threadList.end(); ++index) { + if ((*index)->m_id == id) { + return *index; + } + } + return NULL; +} + +void +CArchMultithreadWindows::insert(CArchThreadImpl* thread) +{ + assert(thread != NULL); + + // thread shouldn't already be on the list + assert(findNoRefOrCreate(thread->m_id) == NULL); + + // append to list + m_threadList.push_back(thread); +} + +void +CArchMultithreadWindows::erase(CArchThreadImpl* thread) +{ + for (CThreadList::iterator index = m_threadList.begin(); + index != m_threadList.end(); ++index) { + if (*index == thread) { + m_threadList.erase(index); + break; + } + } +} + +void +CArchMultithreadWindows::refThread(CArchThreadImpl* thread) +{ + assert(thread != NULL); + assert(findNoRefOrCreate(thread->m_id) != NULL); + ++thread->m_refCount; +} + +void +CArchMultithreadWindows::testCancelThreadImpl(CArchThreadImpl* thread) +{ + assert(thread != NULL); + + // poll cancel event. return if not set. + const DWORD result = WaitForSingleObject(thread->m_cancel, 0); + if (result != WAIT_OBJECT_0) { + return; + } + + // update cancel state + lockMutex(m_threadMutex); + bool cancel = !thread->m_cancelling; + thread->m_cancelling = true; + ResetEvent(thread->m_cancel); + unlockMutex(m_threadMutex); + + // unwind thread's stack if cancelling + if (cancel) { + throw XThreadCancel(); + } +} + +unsigned int __stdcall +CArchMultithreadWindows::threadFunc(void* vrep) +{ + // get the thread + CArchThreadImpl* thread = reinterpret_cast(vrep); + + // run thread + s_instance->doThreadFunc(thread); + + // terminate the thread + return 0; +} + +void +CArchMultithreadWindows::doThreadFunc(CArchThread thread) +{ + // wait for parent to initialize this object + lockMutex(m_threadMutex); + unlockMutex(m_threadMutex); + + void* result = NULL; + try { + // go + result = (*thread->m_func)(thread->m_userData); + } + + catch (XThreadCancel&) { + // client called cancel() + } + catch (...) { + // note -- don't catch (...) to avoid masking bugs + SetEvent(thread->m_exit); + closeThread(thread); + throw; + } + + // thread has exited + lockMutex(m_threadMutex); + thread->m_result = result; + unlockMutex(m_threadMutex); + SetEvent(thread->m_exit); + + // done with thread + closeThread(thread); +} diff --git a/src/lib/arch/CArchMultithreadWindows.h b/src/lib/arch/CArchMultithreadWindows.h new file mode 100644 index 00000000..71414b0b --- /dev/null +++ b/src/lib/arch/CArchMultithreadWindows.h @@ -0,0 +1,118 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHMULTITHREADWINDOWS_H +#define CARCHMULTITHREADWINDOWS_H + +#define WIN32_LEAN_AND_MEAN + +#include "IArchMultithread.h" +#include "stdlist.h" +#include + +#define ARCH_MULTITHREAD CArchMultithreadWindows + +class CArchCondImpl { +public: + enum { kSignal = 0, kBroadcast }; + + HANDLE m_events[2]; + mutable int m_waitCount; + CArchMutex m_waitCountMutex; +}; + +class CArchMutexImpl { +public: + CRITICAL_SECTION m_mutex; +}; + +//! Win32 implementation of IArchMultithread +class CArchMultithreadWindows : public IArchMultithread { +public: + CArchMultithreadWindows(); + virtual ~CArchMultithreadWindows(); + + //! @name manipulators + //@{ + + void setNetworkDataForCurrentThread(void*); + + //@} + //! @name accessors + //@{ + + HANDLE getCancelEventForCurrentThread(); + + void* getNetworkDataForThread(CArchThread); + + static CArchMultithreadWindows* getInstance(); + + //@} + + // IArchMultithread overrides + virtual CArchCond newCondVar(); + virtual void closeCondVar(CArchCond); + virtual void signalCondVar(CArchCond); + virtual void broadcastCondVar(CArchCond); + virtual bool waitCondVar(CArchCond, CArchMutex, double timeout); + virtual CArchMutex newMutex(); + virtual void closeMutex(CArchMutex); + virtual void lockMutex(CArchMutex); + virtual void unlockMutex(CArchMutex); + virtual CArchThread newThread(ThreadFunc, void*); + virtual CArchThread newCurrentThread(); + virtual CArchThread copyThread(CArchThread); + virtual void closeThread(CArchThread); + virtual void cancelThread(CArchThread); + virtual void setPriorityOfThread(CArchThread, int n); + virtual void testCancelThread(); + virtual bool wait(CArchThread, double timeout); + virtual bool isSameThread(CArchThread, CArchThread); + virtual bool isExitedThread(CArchThread); + virtual void* getResultOfThread(CArchThread); + virtual ThreadID getIDOfThread(CArchThread); + virtual void setSignalHandler(ESignal, SignalFunc, void*); + virtual void raiseSignal(ESignal); + +private: + CArchThreadImpl* find(DWORD id); + CArchThreadImpl* findNoRef(DWORD id); + CArchThreadImpl* findNoRefOrCreate(DWORD id); + void insert(CArchThreadImpl* thread); + void erase(CArchThreadImpl* thread); + + void refThread(CArchThreadImpl* rep); + void testCancelThreadImpl(CArchThreadImpl* rep); + + void doThreadFunc(CArchThread thread); + static unsigned int __stdcall threadFunc(void* vrep); + +private: + typedef std::list CThreadList; + + static CArchMultithreadWindows* s_instance; + + CArchMutex m_threadMutex; + + CThreadList m_threadList; + CArchThread m_mainThread; + + SignalFunc m_signalFunc[kNUM_SIGNALS]; + void* m_signalUserData[kNUM_SIGNALS]; +}; + +#endif diff --git a/src/lib/arch/CArchNetworkBSD.cpp b/src/lib/arch/CArchNetworkBSD.cpp new file mode 100644 index 00000000..0b66ffbb --- /dev/null +++ b/src/lib/arch/CArchNetworkBSD.cpp @@ -0,0 +1,986 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchNetworkBSD.h" +#include "CArch.h" +#include "CArchMultithreadPosix.h" +#include "XArchUnix.h" +#if HAVE_UNISTD_H +# include +#endif +#include +#include +#if !defined(TCP_NODELAY) +# include +#endif +#include +#include +#include +#include + +#if HAVE_POLL +# include +#else +# if HAVE_SYS_SELECT_H +# include +# endif +# if HAVE_SYS_TIME_H +# include +# endif +#endif + +#if !HAVE_INET_ATON +# include +#endif + +static const int s_family[] = { + PF_UNSPEC, + PF_INET +}; +static const int s_type[] = { + SOCK_DGRAM, + SOCK_STREAM +}; + +#if !HAVE_INET_ATON +// parse dotted quad addresses. we don't bother with the weird BSD'ism +// of handling octal and hex and partial forms. +static +in_addr_t +inet_aton(const char* cp, struct in_addr* inp) +{ + unsigned int a, b, c, d; + if (sscanf(cp, "%u.%u.%u.%u", &a, &b, &c, &d) != 4) { + return 0; + } + if (a >= 256 || b >= 256 || c >= 256 || d >= 256) { + return 0; + } + unsigned char* incp = (unsigned char*)inp; + incp[0] = (unsigned char)(a & 0xffu); + incp[1] = (unsigned char)(b & 0xffu); + incp[2] = (unsigned char)(c & 0xffu); + incp[3] = (unsigned char)(d & 0xffu); + return inp->s_addr; +} +#endif + +// +// CArchNetworkBSD +// + +CArchNetworkBSD::CArchNetworkBSD() +{ +} + +CArchNetworkBSD::~CArchNetworkBSD() +{ + ARCH->closeMutex(m_mutex); +} + +void +CArchNetworkBSD::init() +{ + // create mutex to make some calls thread safe + m_mutex = ARCH->newMutex(); +} + +CArchSocket +CArchNetworkBSD::newSocket(EAddressFamily family, ESocketType type) +{ + // create socket + int fd = socket(s_family[family], s_type[type], 0); + if (fd == -1) { + throwError(errno); + } + try { + setBlockingOnSocket(fd, false); + } + catch (...) { + close(fd); + throw; + } + + // allocate socket object + CArchSocketImpl* newSocket = new CArchSocketImpl; + newSocket->m_fd = fd; + newSocket->m_refCount = 1; + return newSocket; +} + +CArchSocket +CArchNetworkBSD::copySocket(CArchSocket s) +{ + assert(s != NULL); + + // ref the socket and return it + ARCH->lockMutex(m_mutex); + ++s->m_refCount; + ARCH->unlockMutex(m_mutex); + return s; +} + +void +CArchNetworkBSD::closeSocket(CArchSocket s) +{ + assert(s != NULL); + + // unref the socket and note if it should be released + ARCH->lockMutex(m_mutex); + const bool doClose = (--s->m_refCount == 0); + ARCH->unlockMutex(m_mutex); + + // close the socket if necessary + if (doClose) { + if (close(s->m_fd) == -1) { + // close failed. restore the last ref and throw. + int err = errno; + ARCH->lockMutex(m_mutex); + ++s->m_refCount; + ARCH->unlockMutex(m_mutex); + throwError(err); + } + delete s; + } +} + +void +CArchNetworkBSD::closeSocketForRead(CArchSocket s) +{ + assert(s != NULL); + + if (shutdown(s->m_fd, 0) == -1) { + if (errno != ENOTCONN) { + throwError(errno); + } + } +} + +void +CArchNetworkBSD::closeSocketForWrite(CArchSocket s) +{ + assert(s != NULL); + + if (shutdown(s->m_fd, 1) == -1) { + if (errno != ENOTCONN) { + throwError(errno); + } + } +} + +void +CArchNetworkBSD::bindSocket(CArchSocket s, CArchNetAddress addr) +{ + assert(s != NULL); + assert(addr != NULL); + + if (bind(s->m_fd, &addr->m_addr, addr->m_len) == -1) { + throwError(errno); + } +} + +void +CArchNetworkBSD::listenOnSocket(CArchSocket s) +{ + assert(s != NULL); + + // hardcoding backlog + if (listen(s->m_fd, 3) == -1) { + throwError(errno); + } +} + +CArchSocket +CArchNetworkBSD::acceptSocket(CArchSocket s, CArchNetAddress* addr) +{ + assert(s != NULL); + + // if user passed NULL in addr then use scratch space + CArchNetAddress dummy; + if (addr == NULL) { + addr = &dummy; + } + + // create new socket and address + CArchSocketImpl* newSocket = new CArchSocketImpl; + *addr = new CArchNetAddressImpl; + + // accept on socket + ACCEPT_TYPE_ARG3 len = (ACCEPT_TYPE_ARG3)((*addr)->m_len); + int fd = accept(s->m_fd, &(*addr)->m_addr, &len); + (*addr)->m_len = (socklen_t)len; + if (fd == -1) { + int err = errno; + delete newSocket; + delete *addr; + *addr = NULL; + if (err == EAGAIN) { + return NULL; + } + throwError(err); + } + + try { + setBlockingOnSocket(fd, false); + } + catch (...) { + close(fd); + delete newSocket; + delete *addr; + *addr = NULL; + throw; + } + + // initialize socket + newSocket->m_fd = fd; + newSocket->m_refCount = 1; + + // discard address if not requested + if (addr == &dummy) { + ARCH->closeAddr(dummy); + } + + return newSocket; +} + +bool +CArchNetworkBSD::connectSocket(CArchSocket s, CArchNetAddress addr) +{ + assert(s != NULL); + assert(addr != NULL); + + if (connect(s->m_fd, &addr->m_addr, addr->m_len) == -1) { + if (errno == EISCONN) { + return true; + } + if (errno == EINPROGRESS) { + return false; + } + throwError(errno); + } + return true; +} + +#if HAVE_POLL + +int +CArchNetworkBSD::pollSocket(CPollEntry pe[], int num, double timeout) +{ + assert(pe != NULL || num == 0); + + // return if nothing to do + if (num == 0) { + if (timeout > 0.0) { + ARCH->sleep(timeout); + } + return 0; + } + + // allocate space for translated query + struct pollfd* pfd = new struct pollfd[1 + num]; + + // translate query + for (int i = 0; i < num; ++i) { + pfd[i].fd = (pe[i].m_socket == NULL) ? -1 : pe[i].m_socket->m_fd; + pfd[i].events = 0; + if ((pe[i].m_events & kPOLLIN) != 0) { + pfd[i].events |= POLLIN; + } + if ((pe[i].m_events & kPOLLOUT) != 0) { + pfd[i].events |= POLLOUT; + } + } + int n = num; + + // add the unblock pipe + const int* unblockPipe = getUnblockPipe(); + if (unblockPipe != NULL) { + pfd[n].fd = unblockPipe[0]; + pfd[n].events = POLLIN; + ++n; + } + + // prepare timeout + int t = (timeout < 0.0) ? -1 : static_cast(1000.0 * timeout); + + // do the poll + n = poll(pfd, n, t); + + // reset the unblock pipe + if (n > 0 && unblockPipe != NULL && (pfd[num].revents & POLLIN) != 0) { + // the unblock event was signalled. flush the pipe. + char dummy[100]; + int ignore; + + do { + ignore = read(unblockPipe[0], dummy, sizeof(dummy)); + } while (errno != EAGAIN); + + // don't count this unblock pipe in return value + --n; + } + + // handle results + if (n == -1) { + if (errno == EINTR) { + // interrupted system call + ARCH->testCancelThread(); + delete[] pfd; + return 0; + } + delete[] pfd; + throwError(errno); + } + + // translate back + for (int i = 0; i < num; ++i) { + pe[i].m_revents = 0; + if ((pfd[i].revents & POLLIN) != 0) { + pe[i].m_revents |= kPOLLIN; + } + if ((pfd[i].revents & POLLOUT) != 0) { + pe[i].m_revents |= kPOLLOUT; + } + if ((pfd[i].revents & POLLERR) != 0) { + pe[i].m_revents |= kPOLLERR; + } + if ((pfd[i].revents & POLLNVAL) != 0) { + pe[i].m_revents |= kPOLLNVAL; + } + } + + delete[] pfd; + return n; +} + +#else + +int +CArchNetworkBSD::pollSocket(CPollEntry pe[], int num, double timeout) +{ + int i, n; + + // prepare sets for select + n = 0; + fd_set readSet, writeSet, errSet; + fd_set* readSetP = NULL; + fd_set* writeSetP = NULL; + fd_set* errSetP = NULL; + FD_ZERO(&readSet); + FD_ZERO(&writeSet); + FD_ZERO(&errSet); + for (i = 0; i < num; ++i) { + // reset return flags + pe[i].m_revents = 0; + + // set invalid flag if socket is bogus then go to next socket + if (pe[i].m_socket == NULL) { + pe[i].m_revents |= kPOLLNVAL; + continue; + } + + int fdi = pe[i].m_socket->m_fd; + if (pe[i].m_events & kPOLLIN) { + FD_SET(pe[i].m_socket->m_fd, &readSet); + readSetP = &readSet; + if (fdi > n) { + n = fdi; + } + } + if (pe[i].m_events & kPOLLOUT) { + FD_SET(pe[i].m_socket->m_fd, &writeSet); + writeSetP = &writeSet; + if (fdi > n) { + n = fdi; + } + } + if (true) { + FD_SET(pe[i].m_socket->m_fd, &errSet); + errSetP = &errSet; + if (fdi > n) { + n = fdi; + } + } + } + + // add the unblock pipe + const int* unblockPipe = getUnblockPipe(); + if (unblockPipe != NULL) { + FD_SET(unblockPipe[0], &readSet); + readSetP = &readSet; + if (unblockPipe[0] > n) { + n = unblockPipe[0]; + } + } + + // if there are no sockets then don't block forever + if (n == 0 && timeout < 0.0) { + timeout = 0.0; + } + + // prepare timeout for select + struct timeval timeout2; + struct timeval* timeout2P; + if (timeout < 0.0) { + timeout2P = NULL; + } + else { + timeout2P = &timeout2; + timeout2.tv_sec = static_cast(timeout); + timeout2.tv_usec = static_cast(1.0e+6 * + (timeout - timeout2.tv_sec)); + } + + // do the select + n = select((SELECT_TYPE_ARG1) n + 1, + SELECT_TYPE_ARG234 readSetP, + SELECT_TYPE_ARG234 writeSetP, + SELECT_TYPE_ARG234 errSetP, + SELECT_TYPE_ARG5 timeout2P); + + // reset the unblock pipe + if (n > 0 && unblockPipe != NULL && FD_ISSET(unblockPipe[0], &readSet)) { + // the unblock event was signalled. flush the pipe. + char dummy[100]; + do { + read(unblockPipe[0], dummy, sizeof(dummy)); + } while (errno != EAGAIN); + } + + // handle results + if (n == -1) { + if (errno == EINTR) { + // interrupted system call + ARCH->testCancelThread(); + return 0; + } + throwError(errno); + } + n = 0; + for (i = 0; i < num; ++i) { + if (pe[i].m_socket != NULL) { + if (FD_ISSET(pe[i].m_socket->m_fd, &readSet)) { + pe[i].m_revents |= kPOLLIN; + } + if (FD_ISSET(pe[i].m_socket->m_fd, &writeSet)) { + pe[i].m_revents |= kPOLLOUT; + } + if (FD_ISSET(pe[i].m_socket->m_fd, &errSet)) { + pe[i].m_revents |= kPOLLERR; + } + } + if (pe[i].m_revents != 0) { + ++n; + } + } + + return n; +} + +#endif + +void +CArchNetworkBSD::unblockPollSocket(CArchThread thread) +{ + const int* unblockPipe = getUnblockPipeForThread(thread); + if (unblockPipe != NULL) { + char dummy = 0; + int ignore; + + ignore = write(unblockPipe[1], &dummy, 1); + } +} + +size_t +CArchNetworkBSD::readSocket(CArchSocket s, void* buf, size_t len) +{ + assert(s != NULL); + + ssize_t n = read(s->m_fd, buf, len); + if (n == -1) { + if (errno == EINTR || errno == EAGAIN) { + return 0; + } + throwError(errno); + } + return n; +} + +size_t +CArchNetworkBSD::writeSocket(CArchSocket s, const void* buf, size_t len) +{ + assert(s != NULL); + + ssize_t n = write(s->m_fd, buf, len); + if (n == -1) { + if (errno == EINTR || errno == EAGAIN) { + return 0; + } + throwError(errno); + } + return n; +} + +void +CArchNetworkBSD::throwErrorOnSocket(CArchSocket s) +{ + assert(s != NULL); + + // get the error from the socket layer + int err = 0; + socklen_t size = (socklen_t)sizeof(err); + if (getsockopt(s->m_fd, SOL_SOCKET, SO_ERROR, + (optval_t*)&err, &size) == -1) { + err = errno; + } + + // throw if there's an error + if (err != 0) { + throwError(err); + } +} + +void +CArchNetworkBSD::setBlockingOnSocket(int fd, bool blocking) +{ + assert(fd != -1); + + int mode = fcntl(fd, F_GETFL, 0); + if (mode == -1) { + throwError(errno); + } + if (blocking) { + mode &= ~O_NONBLOCK; + } + else { + mode |= O_NONBLOCK; + } + if (fcntl(fd, F_SETFL, mode) == -1) { + throwError(errno); + } +} + +bool +CArchNetworkBSD::setNoDelayOnSocket(CArchSocket s, bool noDelay) +{ + assert(s != NULL); + + // get old state + int oflag; + socklen_t size = (socklen_t)sizeof(oflag); + if (getsockopt(s->m_fd, IPPROTO_TCP, TCP_NODELAY, + (optval_t*)&oflag, &size) == -1) { + throwError(errno); + } + + int flag = noDelay ? 1 : 0; + size = (socklen_t)sizeof(flag); + if (setsockopt(s->m_fd, IPPROTO_TCP, TCP_NODELAY, + (optval_t*)&flag, size) == -1) { + throwError(errno); + } + + return (oflag != 0); +} + +bool +CArchNetworkBSD::setReuseAddrOnSocket(CArchSocket s, bool reuse) +{ + assert(s != NULL); + + // get old state + int oflag; + socklen_t size = (socklen_t)sizeof(oflag); + if (getsockopt(s->m_fd, SOL_SOCKET, SO_REUSEADDR, + (optval_t*)&oflag, &size) == -1) { + throwError(errno); + } + + int flag = reuse ? 1 : 0; + size = (socklen_t)sizeof(flag); + if (setsockopt(s->m_fd, SOL_SOCKET, SO_REUSEADDR, + (optval_t*)&flag, size) == -1) { + throwError(errno); + } + + return (oflag != 0); +} + +std::string +CArchNetworkBSD::getHostName() +{ + char name[256]; + if (gethostname(name, sizeof(name)) == -1) { + name[0] = '\0'; + } + else { + name[sizeof(name) - 1] = '\0'; + } + return name; +} + +CArchNetAddress +CArchNetworkBSD::newAnyAddr(EAddressFamily family) +{ + // allocate address + CArchNetAddressImpl* addr = new CArchNetAddressImpl; + + // fill it in + switch (family) { + case kINET: { + struct sockaddr_in* ipAddr = + reinterpret_cast(&addr->m_addr); + ipAddr->sin_family = AF_INET; + ipAddr->sin_port = 0; + ipAddr->sin_addr.s_addr = INADDR_ANY; + addr->m_len = (socklen_t)sizeof(struct sockaddr_in); + break; + } + + default: + delete addr; + assert(0 && "invalid family"); + } + + return addr; +} + +CArchNetAddress +CArchNetworkBSD::copyAddr(CArchNetAddress addr) +{ + assert(addr != NULL); + + // allocate and copy address + return new CArchNetAddressImpl(*addr); +} + +CArchNetAddress +CArchNetworkBSD::nameToAddr(const std::string& name) +{ + // allocate address + CArchNetAddressImpl* addr = new CArchNetAddressImpl; + + // try to convert assuming an IPv4 dot notation address + struct sockaddr_in inaddr; + memset(&inaddr, 0, sizeof(inaddr)); + if (inet_aton(name.c_str(), &inaddr.sin_addr) != 0) { + // it's a dot notation address + addr->m_len = (socklen_t)sizeof(struct sockaddr_in); + inaddr.sin_family = AF_INET; + inaddr.sin_port = 0; + memcpy(&addr->m_addr, &inaddr, addr->m_len); + } + + else { + // mutexed address lookup (ugh) + ARCH->lockMutex(m_mutex); + struct hostent* info = gethostbyname(name.c_str()); + if (info == NULL) { + ARCH->unlockMutex(m_mutex); + delete addr; + throwNameError(h_errno); + } + + // copy over address (only IPv4 currently supported) + if (info->h_addrtype == AF_INET) { + addr->m_len = (socklen_t)sizeof(struct sockaddr_in); + inaddr.sin_family = info->h_addrtype; + inaddr.sin_port = 0; + memcpy(&inaddr.sin_addr, info->h_addr_list[0], + sizeof(inaddr.sin_addr)); + memcpy(&addr->m_addr, &inaddr, addr->m_len); + } + else { + ARCH->unlockMutex(m_mutex); + delete addr; + throw XArchNetworkNameUnsupported( + "The requested name is valid but " + "does not have a supported address family"); + } + + // done with static buffer + ARCH->unlockMutex(m_mutex); + } + + return addr; +} + +void +CArchNetworkBSD::closeAddr(CArchNetAddress addr) +{ + assert(addr != NULL); + + delete addr; +} + +std::string +CArchNetworkBSD::addrToName(CArchNetAddress addr) +{ + assert(addr != NULL); + + // mutexed name lookup (ugh) + ARCH->lockMutex(m_mutex); + struct hostent* info = gethostbyaddr( + reinterpret_cast(&addr->m_addr), + addr->m_len, addr->m_addr.sa_family); + if (info == NULL) { + ARCH->unlockMutex(m_mutex); + throwNameError(h_errno); + } + + // save (primary) name + std::string name = info->h_name; + + // done with static buffer + ARCH->unlockMutex(m_mutex); + + return name; +} + +std::string +CArchNetworkBSD::addrToString(CArchNetAddress addr) +{ + assert(addr != NULL); + + switch (getAddrFamily(addr)) { + case kINET: { + struct sockaddr_in* ipAddr = + reinterpret_cast(&addr->m_addr); + ARCH->lockMutex(m_mutex); + std::string s = inet_ntoa(ipAddr->sin_addr); + ARCH->unlockMutex(m_mutex); + return s; + } + + default: + assert(0 && "unknown address family"); + return ""; + } +} + +IArchNetwork::EAddressFamily +CArchNetworkBSD::getAddrFamily(CArchNetAddress addr) +{ + assert(addr != NULL); + + switch (addr->m_addr.sa_family) { + case AF_INET: + return kINET; + + default: + return kUNKNOWN; + } +} + +void +CArchNetworkBSD::setAddrPort(CArchNetAddress addr, int port) +{ + assert(addr != NULL); + + switch (getAddrFamily(addr)) { + case kINET: { + struct sockaddr_in* ipAddr = + reinterpret_cast(&addr->m_addr); + ipAddr->sin_port = htons(port); + break; + } + + default: + assert(0 && "unknown address family"); + break; + } +} + +int +CArchNetworkBSD::getAddrPort(CArchNetAddress addr) +{ + assert(addr != NULL); + + switch (getAddrFamily(addr)) { + case kINET: { + struct sockaddr_in* ipAddr = + reinterpret_cast(&addr->m_addr); + return ntohs(ipAddr->sin_port); + } + + default: + assert(0 && "unknown address family"); + return 0; + } +} + +bool +CArchNetworkBSD::isAnyAddr(CArchNetAddress addr) +{ + assert(addr != NULL); + + switch (getAddrFamily(addr)) { + case kINET: { + struct sockaddr_in* ipAddr = + reinterpret_cast(&addr->m_addr); + return (ipAddr->sin_addr.s_addr == INADDR_ANY && + addr->m_len == (socklen_t)sizeof(struct sockaddr_in)); + } + + default: + assert(0 && "unknown address family"); + return true; + } +} + +bool +CArchNetworkBSD::isEqualAddr(CArchNetAddress a, CArchNetAddress b) +{ + return (a->m_len == b->m_len && + memcmp(&a->m_addr, &b->m_addr, a->m_len) == 0); +} + +const int* +CArchNetworkBSD::getUnblockPipe() +{ + CArchMultithreadPosix* mt = CArchMultithreadPosix::getInstance(); + CArchThread thread = mt->newCurrentThread(); + const int* p = getUnblockPipeForThread(thread); + ARCH->closeThread(thread); + return p; +} + +const int* +CArchNetworkBSD::getUnblockPipeForThread(CArchThread thread) +{ + CArchMultithreadPosix* mt = CArchMultithreadPosix::getInstance(); + int* unblockPipe = (int*)mt->getNetworkDataForThread(thread); + if (unblockPipe == NULL) { + unblockPipe = new int[2]; + if (pipe(unblockPipe) != -1) { + try { + setBlockingOnSocket(unblockPipe[0], false); + mt->setNetworkDataForCurrentThread(unblockPipe); + } + catch (...) { + delete[] unblockPipe; + unblockPipe = NULL; + } + } + else { + delete[] unblockPipe; + unblockPipe = NULL; + } + } + return unblockPipe; +} + +void +CArchNetworkBSD::throwError(int err) +{ + switch (err) { + case EINTR: + ARCH->testCancelThread(); + throw XArchNetworkInterrupted(new XArchEvalUnix(err)); + + case EACCES: + case EPERM: + throw XArchNetworkAccess(new XArchEvalUnix(err)); + + case ENFILE: + case EMFILE: + case ENODEV: + case ENOBUFS: + case ENOMEM: + case ENETDOWN: +#if defined(ENOSR) + case ENOSR: +#endif + throw XArchNetworkResource(new XArchEvalUnix(err)); + + case EPROTOTYPE: + case EPROTONOSUPPORT: + case EAFNOSUPPORT: + case EPFNOSUPPORT: + case ESOCKTNOSUPPORT: + case EINVAL: + case ENOPROTOOPT: + case EOPNOTSUPP: + case ESHUTDOWN: +#if defined(ENOPKG) + case ENOPKG: +#endif + throw XArchNetworkSupport(new XArchEvalUnix(err)); + + case EIO: + throw XArchNetworkIO(new XArchEvalUnix(err)); + + case EADDRNOTAVAIL: + throw XArchNetworkNoAddress(new XArchEvalUnix(err)); + + case EADDRINUSE: + throw XArchNetworkAddressInUse(new XArchEvalUnix(err)); + + case EHOSTUNREACH: + case ENETUNREACH: + throw XArchNetworkNoRoute(new XArchEvalUnix(err)); + + case ENOTCONN: + throw XArchNetworkNotConnected(new XArchEvalUnix(err)); + + case EPIPE: + throw XArchNetworkShutdown(new XArchEvalUnix(err)); + + case ECONNABORTED: + case ECONNRESET: + throw XArchNetworkDisconnected(new XArchEvalUnix(err)); + + case ECONNREFUSED: + throw XArchNetworkConnectionRefused(new XArchEvalUnix(err)); + + case EHOSTDOWN: + case ETIMEDOUT: + throw XArchNetworkTimedOut(new XArchEvalUnix(err)); + + default: + throw XArchNetwork(new XArchEvalUnix(err)); + } +} + +void +CArchNetworkBSD::throwNameError(int err) +{ + static const char* s_msg[] = { + "The specified host is unknown", + "The requested name is valid but does not have an IP address", + "A non-recoverable name server error occurred", + "A temporary error occurred on an authoritative name server", + "An unknown name server error occurred" + }; + + switch (err) { + case HOST_NOT_FOUND: + throw XArchNetworkNameUnknown(s_msg[0]); + + case NO_DATA: + throw XArchNetworkNameNoAddress(s_msg[1]); + + case NO_RECOVERY: + throw XArchNetworkNameFailure(s_msg[2]); + + case TRY_AGAIN: + throw XArchNetworkNameUnavailable(s_msg[3]); + + default: + throw XArchNetworkName(s_msg[4]); + } +} diff --git a/src/lib/arch/CArchNetworkBSD.h b/src/lib/arch/CArchNetworkBSD.h new file mode 100644 index 00000000..4f0ea5eb --- /dev/null +++ b/src/lib/arch/CArchNetworkBSD.h @@ -0,0 +1,105 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHNETWORKBSD_H +#define CARCHNETWORKBSD_H + +#include "IArchNetwork.h" +#include "IArchMultithread.h" +#if HAVE_SYS_TYPES_H +# include +#endif +#if HAVE_SYS_SOCKET_H +# include +#endif + +#if !HAVE_SOCKLEN_T +typedef int socklen_t; +#endif + +// old systems may use char* for [gs]etsockopt()'s optval argument. +// this should be void on modern systems but char is forwards +// compatible so we always use it. +typedef char optval_t; + +#define ARCH_NETWORK CArchNetworkBSD + +class CArchSocketImpl { +public: + int m_fd; + int m_refCount; +}; + +class CArchNetAddressImpl { +public: + CArchNetAddressImpl() : m_len(sizeof(m_addr)) { } + +public: + struct sockaddr m_addr; + socklen_t m_len; +}; + +//! Berkeley (BSD) sockets implementation of IArchNetwork +class CArchNetworkBSD : public IArchNetwork { +public: + CArchNetworkBSD(); + virtual ~CArchNetworkBSD(); + + virtual void init(); + + // IArchNetwork overrides + virtual CArchSocket newSocket(EAddressFamily, ESocketType); + virtual CArchSocket copySocket(CArchSocket s); virtual void closeSocket(CArchSocket s); + virtual void closeSocketForRead(CArchSocket s); + virtual void closeSocketForWrite(CArchSocket s); + virtual void bindSocket(CArchSocket s, CArchNetAddress addr); + virtual void listenOnSocket(CArchSocket s); + virtual CArchSocket acceptSocket(CArchSocket s, CArchNetAddress* addr); + virtual bool connectSocket(CArchSocket s, CArchNetAddress name); + virtual int pollSocket(CPollEntry[], int num, double timeout); + virtual void unblockPollSocket(CArchThread thread); + virtual size_t readSocket(CArchSocket s, void* buf, size_t len); + virtual size_t writeSocket(CArchSocket s, + const void* buf, size_t len); + virtual void throwErrorOnSocket(CArchSocket); + virtual bool setNoDelayOnSocket(CArchSocket, bool noDelay); + virtual bool setReuseAddrOnSocket(CArchSocket, bool reuse); + virtual std::string getHostName(); + virtual CArchNetAddress newAnyAddr(EAddressFamily); + virtual CArchNetAddress copyAddr(CArchNetAddress); + virtual CArchNetAddress nameToAddr(const std::string&); + virtual void closeAddr(CArchNetAddress); + virtual std::string addrToName(CArchNetAddress); + virtual std::string addrToString(CArchNetAddress); + virtual EAddressFamily getAddrFamily(CArchNetAddress); + virtual void setAddrPort(CArchNetAddress, int port); + virtual int getAddrPort(CArchNetAddress); + virtual bool isAnyAddr(CArchNetAddress); + virtual bool isEqualAddr(CArchNetAddress, CArchNetAddress); + +private: + const int* getUnblockPipe(); + const int* getUnblockPipeForThread(CArchThread); + void setBlockingOnSocket(int fd, bool blocking); + void throwError(int); + void throwNameError(int); + +private: + CArchMutex m_mutex; +}; + +#endif diff --git a/src/lib/arch/CArchNetworkWinsock.cpp b/src/lib/arch/CArchNetworkWinsock.cpp new file mode 100644 index 00000000..55bfba6a --- /dev/null +++ b/src/lib/arch/CArchNetworkWinsock.cpp @@ -0,0 +1,942 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchNetworkWinsock.h" +#include "CArch.h" +#include "CArchMultithreadWindows.h" +#include "IArchMultithread.h" +#include "XArchWindows.h" +#include + +static const int s_family[] = { + PF_UNSPEC, + PF_INET +}; +static const int s_type[] = { + SOCK_DGRAM, + SOCK_STREAM +}; + +static SOCKET (PASCAL FAR *accept_winsock)(SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen); +static int (PASCAL FAR *bind_winsock)(SOCKET s, const struct sockaddr FAR *addr, int namelen); +static int (PASCAL FAR *close_winsock)(SOCKET s); +static int (PASCAL FAR *connect_winsock)(SOCKET s, const struct sockaddr FAR *name, int namelen); +static int (PASCAL FAR *gethostname_winsock)(char FAR * name, int namelen); +static int (PASCAL FAR *getsockerror_winsock)(void); +static int (PASCAL FAR *getsockopt_winsock)(SOCKET s, int level, int optname, void FAR * optval, int FAR *optlen); +static u_short (PASCAL FAR *htons_winsock)(u_short v); +static char FAR * (PASCAL FAR *inet_ntoa_winsock)(struct in_addr in); +static unsigned long (PASCAL FAR *inet_addr_winsock)(const char FAR * cp); +static int (PASCAL FAR *ioctl_winsock)(SOCKET s, int cmd, void FAR * data); +static int (PASCAL FAR *listen_winsock)(SOCKET s, int backlog); +static u_short (PASCAL FAR *ntohs_winsock)(u_short v); +static int (PASCAL FAR *recv_winsock)(SOCKET s, void FAR * buf, int len, int flags); +static int (PASCAL FAR *select_winsock)(int nfds, fd_set FAR *readfds, fd_set FAR *writefds, fd_set FAR *exceptfds, const struct timeval FAR *timeout); +static int (PASCAL FAR *send_winsock)(SOCKET s, const void FAR * buf, int len, int flags); +static int (PASCAL FAR *setsockopt_winsock)(SOCKET s, int level, int optname, const void FAR * optval, int optlen); +static int (PASCAL FAR *shutdown_winsock)(SOCKET s, int how); +static SOCKET (PASCAL FAR *socket_winsock)(int af, int type, int protocol); +static struct hostent FAR * (PASCAL FAR *gethostbyaddr_winsock)(const char FAR * addr, int len, int type); +static struct hostent FAR * (PASCAL FAR *gethostbyname_winsock)(const char FAR * name); +static int (PASCAL FAR *WSACleanup_winsock)(void); +static int (PASCAL FAR *WSAFDIsSet_winsock)(SOCKET, fd_set FAR * fdset); +static WSAEVENT (PASCAL FAR *WSACreateEvent_winsock)(void); +static BOOL (PASCAL FAR *WSACloseEvent_winsock)(WSAEVENT); +static BOOL (PASCAL FAR *WSASetEvent_winsock)(WSAEVENT); +static BOOL (PASCAL FAR *WSAResetEvent_winsock)(WSAEVENT); +static int (PASCAL FAR *WSAEventSelect_winsock)(SOCKET, WSAEVENT, long); +static DWORD (PASCAL FAR *WSAWaitForMultipleEvents_winsock)(DWORD, const WSAEVENT FAR*, BOOL, DWORD, BOOL); +static int (PASCAL FAR *WSAEnumNetworkEvents_winsock)(SOCKET, WSAEVENT, LPWSANETWORKEVENTS); + +#undef FD_ISSET +#define FD_ISSET(fd, set) WSAFDIsSet_winsock((SOCKET)(fd), (fd_set FAR *)(set)) + +#define setfunc(var, name, type) var = (type)netGetProcAddress(module, #name) + +static HMODULE s_networkModule = NULL; + +static +FARPROC +netGetProcAddress(HMODULE module, LPCSTR name) +{ + FARPROC func = ::GetProcAddress(module, name); + if (!func) { + throw XArchNetworkSupport(""); + } + return func; +} + +CArchNetAddressImpl* +CArchNetAddressImpl::alloc(size_t size) +{ + size_t totalSize = size + ADDR_HDR_SIZE; + CArchNetAddressImpl* addr = (CArchNetAddressImpl*)malloc(totalSize); + addr->m_len = (int)size; + return addr; +} + + +// +// CArchNetworkWinsock +// + +CArchNetworkWinsock::CArchNetworkWinsock() +{ +} + +CArchNetworkWinsock::~CArchNetworkWinsock() +{ + if (s_networkModule != NULL) { + WSACleanup_winsock(); + ::FreeLibrary(s_networkModule); + + WSACleanup_winsock = NULL; + s_networkModule = NULL; + } + ARCH->closeMutex(m_mutex); +} + +void +CArchNetworkWinsock::init() +{ + static const char* s_library[] = { "ws2_32.dll" }; + + assert(WSACleanup_winsock == NULL); + assert(s_networkModule == NULL); + + // try each winsock library + for (size_t i = 0; i < sizeof(s_library) / sizeof(s_library[0]); ++i) { + try { + initModule((HMODULE)::LoadLibrary(s_library[i])); + m_mutex = ARCH->newMutex(); + return; + } + catch (XArchNetwork&) { + // ignore + } + } + + // can't initialize any library + throw XArchNetworkSupport("Cannot load winsock library"); +} + +void +CArchNetworkWinsock::initModule(HMODULE module) +{ + if (module == NULL) { + throw XArchNetworkSupport(""); + } + + // get startup function address + int (PASCAL FAR *startup)(WORD, LPWSADATA); + setfunc(startup, WSAStartup, int(PASCAL FAR*)(WORD, LPWSADATA)); + + // startup network library + WORD version = MAKEWORD(2 /*major*/, 0 /*minor*/); + WSADATA data; + int err = startup(version, &data); + if (data.wVersion != version) { + throw XArchNetworkSupport(new XArchEvalWinsock(err)); + } + if (err != 0) { + // some other initialization error + throwError(err); + } + + // get function addresses + setfunc(accept_winsock, accept, SOCKET (PASCAL FAR *)(SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen)); + setfunc(bind_winsock, bind, int (PASCAL FAR *)(SOCKET s, const struct sockaddr FAR *addr, int namelen)); + setfunc(close_winsock, closesocket, int (PASCAL FAR *)(SOCKET s)); + setfunc(connect_winsock, connect, int (PASCAL FAR *)(SOCKET s, const struct sockaddr FAR *name, int namelen)); + setfunc(gethostname_winsock, gethostname, int (PASCAL FAR *)(char FAR * name, int namelen)); + setfunc(getsockerror_winsock, WSAGetLastError, int (PASCAL FAR *)(void)); + setfunc(getsockopt_winsock, getsockopt, int (PASCAL FAR *)(SOCKET s, int level, int optname, void FAR * optval, int FAR *optlen)); + setfunc(htons_winsock, htons, u_short (PASCAL FAR *)(u_short v)); + setfunc(inet_ntoa_winsock, inet_ntoa, char FAR * (PASCAL FAR *)(struct in_addr in)); + setfunc(inet_addr_winsock, inet_addr, unsigned long (PASCAL FAR *)(const char FAR * cp)); + setfunc(ioctl_winsock, ioctlsocket, int (PASCAL FAR *)(SOCKET s, int cmd, void FAR *)); + setfunc(listen_winsock, listen, int (PASCAL FAR *)(SOCKET s, int backlog)); + setfunc(ntohs_winsock, ntohs, u_short (PASCAL FAR *)(u_short v)); + setfunc(recv_winsock, recv, int (PASCAL FAR *)(SOCKET s, void FAR * buf, int len, int flags)); + setfunc(select_winsock, select, int (PASCAL FAR *)(int nfds, fd_set FAR *readfds, fd_set FAR *writefds, fd_set FAR *exceptfds, const struct timeval FAR *timeout)); + setfunc(send_winsock, send, int (PASCAL FAR *)(SOCKET s, const void FAR * buf, int len, int flags)); + setfunc(setsockopt_winsock, setsockopt, int (PASCAL FAR *)(SOCKET s, int level, int optname, const void FAR * optval, int optlen)); + setfunc(shutdown_winsock, shutdown, int (PASCAL FAR *)(SOCKET s, int how)); + setfunc(socket_winsock, socket, SOCKET (PASCAL FAR *)(int af, int type, int protocol)); + setfunc(gethostbyaddr_winsock, gethostbyaddr, struct hostent FAR * (PASCAL FAR *)(const char FAR * addr, int len, int type)); + setfunc(gethostbyname_winsock, gethostbyname, struct hostent FAR * (PASCAL FAR *)(const char FAR * name)); + setfunc(WSACleanup_winsock, WSACleanup, int (PASCAL FAR *)(void)); + setfunc(WSAFDIsSet_winsock, __WSAFDIsSet, int (PASCAL FAR *)(SOCKET, fd_set FAR *)); + setfunc(WSACreateEvent_winsock, WSACreateEvent, WSAEVENT (PASCAL FAR *)(void)); + setfunc(WSACloseEvent_winsock, WSACloseEvent, BOOL (PASCAL FAR *)(WSAEVENT)); + setfunc(WSASetEvent_winsock, WSASetEvent, BOOL (PASCAL FAR *)(WSAEVENT)); + setfunc(WSAResetEvent_winsock, WSAResetEvent, BOOL (PASCAL FAR *)(WSAEVENT)); + setfunc(WSAEventSelect_winsock, WSAEventSelect, int (PASCAL FAR *)(SOCKET, WSAEVENT, long)); + setfunc(WSAWaitForMultipleEvents_winsock, WSAWaitForMultipleEvents, DWORD (PASCAL FAR *)(DWORD, const WSAEVENT FAR*, BOOL, DWORD, BOOL)); + setfunc(WSAEnumNetworkEvents_winsock, WSAEnumNetworkEvents, int (PASCAL FAR *)(SOCKET, WSAEVENT, LPWSANETWORKEVENTS)); + + s_networkModule = module; +} + +CArchSocket +CArchNetworkWinsock::newSocket(EAddressFamily family, ESocketType type) +{ + // create socket + SOCKET fd = socket_winsock(s_family[family], s_type[type], 0); + if (fd == INVALID_SOCKET) { + throwError(getsockerror_winsock()); + } + try { + setBlockingOnSocket(fd, false); + } + catch (...) { + close_winsock(fd); + throw; + } + + // allocate socket object + CArchSocketImpl* socket = new CArchSocketImpl; + socket->m_socket = fd; + socket->m_refCount = 1; + socket->m_event = WSACreateEvent_winsock(); + socket->m_pollWrite = true; + return socket; +} + +CArchSocket +CArchNetworkWinsock::copySocket(CArchSocket s) +{ + assert(s != NULL); + + // ref the socket and return it + ARCH->lockMutex(m_mutex); + ++s->m_refCount; + ARCH->unlockMutex(m_mutex); + return s; +} + +void +CArchNetworkWinsock::closeSocket(CArchSocket s) +{ + assert(s != NULL); + + // unref the socket and note if it should be released + ARCH->lockMutex(m_mutex); + const bool doClose = (--s->m_refCount == 0); + ARCH->unlockMutex(m_mutex); + + // close the socket if necessary + if (doClose) { + if (close_winsock(s->m_socket) == SOCKET_ERROR) { + // close failed. restore the last ref and throw. + int err = getsockerror_winsock(); + ARCH->lockMutex(m_mutex); + ++s->m_refCount; + ARCH->unlockMutex(m_mutex); + throwError(err); + } + WSACloseEvent_winsock(s->m_event); + delete s; + } +} + +void +CArchNetworkWinsock::closeSocketForRead(CArchSocket s) +{ + assert(s != NULL); + + if (shutdown_winsock(s->m_socket, SD_RECEIVE) == SOCKET_ERROR) { + if (getsockerror_winsock() != WSAENOTCONN) { + throwError(getsockerror_winsock()); + } + } +} + +void +CArchNetworkWinsock::closeSocketForWrite(CArchSocket s) +{ + assert(s != NULL); + + if (shutdown_winsock(s->m_socket, SD_SEND) == SOCKET_ERROR) { + if (getsockerror_winsock() != WSAENOTCONN) { + throwError(getsockerror_winsock()); + } + } +} + +void +CArchNetworkWinsock::bindSocket(CArchSocket s, CArchNetAddress addr) +{ + assert(s != NULL); + assert(addr != NULL); + + if (bind_winsock(s->m_socket, &addr->m_addr, addr->m_len) == SOCKET_ERROR) { + throwError(getsockerror_winsock()); + } +} + +void +CArchNetworkWinsock::listenOnSocket(CArchSocket s) +{ + assert(s != NULL); + + // hardcoding backlog + if (listen_winsock(s->m_socket, 3) == SOCKET_ERROR) { + throwError(getsockerror_winsock()); + } +} + +CArchSocket +CArchNetworkWinsock::acceptSocket(CArchSocket s, CArchNetAddress* addr) +{ + assert(s != NULL); + + // create new socket and temporary address + CArchSocketImpl* socket = new CArchSocketImpl; + CArchNetAddress tmp = CArchNetAddressImpl::alloc(sizeof(struct sockaddr)); + + // accept on socket + SOCKET fd = accept_winsock(s->m_socket, &tmp->m_addr, &tmp->m_len); + if (fd == INVALID_SOCKET) { + int err = getsockerror_winsock(); + delete socket; + free(tmp); + *addr = NULL; + if (err == WSAEWOULDBLOCK) { + return NULL; + } + throwError(err); + } + + try { + setBlockingOnSocket(fd, false); + } + catch (...) { + close_winsock(fd); + delete socket; + free(tmp); + *addr = NULL; + throw; + } + + // initialize socket + socket->m_socket = fd; + socket->m_refCount = 1; + socket->m_event = WSACreateEvent_winsock(); + socket->m_pollWrite = true; + + // copy address if requested + if (addr != NULL) { + *addr = ARCH->copyAddr(tmp); + } + + free(tmp); + return socket; +} + +bool +CArchNetworkWinsock::connectSocket(CArchSocket s, CArchNetAddress addr) +{ + assert(s != NULL); + assert(addr != NULL); + + if (connect_winsock(s->m_socket, &addr->m_addr, + addr->m_len) == SOCKET_ERROR) { + if (getsockerror_winsock() == WSAEISCONN) { + return true; + } + if (getsockerror_winsock() == WSAEWOULDBLOCK) { + return false; + } + throwError(getsockerror_winsock()); + } + return true; +} + +int +CArchNetworkWinsock::pollSocket(CPollEntry pe[], int num, double timeout) +{ + int i; + DWORD n; + + // prepare sockets and wait list + bool canWrite = false; + WSAEVENT* events = (WSAEVENT*)alloca((num + 1) * sizeof(WSAEVENT)); + for (i = 0, n = 0; i < num; ++i) { + // reset return flags + pe[i].m_revents = 0; + + // set invalid flag if socket is bogus then go to next socket + if (pe[i].m_socket == NULL) { + pe[i].m_revents |= kPOLLNVAL; + continue; + } + + // select desired events + long socketEvents = 0; + if ((pe[i].m_events & kPOLLIN) != 0) { + socketEvents |= FD_READ | FD_ACCEPT | FD_CLOSE; + } + if ((pe[i].m_events & kPOLLOUT) != 0) { + socketEvents |= FD_WRITE | FD_CONNECT | FD_CLOSE; + + // if m_pollWrite is false then we assume the socket is + // writable. winsock doesn't signal writability except + // when the state changes from unwritable. + if (!pe[i].m_socket->m_pollWrite) { + canWrite = true; + pe[i].m_revents |= kPOLLOUT; + } + } + + // if no events then ignore socket + if (socketEvents == 0) { + continue; + } + + // select socket for desired events + WSAEventSelect_winsock(pe[i].m_socket->m_socket, + pe[i].m_socket->m_event, socketEvents); + + // add socket event to wait list + events[n++] = pe[i].m_socket->m_event; + } + + // if no sockets then return immediately + if (n == 0) { + return 0; + } + + // add the unblock event + CArchMultithreadWindows* mt = CArchMultithreadWindows::getInstance(); + CArchThread thread = mt->newCurrentThread(); + WSAEVENT* unblockEvent = (WSAEVENT*)mt->getNetworkDataForThread(thread); + ARCH->closeThread(thread); + if (unblockEvent == NULL) { + unblockEvent = new WSAEVENT; + *unblockEvent = WSACreateEvent_winsock(); + mt->setNetworkDataForCurrentThread(unblockEvent); + } + events[n++] = *unblockEvent; + + // prepare timeout + DWORD t = (timeout < 0.0) ? INFINITE : (DWORD)(1000.0 * timeout); + if (canWrite) { + // if we know we can write then don't block + t = 0; + } + + // wait + DWORD result = WSAWaitForMultipleEvents_winsock(n, events, FALSE, t, FALSE); + + // reset the unblock event + WSAResetEvent_winsock(*unblockEvent); + + // handle results + if (result == WSA_WAIT_FAILED) { + if (getsockerror_winsock() == WSAEINTR) { + // interrupted system call + ARCH->testCancelThread(); + return 0; + } + throwError(getsockerror_winsock()); + } + if (result == WSA_WAIT_TIMEOUT && !canWrite) { + return 0; + } + if (result == WSA_WAIT_EVENT_0 + n - 1) { + // the unblock event was signalled + return 0; + } + for (i = 0, n = 0; i < num; ++i) { + // skip events we didn't check + if (pe[i].m_socket == NULL || + (pe[i].m_events & (kPOLLIN | kPOLLOUT)) == 0) { + continue; + } + + // get events + WSANETWORKEVENTS info; + if (WSAEnumNetworkEvents_winsock(pe[i].m_socket->m_socket, + pe[i].m_socket->m_event, &info) == SOCKET_ERROR) { + continue; + } + if ((info.lNetworkEvents & FD_READ) != 0) { + pe[i].m_revents |= kPOLLIN; + } + if ((info.lNetworkEvents & FD_ACCEPT) != 0) { + pe[i].m_revents |= kPOLLIN; + } + if ((info.lNetworkEvents & FD_WRITE) != 0) { + pe[i].m_revents |= kPOLLOUT; + + // socket is now writable so don't bothing polling for + // writable until it becomes unwritable. + pe[i].m_socket->m_pollWrite = false; + } + if ((info.lNetworkEvents & FD_CONNECT) != 0) { + if (info.iErrorCode[FD_CONNECT_BIT] != 0) { + pe[i].m_revents |= kPOLLERR; + } + else { + pe[i].m_revents |= kPOLLOUT; + pe[i].m_socket->m_pollWrite = false; + } + } + if ((info.lNetworkEvents & FD_CLOSE) != 0) { + if (info.iErrorCode[FD_CLOSE_BIT] != 0) { + pe[i].m_revents |= kPOLLERR; + } + else { + if ((pe[i].m_events & kPOLLIN) != 0) { + pe[i].m_revents |= kPOLLIN; + } + if ((pe[i].m_events & kPOLLOUT) != 0) { + pe[i].m_revents |= kPOLLOUT; + } + } + } + if (pe[i].m_revents != 0) { + ++n; + } + } + + return (int)n; +} + +void +CArchNetworkWinsock::unblockPollSocket(CArchThread thread) +{ + // set the unblock event + CArchMultithreadWindows* mt = CArchMultithreadWindows::getInstance(); + WSAEVENT* unblockEvent = (WSAEVENT*)mt->getNetworkDataForThread(thread); + if (unblockEvent != NULL) { + WSASetEvent_winsock(*unblockEvent); + } +} + +size_t +CArchNetworkWinsock::readSocket(CArchSocket s, void* buf, size_t len) +{ + assert(s != NULL); + + int n = recv_winsock(s->m_socket, buf, (int)len, 0); + if (n == SOCKET_ERROR) { + int err = getsockerror_winsock(); + if (err == WSAEINTR || err == WSAEWOULDBLOCK) { + return 0; + } + throwError(err); + } + return static_cast(n); +} + +size_t +CArchNetworkWinsock::writeSocket(CArchSocket s, const void* buf, size_t len) +{ + assert(s != NULL); + + int n = send_winsock(s->m_socket, buf, (int)len, 0); + if (n == SOCKET_ERROR) { + int err = getsockerror_winsock(); + if (err == WSAEINTR) { + return 0; + } + if (err == WSAEWOULDBLOCK) { + s->m_pollWrite = true; + return 0; + } + throwError(err); + } + return static_cast(n); +} + +void +CArchNetworkWinsock::throwErrorOnSocket(CArchSocket s) +{ + assert(s != NULL); + + // get the error from the socket layer + int err = 0; + int size = sizeof(err); + if (getsockopt_winsock(s->m_socket, SOL_SOCKET, + SO_ERROR, &err, &size) == SOCKET_ERROR) { + err = getsockerror_winsock(); + } + + // throw if there's an error + if (err != 0) { + throwError(err); + } +} + +void +CArchNetworkWinsock::setBlockingOnSocket(SOCKET s, bool blocking) +{ + assert(s != 0); + + int flag = blocking ? 0 : 1; + if (ioctl_winsock(s, FIONBIO, &flag) == SOCKET_ERROR) { + throwError(getsockerror_winsock()); + } +} + +bool +CArchNetworkWinsock::setNoDelayOnSocket(CArchSocket s, bool noDelay) +{ + assert(s != NULL); + + // get old state + BOOL oflag; + int size = sizeof(oflag); + if (getsockopt_winsock(s->m_socket, IPPROTO_TCP, + TCP_NODELAY, &oflag, &size) == SOCKET_ERROR) { + throwError(getsockerror_winsock()); + } + + // set new state + BOOL flag = noDelay ? 1 : 0; + size = sizeof(flag); + if (setsockopt_winsock(s->m_socket, IPPROTO_TCP, + TCP_NODELAY, &flag, size) == SOCKET_ERROR) { + throwError(getsockerror_winsock()); + } + + return (oflag != 0); +} + +bool +CArchNetworkWinsock::setReuseAddrOnSocket(CArchSocket s, bool reuse) +{ + assert(s != NULL); + + // get old state + BOOL oflag; + int size = sizeof(oflag); + if (getsockopt_winsock(s->m_socket, SOL_SOCKET, + SO_REUSEADDR, &oflag, &size) == SOCKET_ERROR) { + throwError(getsockerror_winsock()); + } + + // set new state + BOOL flag = reuse ? 1 : 0; + size = sizeof(flag); + if (setsockopt_winsock(s->m_socket, SOL_SOCKET, + SO_REUSEADDR, &flag, size) == SOCKET_ERROR) { + throwError(getsockerror_winsock()); + } + + return (oflag != 0); +} + +std::string +CArchNetworkWinsock::getHostName() +{ + char name[256]; + if (gethostname_winsock(name, sizeof(name)) == -1) { + name[0] = '\0'; + } + else { + name[sizeof(name) - 1] = '\0'; + } + return name; +} + +CArchNetAddress +CArchNetworkWinsock::newAnyAddr(EAddressFamily family) +{ + CArchNetAddressImpl* addr = NULL; + switch (family) { + case kINET: { + addr = CArchNetAddressImpl::alloc(sizeof(struct sockaddr_in)); + struct sockaddr_in* ipAddr = TYPED_ADDR(struct sockaddr_in, addr); + ipAddr->sin_family = AF_INET; + ipAddr->sin_port = 0; + ipAddr->sin_addr.s_addr = INADDR_ANY; + break; + } + + default: + assert(0 && "invalid family"); + } + return addr; +} + +CArchNetAddress +CArchNetworkWinsock::copyAddr(CArchNetAddress addr) +{ + assert(addr != NULL); + + CArchNetAddressImpl* copy = CArchNetAddressImpl::alloc(addr->m_len); + memcpy(TYPED_ADDR(void, copy), TYPED_ADDR(void, addr), addr->m_len); + return copy; +} + +CArchNetAddress +CArchNetworkWinsock::nameToAddr(const std::string& name) +{ + // allocate address + CArchNetAddressImpl* addr = NULL; + + // try to convert assuming an IPv4 dot notation address + struct sockaddr_in inaddr; + memset(&inaddr, 0, sizeof(inaddr)); + inaddr.sin_family = AF_INET; + inaddr.sin_port = 0; + inaddr.sin_addr.s_addr = inet_addr_winsock(name.c_str()); + if (inaddr.sin_addr.s_addr != INADDR_NONE) { + // it's a dot notation address + addr = CArchNetAddressImpl::alloc(sizeof(struct sockaddr_in)); + memcpy(TYPED_ADDR(void, addr), &inaddr, addr->m_len); + } + + else { + // address lookup + struct hostent* info = gethostbyname_winsock(name.c_str()); + if (info == NULL) { + throwNameError(getsockerror_winsock()); + } + + // copy over address (only IPv4 currently supported) + if (info->h_addrtype == AF_INET) { + addr = CArchNetAddressImpl::alloc(sizeof(struct sockaddr_in)); + memcpy(&inaddr.sin_addr, info->h_addr_list[0], + sizeof(inaddr.sin_addr)); + memcpy(TYPED_ADDR(void, addr), &inaddr, addr->m_len); + } + else { + throw XArchNetworkNameUnsupported( + "The requested name is valid but " + "does not have a supported address family"); + } + } + + return addr; +} + +void +CArchNetworkWinsock::closeAddr(CArchNetAddress addr) +{ + assert(addr != NULL); + + free(addr); +} + +std::string +CArchNetworkWinsock::addrToName(CArchNetAddress addr) +{ + assert(addr != NULL); + + // name lookup + struct hostent* info = gethostbyaddr_winsock( + reinterpret_cast(&addr->m_addr), + addr->m_len, addr->m_addr.sa_family); + if (info == NULL) { + throwNameError(getsockerror_winsock()); + } + + // return (primary) name + return info->h_name; +} + +std::string +CArchNetworkWinsock::addrToString(CArchNetAddress addr) +{ + assert(addr != NULL); + + switch (getAddrFamily(addr)) { + case kINET: { + struct sockaddr_in* ipAddr = + reinterpret_cast(&addr->m_addr); + return inet_ntoa_winsock(ipAddr->sin_addr); + } + + default: + assert(0 && "unknown address family"); + return ""; + } +} + +IArchNetwork::EAddressFamily +CArchNetworkWinsock::getAddrFamily(CArchNetAddress addr) +{ + assert(addr != NULL); + + switch (addr->m_addr.sa_family) { + case AF_INET: + return kINET; + + default: + return kUNKNOWN; + } +} + +void +CArchNetworkWinsock::setAddrPort(CArchNetAddress addr, int port) +{ + assert(addr != NULL); + + switch (getAddrFamily(addr)) { + case kINET: { + struct sockaddr_in* ipAddr = + reinterpret_cast(&addr->m_addr); + ipAddr->sin_port = htons_winsock(static_cast(port)); + break; + } + + default: + assert(0 && "unknown address family"); + break; + } +} + +int +CArchNetworkWinsock::getAddrPort(CArchNetAddress addr) +{ + assert(addr != NULL); + + switch (getAddrFamily(addr)) { + case kINET: { + struct sockaddr_in* ipAddr = + reinterpret_cast(&addr->m_addr); + return ntohs_winsock(ipAddr->sin_port); + } + + default: + assert(0 && "unknown address family"); + return 0; + } +} + +bool +CArchNetworkWinsock::isAnyAddr(CArchNetAddress addr) +{ + assert(addr != NULL); + + switch (getAddrFamily(addr)) { + case kINET: { + struct sockaddr_in* ipAddr = + reinterpret_cast(&addr->m_addr); + return (addr->m_len == sizeof(struct sockaddr_in) && + ipAddr->sin_addr.s_addr == INADDR_ANY); + } + + default: + assert(0 && "unknown address family"); + return true; + } +} + +bool +CArchNetworkWinsock::isEqualAddr(CArchNetAddress a, CArchNetAddress b) +{ + return (a == b || (a->m_len == b->m_len && + memcmp(&a->m_addr, &b->m_addr, a->m_len) == 0)); +} + +void +CArchNetworkWinsock::throwError(int err) +{ + switch (err) { + case WSAEACCES: + throw XArchNetworkAccess(new XArchEvalWinsock(err)); + + case WSAEMFILE: + case WSAENOBUFS: + case WSAENETDOWN: + throw XArchNetworkResource(new XArchEvalWinsock(err)); + + case WSAEPROTOTYPE: + case WSAEPROTONOSUPPORT: + case WSAEAFNOSUPPORT: + case WSAEPFNOSUPPORT: + case WSAESOCKTNOSUPPORT: + case WSAEINVAL: + case WSAENOPROTOOPT: + case WSAEOPNOTSUPP: + case WSAESHUTDOWN: + case WSANOTINITIALISED: + case WSAVERNOTSUPPORTED: + case WSASYSNOTREADY: + throw XArchNetworkSupport(new XArchEvalWinsock(err)); + + case WSAEADDRNOTAVAIL: + throw XArchNetworkNoAddress(new XArchEvalWinsock(err)); + + case WSAEADDRINUSE: + throw XArchNetworkAddressInUse(new XArchEvalWinsock(err)); + + case WSAEHOSTUNREACH: + case WSAENETUNREACH: + throw XArchNetworkNoRoute(new XArchEvalWinsock(err)); + + case WSAENOTCONN: + throw XArchNetworkNotConnected(new XArchEvalWinsock(err)); + + case WSAEDISCON: + throw XArchNetworkShutdown(new XArchEvalWinsock(err)); + + case WSAENETRESET: + case WSAECONNABORTED: + case WSAECONNRESET: + throw XArchNetworkDisconnected(new XArchEvalWinsock(err)); + + case WSAECONNREFUSED: + throw XArchNetworkConnectionRefused(new XArchEvalWinsock(err)); + + case WSAEHOSTDOWN: + case WSAETIMEDOUT: + throw XArchNetworkTimedOut(new XArchEvalWinsock(err)); + + case WSAHOST_NOT_FOUND: + throw XArchNetworkNameUnknown(new XArchEvalWinsock(err)); + + case WSANO_DATA: + throw XArchNetworkNameNoAddress(new XArchEvalWinsock(err)); + + case WSANO_RECOVERY: + throw XArchNetworkNameFailure(new XArchEvalWinsock(err)); + + case WSATRY_AGAIN: + throw XArchNetworkNameUnavailable(new XArchEvalWinsock(err)); + + default: + throw XArchNetwork(new XArchEvalWinsock(err)); + } +} + +void +CArchNetworkWinsock::throwNameError(int err) +{ + switch (err) { + case WSAHOST_NOT_FOUND: + throw XArchNetworkNameUnknown(new XArchEvalWinsock(err)); + + case WSANO_DATA: + throw XArchNetworkNameNoAddress(new XArchEvalWinsock(err)); + + case WSANO_RECOVERY: + throw XArchNetworkNameFailure(new XArchEvalWinsock(err)); + + case WSATRY_AGAIN: + throw XArchNetworkNameUnavailable(new XArchEvalWinsock(err)); + + default: + throw XArchNetworkName(new XArchEvalWinsock(err)); + } +} diff --git a/src/lib/arch/CArchNetworkWinsock.h b/src/lib/arch/CArchNetworkWinsock.h new file mode 100644 index 00000000..c0c371b1 --- /dev/null +++ b/src/lib/arch/CArchNetworkWinsock.h @@ -0,0 +1,104 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHNETWORKWINSOCK_H +#define CARCHNETWORKWINSOCK_H + +#define WIN32_LEAN_AND_MEAN + +// declare no functions in winsock2 +#define INCL_WINSOCK_API_PROTOTYPES 0 +#define INCL_WINSOCK_API_TYPEDEFS 0 + +#include "IArchNetwork.h" +#include "IArchMultithread.h" +#include +#include + +#define ARCH_NETWORK CArchNetworkWinsock + +class CArchSocketImpl { +public: + SOCKET m_socket; + int m_refCount; + WSAEVENT m_event; + bool m_pollWrite; +}; + +class CArchNetAddressImpl { +public: + static CArchNetAddressImpl* alloc(size_t); + +public: + int m_len; + struct sockaddr m_addr; +}; +#define ADDR_HDR_SIZE offsetof(CArchNetAddressImpl, m_addr) +#define TYPED_ADDR(type_, addr_) (reinterpret_cast(&addr_->m_addr)) + +//! Win32 implementation of IArchNetwork +class CArchNetworkWinsock : public IArchNetwork { +public: + CArchNetworkWinsock(); + virtual ~CArchNetworkWinsock(); + + virtual void init(); + + // IArchNetwork overrides + virtual CArchSocket newSocket(EAddressFamily, ESocketType); + virtual CArchSocket copySocket(CArchSocket s); + virtual void closeSocket(CArchSocket s); + virtual void closeSocketForRead(CArchSocket s); + virtual void closeSocketForWrite(CArchSocket s); + virtual void bindSocket(CArchSocket s, CArchNetAddress addr); + virtual void listenOnSocket(CArchSocket s); + virtual CArchSocket acceptSocket(CArchSocket s, CArchNetAddress* addr); + virtual bool connectSocket(CArchSocket s, CArchNetAddress name); + virtual int pollSocket(CPollEntry[], int num, double timeout); + virtual void unblockPollSocket(CArchThread thread); + virtual size_t readSocket(CArchSocket s, void* buf, size_t len); + virtual size_t writeSocket(CArchSocket s, + const void* buf, size_t len); + virtual void throwErrorOnSocket(CArchSocket); + virtual bool setNoDelayOnSocket(CArchSocket, bool noDelay); + virtual bool setReuseAddrOnSocket(CArchSocket, bool reuse); + virtual std::string getHostName(); + virtual CArchNetAddress newAnyAddr(EAddressFamily); + virtual CArchNetAddress copyAddr(CArchNetAddress); + virtual CArchNetAddress nameToAddr(const std::string&); + virtual void closeAddr(CArchNetAddress); + virtual std::string addrToName(CArchNetAddress); + virtual std::string addrToString(CArchNetAddress); + virtual EAddressFamily getAddrFamily(CArchNetAddress); + virtual void setAddrPort(CArchNetAddress, int port); + virtual int getAddrPort(CArchNetAddress); + virtual bool isAnyAddr(CArchNetAddress); + virtual bool isEqualAddr(CArchNetAddress, CArchNetAddress); + +private: + void initModule(HMODULE); + + void setBlockingOnSocket(SOCKET, bool blocking); + + void throwError(int); + void throwNameError(int); + +private: + CArchMutex m_mutex; +}; + +#endif diff --git a/src/lib/arch/CArchPluginUnix.cpp b/src/lib/arch/CArchPluginUnix.cpp new file mode 100644 index 00000000..68f243ce --- /dev/null +++ b/src/lib/arch/CArchPluginUnix.cpp @@ -0,0 +1,31 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CArchPluginUnix.h" + +CArchPluginUnix::CArchPluginUnix() +{ +} + +CArchPluginUnix::~CArchPluginUnix() +{ +} + +void +CArchPluginUnix::init(void* eventTarget) +{ +} diff --git a/src/lib/arch/CArchPluginUnix.h b/src/lib/arch/CArchPluginUnix.h new file mode 100644 index 00000000..428feef2 --- /dev/null +++ b/src/lib/arch/CArchPluginUnix.h @@ -0,0 +1,32 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "IArchPlugin.h" + +#define ARCH_PLUGIN CArchPluginUnix + +//! Unix implementation of IArchPlugin +class CArchPluginUnix : public IArchPlugin { +public: + CArchPluginUnix(); + virtual ~CArchPluginUnix(); + + // IArchPlugin overrides + void init(void* eventTarget); +}; diff --git a/src/lib/arch/CArchPluginWindows.cpp b/src/lib/arch/CArchPluginWindows.cpp new file mode 100644 index 00000000..62b4da71 --- /dev/null +++ b/src/lib/arch/CArchPluginWindows.cpp @@ -0,0 +1,126 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CArchPluginWindows.h" +#include "XArchWindows.h" +#include "CLog.h" +#include "IEventQueue.h" +#include "CEvent.h" +#include "CScreen.h" +#include "IPlatformScreen.h" // temp + +#define WIN32_LEAN_AND_MEAN +#include +#include + +typedef int (*initFunc)(void (*sendEvent)(const char*, void*), void (*log)(const char*)); + +void* CArchPluginWindows::m_eventTarget = NULL; + +CArchPluginWindows::CArchPluginWindows() +{ +} + +CArchPluginWindows::~CArchPluginWindows() +{ +} + +void +CArchPluginWindows::init(void* eventTarget) +{ + m_eventTarget = eventTarget; + + CString dir = getPluginsDir(); + LOG((CLOG_DEBUG "plugins dir: %s", dir.c_str())); + + CString pattern = CString(dir).append("\\*.dll"); + std::vector plugins; + getFilenames(pattern, plugins); + + std::vector::iterator it; + for (it = plugins.begin(); it != plugins.end(); ++it) + load(*it); +} + +void +CArchPluginWindows::load(const CString& dllFilename) +{ + LOG((CLOG_DEBUG "loading plugin: %s", dllFilename.c_str())); + CString path = CString(getPluginsDir()).append("\\").append(dllFilename); + HINSTANCE library = LoadLibrary(path.c_str()); + if (library == NULL) + throw XArch(new XArchEvalWindows); + + initFunc initPlugin = (initFunc)GetProcAddress(library, "init"); + initPlugin(&sendEvent, &log); +} + +CString +CArchPluginWindows::getModuleDir() +{ + TCHAR c_modulePath[MAX_PATH]; + GetModuleFileName(NULL, c_modulePath, MAX_PATH); + if (GetLastError() != ERROR_SUCCESS) { + throw XArch(new XArchEvalWindows); + } + + CString modulePath(c_modulePath); + size_t lastSlash = modulePath.find_last_of("\\"); + + if (lastSlash != CString::npos) { + return modulePath.substr(0, lastSlash); + } + + throw XArch("could not get module path."); +} + +void +CArchPluginWindows::getFilenames(const CString& pattern, std::vector& filenames) +{ + WIN32_FIND_DATA data; + HANDLE find = FindFirstFile(pattern.c_str(), &data); + if (find == INVALID_HANDLE_VALUE) { + FindClose(find); + LOG((CLOG_DEBUG "plugins dir is empty: %s", pattern.c_str())); + return; + } + + do { + filenames.push_back(data.cFileName); + } while (FindNextFile(find, &data)); + + FindClose(find); +} + +CString CArchPluginWindows::getPluginsDir() +{ + return getModuleDir().append("\\").append(PLUGINS_DIR); +} + +void +sendEvent(const char* eventName, void* data) +{ + LOG((CLOG_DEBUG5 "plugin sending event")); + CEvent::Type type = EVENTQUEUE->getRegisteredType(eventName); + EVENTQUEUE->addEvent(CEvent(type, CArchPluginWindows::m_eventTarget, data)); +} + +void +log(const char* text) +{ + LOG((CLOG_DEBUG "plugin: %s", text)); +} diff --git a/src/lib/arch/CArchPluginWindows.h b/src/lib/arch/CArchPluginWindows.h new file mode 100644 index 00000000..9cc2f4ed --- /dev/null +++ b/src/lib/arch/CArchPluginWindows.h @@ -0,0 +1,47 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "IArchPlugin.h" +#include "CString.h" +#include + +#define ARCH_PLUGIN CArchPluginWindows + +class CScreen; + +//! Windows implementation of IArchPlugin +class CArchPluginWindows : public IArchPlugin { +public: + CArchPluginWindows(); + virtual ~CArchPluginWindows(); + + // IArchPlugin overrides + void init(void* eventTarget); + + static void* m_eventTarget; + +private: + CString getModuleDir(); + void getFilenames(const CString& pattern, std::vector& filenames); + void load(const CString& dllPath); + CString getPluginsDir(); +}; + +void sendEvent(const char* text, void* data); +void log(const char* text); diff --git a/src/lib/arch/CArchSleepUnix.cpp b/src/lib/arch/CArchSleepUnix.cpp new file mode 100644 index 00000000..9f5b5c62 --- /dev/null +++ b/src/lib/arch/CArchSleepUnix.cpp @@ -0,0 +1,91 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchSleepUnix.h" +#include "CArch.h" +#if TIME_WITH_SYS_TIME +# include +# include +#else +# if HAVE_SYS_TIME_H +# include +# else +# include +# endif +#endif +#if !HAVE_NANOSLEEP +# if HAVE_SYS_SELECT_H +# include +# endif +# if HAVE_SYS_TYPES_H +# include +# endif +# if HAVE_UNISTD_H +# include +# endif +#endif + +// +// CArchSleepUnix +// + +CArchSleepUnix::CArchSleepUnix() +{ + // do nothing +} + +CArchSleepUnix::~CArchSleepUnix() +{ + // do nothing +} + +void +CArchSleepUnix::sleep(double timeout) +{ + ARCH->testCancelThread(); + if (timeout < 0.0) { + return; + } + +#if HAVE_NANOSLEEP + // prep timeout + struct timespec t; + t.tv_sec = (long)timeout; + t.tv_nsec = (long)(1.0e+9 * (timeout - (double)t.tv_sec)); + + // wait + while (nanosleep(&t, &t) < 0) + ARCH->testCancelThread(); +#else + /* emulate nanosleep() with select() */ + double startTime = ARCH->time(); + double timeLeft = timeout; + while (timeLeft > 0.0) { + struct timeval timeout2; + timeout2.tv_sec = static_cast(timeLeft); + timeout2.tv_usec = static_cast(1.0e+6 * (timeLeft - + timeout2.tv_sec)); + select((SELECT_TYPE_ARG1) 0, + SELECT_TYPE_ARG234 NULL, + SELECT_TYPE_ARG234 NULL, + SELECT_TYPE_ARG234 NULL, + SELECT_TYPE_ARG5 &timeout2); + ARCH->testCancelThread(); + timeLeft = timeout - (ARCH->time() - startTime); + } +#endif +} diff --git a/src/lib/arch/CArchSleepUnix.h b/src/lib/arch/CArchSleepUnix.h new file mode 100644 index 00000000..245e52ef --- /dev/null +++ b/src/lib/arch/CArchSleepUnix.h @@ -0,0 +1,35 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHSLEEPUNIX_H +#define CARCHSLEEPUNIX_H + +#include "IArchSleep.h" + +#define ARCH_SLEEP CArchSleepUnix + +//! Unix implementation of IArchSleep +class CArchSleepUnix : public IArchSleep { +public: + CArchSleepUnix(); + virtual ~CArchSleepUnix(); + + // IArchSleep overrides + virtual void sleep(double timeout); +}; + +#endif diff --git a/src/lib/arch/CArchSleepWindows.cpp b/src/lib/arch/CArchSleepWindows.cpp new file mode 100644 index 00000000..ce049471 --- /dev/null +++ b/src/lib/arch/CArchSleepWindows.cpp @@ -0,0 +1,60 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchSleepWindows.h" +#include "CArch.h" +#include "CArchMultithreadWindows.h" + +// +// CArchSleepWindows +// + +CArchSleepWindows::CArchSleepWindows() +{ + // do nothing +} + +CArchSleepWindows::~CArchSleepWindows() +{ + // do nothing +} + +void +CArchSleepWindows::sleep(double timeout) +{ + ARCH->testCancelThread(); + if (timeout < 0.0) { + return; + } + + // get the cancel event from the current thread. this only + // works if we're using the windows multithread object but + // this is windows so that's pretty certain; we'll get a + // link error if we're not, though. + CArchMultithreadWindows* mt = CArchMultithreadWindows::getInstance(); + if (mt != NULL) { + HANDLE cancelEvent = mt->getCancelEventForCurrentThread(); + WaitForSingleObject(cancelEvent, (DWORD)(1000.0 * timeout)); + if (timeout == 0.0) { + Sleep(0); + } + } + else { + Sleep((DWORD)(1000.0 * timeout)); + } + ARCH->testCancelThread(); +} diff --git a/src/lib/arch/CArchSleepWindows.h b/src/lib/arch/CArchSleepWindows.h new file mode 100644 index 00000000..398bda3f --- /dev/null +++ b/src/lib/arch/CArchSleepWindows.h @@ -0,0 +1,35 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHSLEEPWINDOWS_H +#define CARCHSLEEPWINDOWS_H + +#include "IArchSleep.h" + +#define ARCH_SLEEP CArchSleepWindows + +//! Win32 implementation of IArchSleep +class CArchSleepWindows : public IArchSleep { +public: + CArchSleepWindows(); + virtual ~CArchSleepWindows(); + + // IArchSleep overrides + virtual void sleep(double timeout); +}; + +#endif diff --git a/src/lib/arch/CArchStringUnix.cpp b/src/lib/arch/CArchStringUnix.cpp new file mode 100644 index 00000000..65a157e8 --- /dev/null +++ b/src/lib/arch/CArchStringUnix.cpp @@ -0,0 +1,40 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchStringUnix.h" +#include + +// +// CArchStringUnix +// + +#include "CMultibyte.h" +#include "vsnprintf.h" + +CArchStringUnix::CArchStringUnix() +{ +} + +CArchStringUnix::~CArchStringUnix() +{ +} + +IArchString::EWideCharEncoding +CArchStringUnix::getWideCharEncoding() +{ + return kUCS4; +} diff --git a/src/lib/arch/CArchStringUnix.h b/src/lib/arch/CArchStringUnix.h new file mode 100644 index 00000000..d7b31f1a --- /dev/null +++ b/src/lib/arch/CArchStringUnix.h @@ -0,0 +1,36 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHSTRINGUNIX_H +#define CARCHSTRINGUNIX_H + +#include "IArchString.h" + +#define ARCH_STRING CArchStringUnix + +//! Unix implementation of IArchString +class CArchStringUnix : public IArchString { +public: + CArchStringUnix(); + virtual ~CArchStringUnix(); + + // IArchString overrides + virtual EWideCharEncoding + getWideCharEncoding(); +}; + +#endif diff --git a/src/lib/arch/CArchStringWindows.cpp b/src/lib/arch/CArchStringWindows.cpp new file mode 100644 index 00000000..dc76f7da --- /dev/null +++ b/src/lib/arch/CArchStringWindows.cpp @@ -0,0 +1,45 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#define WIN32_LEAN_AND_MEAN + +#include "CArchStringWindows.h" +#include +#include + +// +// CArchStringWindows +// + +#include "CMultibyte.h" +#define HAVE_VSNPRINTF 1 +#define ARCH_VSNPRINTF _vsnprintf +#include "vsnprintf.h" + +CArchStringWindows::CArchStringWindows() +{ +} + +CArchStringWindows::~CArchStringWindows() +{ +} + +IArchString::EWideCharEncoding +CArchStringWindows::getWideCharEncoding() +{ + return kUTF16; +} diff --git a/src/lib/arch/CArchStringWindows.h b/src/lib/arch/CArchStringWindows.h new file mode 100644 index 00000000..9de3dd1e --- /dev/null +++ b/src/lib/arch/CArchStringWindows.h @@ -0,0 +1,36 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHSTRINGWINDOWS_H +#define CARCHSTRINGWINDOWS_H + +#include "IArchString.h" + +#define ARCH_STRING CArchStringWindows + +//! Win32 implementation of IArchString +class CArchStringWindows : public IArchString { +public: + CArchStringWindows(); + virtual ~CArchStringWindows(); + + // IArchString overrides + virtual EWideCharEncoding + getWideCharEncoding(); +}; + +#endif diff --git a/src/lib/arch/CArchSystemUnix.cpp b/src/lib/arch/CArchSystemUnix.cpp new file mode 100644 index 00000000..9b3054ae --- /dev/null +++ b/src/lib/arch/CArchSystemUnix.cpp @@ -0,0 +1,74 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchSystemUnix.h" +#include + +// +// CArchSystemUnix +// + +CArchSystemUnix::CArchSystemUnix() +{ + // do nothing +} + +CArchSystemUnix::~CArchSystemUnix() +{ + // do nothing +} + +std::string +CArchSystemUnix::getOSName() const +{ +#if defined(HAVE_SYS_UTSNAME_H) + struct utsname info; + if (uname(&info) == 0) { + std::string msg; + msg += info.sysname; + msg += " "; + msg += info.release; + msg += " "; + msg += info.version; + return msg; + } +#endif + return "Unix"; +} + +std::string +CArchSystemUnix::getPlatformName() const +{ +#if defined(HAVE_SYS_UTSNAME_H) + struct utsname info; + if (uname(&info) == 0) { + return std::string(info.machine); + } +#endif + return "unknown"; +} + +std::string +CArchSystemUnix::setting(const std::string&) const +{ + return ""; +} + +void +CArchSystemUnix::setting(const std::string&, const std::string&) const +{ +} diff --git a/src/lib/arch/CArchSystemUnix.h b/src/lib/arch/CArchSystemUnix.h new file mode 100644 index 00000000..5d384d37 --- /dev/null +++ b/src/lib/arch/CArchSystemUnix.h @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHSYSTEMUNIX_H +#define CARCHSYSTEMUNIX_H + +#include "IArchSystem.h" + +#define ARCH_SYSTEM CArchSystemUnix + +//! Unix implementation of IArchString +class CArchSystemUnix : public IArchSystem { +public: + CArchSystemUnix(); + virtual ~CArchSystemUnix(); + + // IArchSystem overrides + virtual std::string getOSName() const; + virtual std::string getPlatformName() const; + virtual std::string setting(const std::string&) const; + virtual void setting(const std::string&, const std::string&) const; +}; + +#endif diff --git a/src/lib/arch/CArchSystemWindows.cpp b/src/lib/arch/CArchSystemWindows.cpp new file mode 100644 index 00000000..fce613ed --- /dev/null +++ b/src/lib/arch/CArchSystemWindows.cpp @@ -0,0 +1,178 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchSystemWindows.h" +#include "CArchMiscWindows.h" +#include "XArchWindows.h" + +#include "tchar.h" +#include + +static const char* s_settingsKeyNames[] = { + _T("SOFTWARE"), + _T("Synergy"), + NULL +}; + +// +// CArchSystemWindows +// + +CArchSystemWindows::CArchSystemWindows() +{ + // do nothing +} + +CArchSystemWindows::~CArchSystemWindows() +{ + // do nothing +} + +std::string +CArchSystemWindows::getOSName() const +{ +#if WINVER >= _WIN32_WINNT_WIN2K + OSVERSIONINFOEX info; +#else + OSVERSIONINFO info; +#endif + + info.dwOSVersionInfoSize = sizeof(info); + if (GetVersionEx((OSVERSIONINFO*) &info)) { + switch (info.dwPlatformId) { + case VER_PLATFORM_WIN32_NT: + #if WINVER >= _WIN32_WINNT_WIN2K + if (info.dwMajorVersion == 6) { + if(info.dwMinorVersion == 0) { + if (info.wProductType == VER_NT_WORKSTATION) { + return "Microsoft Windows Vista"; + } else { + return "Microsoft Windows Server 2008"; + } + } else if(info.dwMinorVersion == 1) { + if (info.wProductType == VER_NT_WORKSTATION) { + return "Microsoft Windows 7"; + } else { + return "Microsoft Windows Server 2008 R2"; + } + } + } + #endif + + if (info.dwMajorVersion == 5 && info.dwMinorVersion == 2) { + return "Microsoft Windows Server 2003"; + } + if (info.dwMajorVersion == 5 && info.dwMinorVersion == 1) { + return "Microsoft Windows XP"; + } + if (info.dwMajorVersion == 5 && info.dwMinorVersion == 0) { + return "Microsoft Windows Server 2000"; + } + if (info.dwMajorVersion <= 4) { + return "Microsoft Windows NT"; + } + char buffer[100]; + sprintf(buffer, "Microsoft Windows %d.%d", + info.dwMajorVersion, info.dwMinorVersion); + return buffer; + + case VER_PLATFORM_WIN32_WINDOWS: + if (info.dwMajorVersion == 4 && info.dwMinorVersion == 0) { + if (info.szCSDVersion[1] == 'C' || + info.szCSDVersion[1] == 'B') { + return "Microsoft Windows 95 OSR2"; + } + return "Microsoft Windows 95"; + } + if (info.dwMajorVersion == 4 && info.dwMinorVersion == 10) { + if (info.szCSDVersion[1] == 'A') { + return "Microsoft Windows 98 SE"; + } + return "Microsoft Windows 98"; + } + if (info.dwMajorVersion == 4 && info.dwMinorVersion == 90) { + return "Microsoft Windows ME"; + } + if (info.dwMajorVersion == 4) { + return "Microsoft Windows unknown 95 family"; + } + break; + + default: + break; + } + } + return "Microsoft Windows "; +} + +std::string +CArchSystemWindows::getPlatformName() const +{ +#ifdef _X86_ + if(isWOW64()) + return "x86 (WOW64)"; + else + return "x86"; +#else +#ifdef _AMD64_ + return "x64"; +#else + return "Unknown"; +#endif +#endif +} + +std::string +CArchSystemWindows::setting(const std::string& valueName) const +{ + HKEY key = CArchMiscWindows::openKey(HKEY_LOCAL_MACHINE, s_settingsKeyNames); + if (key == NULL) + return ""; + + return CArchMiscWindows::readValueString(key, valueName.c_str()); +} + +void +CArchSystemWindows::setting(const std::string& valueName, const std::string& valueString) const +{ + HKEY key = CArchMiscWindows::addKey(HKEY_LOCAL_MACHINE, s_settingsKeyNames); + if (key == NULL) + throw XArch(std::string("could not access registry key: ") + valueName); + CArchMiscWindows::setValue(key, valueName.c_str(), valueString.c_str()); +} + +bool +CArchSystemWindows::isWOW64() const +{ +#if WINVER >= _WIN32_WINNT_WINXP + typedef BOOL (WINAPI *LPFN_ISWOW64PROCESS) (HANDLE, PBOOL); + HMODULE hModule = GetModuleHandle(TEXT("kernel32")); + if (!hModule) return FALSE; + + LPFN_ISWOW64PROCESS fnIsWow64Process = + (LPFN_ISWOW64PROCESS) GetProcAddress(hModule, "IsWow64Process"); + + BOOL bIsWow64 = FALSE; + if(NULL != fnIsWow64Process && + fnIsWow64Process(GetCurrentProcess(), &bIsWow64) && + bIsWow64) + { + return true; + } +#endif + return false; +} diff --git a/src/lib/arch/CArchSystemWindows.h b/src/lib/arch/CArchSystemWindows.h new file mode 100644 index 00000000..1fcef482 --- /dev/null +++ b/src/lib/arch/CArchSystemWindows.h @@ -0,0 +1,40 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHSYSTEMWINDOWS_H +#define CARCHSYSTEMWINDOWS_H + +#include "IArchSystem.h" + +#define ARCH_SYSTEM CArchSystemWindows + +//! Win32 implementation of IArchString +class CArchSystemWindows : public IArchSystem { +public: + CArchSystemWindows(); + virtual ~CArchSystemWindows(); + + // IArchSystem overrides + virtual std::string getOSName() const; + virtual std::string getPlatformName() const; + virtual std::string setting(const std::string& valueName) const; + virtual void setting(const std::string& valueName, const std::string& valueString) const; + + bool isWOW64() const; +}; + +#endif diff --git a/src/lib/arch/CArchTaskBarWindows.cpp b/src/lib/arch/CArchTaskBarWindows.cpp new file mode 100644 index 00000000..8c36b81c --- /dev/null +++ b/src/lib/arch/CArchTaskBarWindows.cpp @@ -0,0 +1,501 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchTaskBarWindows.h" +#include "CArchMiscWindows.h" +#include "IArchTaskBarReceiver.h" +#include "CArch.h" +#include "XArch.h" +#include +#include +#include "CAppUtilWindows.h" + +static const UINT kAddReceiver = WM_USER + 10; +static const UINT kRemoveReceiver = WM_USER + 11; +static const UINT kUpdateReceiver = WM_USER + 12; +static const UINT kNotifyReceiver = WM_USER + 13; +static const UINT kFirstReceiverID = WM_USER + 14; + +// +// CArchTaskBarWindows +// + +CArchTaskBarWindows* CArchTaskBarWindows::s_instance = NULL; + +CArchTaskBarWindows::CArchTaskBarWindows() : + m_nextID(kFirstReceiverID) +{ + // save the singleton instance + s_instance = this; +} + +CArchTaskBarWindows::~CArchTaskBarWindows() +{ + if (m_thread != NULL) { + PostMessage(m_hwnd, WM_QUIT, 0, 0); + ARCH->wait(m_thread, -1.0); + ARCH->closeThread(m_thread); + } + ARCH->closeCondVar(m_condVar); + ARCH->closeMutex(m_mutex); + s_instance = NULL; +} + +void +CArchTaskBarWindows::init() +{ + // we need a mutex + m_mutex = ARCH->newMutex(); + + // and a condition variable which uses the above mutex + m_ready = false; + m_condVar = ARCH->newCondVar(); + + // we're going to want to get a result from the thread we're + // about to create to know if it initialized successfully. + // so we lock the condition variable. + ARCH->lockMutex(m_mutex); + + // open a window and run an event loop in a separate thread. + // this has to happen in a separate thread because if we + // create a window on the current desktop with the current + // thread then the current thread won't be able to switch + // desktops if it needs to. + m_thread = ARCH->newThread(&CArchTaskBarWindows::threadEntry, this); + + // wait for child thread + while (!m_ready) { + ARCH->waitCondVar(m_condVar, m_mutex, -1.0); + } + + // ready + ARCH->unlockMutex(m_mutex); +} + +void +CArchTaskBarWindows::addDialog(HWND hwnd) +{ + CArchMiscWindows::addDialog(hwnd); +} + +void +CArchTaskBarWindows::removeDialog(HWND hwnd) +{ + CArchMiscWindows::removeDialog(hwnd); +} + +void +CArchTaskBarWindows::addReceiver(IArchTaskBarReceiver* receiver) +{ + // ignore bogus receiver + if (receiver == NULL) { + return; + } + + // add receiver if necessary + CReceiverToInfoMap::iterator index = m_receivers.find(receiver); + if (index == m_receivers.end()) { + // add it, creating a new message ID for it + CReceiverInfo info; + info.m_id = getNextID(); + index = m_receivers.insert(std::make_pair(receiver, info)).first; + + // add ID to receiver mapping + m_idTable.insert(std::make_pair(info.m_id, index)); + } + + // add receiver + PostMessage(m_hwnd, kAddReceiver, index->second.m_id, 0); +} + +void +CArchTaskBarWindows::removeReceiver(IArchTaskBarReceiver* receiver) +{ + // find receiver + CReceiverToInfoMap::iterator index = m_receivers.find(receiver); + if (index == m_receivers.end()) { + return; + } + + // remove icon. wait for this to finish before returning. + SendMessage(m_hwnd, kRemoveReceiver, index->second.m_id, 0); + + // recycle the ID + recycleID(index->second.m_id); + + // discard + m_idTable.erase(index->second.m_id); + m_receivers.erase(index); +} + +void +CArchTaskBarWindows::updateReceiver(IArchTaskBarReceiver* receiver) +{ + // find receiver + CReceiverToInfoMap::const_iterator index = m_receivers.find(receiver); + if (index == m_receivers.end()) { + return; + } + + // update icon and tool tip + PostMessage(m_hwnd, kUpdateReceiver, index->second.m_id, 0); +} + +UINT +CArchTaskBarWindows::getNextID() +{ + if (m_oldIDs.empty()) { + return m_nextID++; + } + UINT id = m_oldIDs.back(); + m_oldIDs.pop_back(); + return id; +} + +void +CArchTaskBarWindows::recycleID(UINT id) +{ + m_oldIDs.push_back(id); +} + +void +CArchTaskBarWindows::addIcon(UINT id) +{ + ARCH->lockMutex(m_mutex); + CIDToReceiverMap::const_iterator index = m_idTable.find(id); + if (index != m_idTable.end()) { + modifyIconNoLock(index->second, NIM_ADD); + } + ARCH->unlockMutex(m_mutex); +} + +void +CArchTaskBarWindows::removeIcon(UINT id) +{ + ARCH->lockMutex(m_mutex); + removeIconNoLock(id); + ARCH->unlockMutex(m_mutex); +} + +void +CArchTaskBarWindows::updateIcon(UINT id) +{ + ARCH->lockMutex(m_mutex); + CIDToReceiverMap::const_iterator index = m_idTable.find(id); + if (index != m_idTable.end()) { + modifyIconNoLock(index->second, NIM_MODIFY); + } + ARCH->unlockMutex(m_mutex); +} + +void +CArchTaskBarWindows::addAllIcons() +{ + ARCH->lockMutex(m_mutex); + for (CReceiverToInfoMap::const_iterator index = m_receivers.begin(); + index != m_receivers.end(); ++index) { + modifyIconNoLock(index, NIM_ADD); + } + ARCH->unlockMutex(m_mutex); +} + +void +CArchTaskBarWindows::removeAllIcons() +{ + ARCH->lockMutex(m_mutex); + for (CReceiverToInfoMap::const_iterator index = m_receivers.begin(); + index != m_receivers.end(); ++index) { + removeIconNoLock(index->second.m_id); + } + ARCH->unlockMutex(m_mutex); +} + +void +CArchTaskBarWindows::modifyIconNoLock( + CReceiverToInfoMap::const_iterator index, DWORD taskBarMessage) +{ + // get receiver + UINT id = index->second.m_id; + IArchTaskBarReceiver* receiver = index->first; + + // lock receiver so icon and tool tip are guaranteed to be consistent + receiver->lock(); + + // get icon data + HICON icon = reinterpret_cast( + const_cast(receiver->getIcon())); + + // get tool tip + std::string toolTip = receiver->getToolTip(); + + // done querying + receiver->unlock(); + + // prepare to add icon + NOTIFYICONDATA data; + data.cbSize = sizeof(NOTIFYICONDATA); + data.hWnd = m_hwnd; + data.uID = id; + data.uFlags = NIF_MESSAGE; + data.uCallbackMessage = kNotifyReceiver; + data.hIcon = icon; + if (icon != NULL) { + data.uFlags |= NIF_ICON; + } + if (!toolTip.empty()) { + strncpy(data.szTip, toolTip.c_str(), sizeof(data.szTip)); + data.szTip[sizeof(data.szTip) - 1] = '\0'; + data.uFlags |= NIF_TIP; + } + else { + data.szTip[0] = '\0'; + } + + // add icon + if (Shell_NotifyIcon(taskBarMessage, &data) == 0) { + // failed + } +} + +void +CArchTaskBarWindows::removeIconNoLock(UINT id) +{ + NOTIFYICONDATA data; + data.cbSize = sizeof(NOTIFYICONDATA); + data.hWnd = m_hwnd; + data.uID = id; + if (Shell_NotifyIcon(NIM_DELETE, &data) == 0) { + // failed + } +} + +void +CArchTaskBarWindows::handleIconMessage( + IArchTaskBarReceiver* receiver, LPARAM lParam) +{ + // process message + switch (lParam) { + case WM_LBUTTONDOWN: + receiver->showStatus(); + break; + + case WM_LBUTTONDBLCLK: + receiver->primaryAction(); + break; + + case WM_RBUTTONUP: { + POINT p; + GetCursorPos(&p); + receiver->runMenu(p.x, p.y); + break; + } + + case WM_MOUSEMOVE: + // currently unused + break; + + default: + // unused + break; + } +} + +bool +CArchTaskBarWindows::processDialogs(MSG* msg) +{ + // only one thread can be in this method on any particular object + // at any given time. that's not a problem since only our event + // loop calls this method and there's just one of those. + + ARCH->lockMutex(m_mutex); + + // remove removed dialogs + m_dialogs.erase(false); + + // merge added dialogs into the dialog list + for (CDialogs::const_iterator index = m_addedDialogs.begin(); + index != m_addedDialogs.end(); ++index) { + m_dialogs.insert(std::make_pair(index->first, index->second)); + } + m_addedDialogs.clear(); + + ARCH->unlockMutex(m_mutex); + + // check message against all dialogs until one handles it. + // note that we don't hold a lock while checking because + // the message is processed and may make calls to this + // object. that's okay because addDialog() and + // removeDialog() don't change the map itself (just the + // values of some elements). + ARCH->lockMutex(m_mutex); + for (CDialogs::const_iterator index = m_dialogs.begin(); + index != m_dialogs.end(); ++index) { + if (index->second) { + ARCH->unlockMutex(m_mutex); + if (IsDialogMessage(index->first, msg)) { + return true; + } + ARCH->lockMutex(m_mutex); + } + } + ARCH->unlockMutex(m_mutex); + + return false; +} + +LRESULT +CArchTaskBarWindows::wndProc(HWND hwnd, + UINT msg, WPARAM wParam, LPARAM lParam) +{ + switch (msg) { + case kNotifyReceiver: { + // lookup receiver + CIDToReceiverMap::const_iterator index = m_idTable.find((UINT)wParam); + if (index != m_idTable.end()) { + IArchTaskBarReceiver* receiver = index->second->first; + handleIconMessage(receiver, lParam); + return 0; + } + break; + } + + case kAddReceiver: + addIcon((UINT)wParam); + break; + + case kRemoveReceiver: + removeIcon((UINT)wParam); + break; + + case kUpdateReceiver: + updateIcon((UINT)wParam); + break; + + default: + if (msg == m_taskBarRestart) { + // task bar was recreated so re-add our icons + addAllIcons(); + } + break; + } + + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +LRESULT CALLBACK +CArchTaskBarWindows::staticWndProc(HWND hwnd, UINT msg, + WPARAM wParam, LPARAM lParam) +{ + // if msg is WM_NCCREATE, extract the CArchTaskBarWindows* and put + // it in the extra window data then forward the call. + CArchTaskBarWindows* self = NULL; + if (msg == WM_NCCREATE) { + CREATESTRUCT* createInfo; + createInfo = reinterpret_cast(lParam); + self = reinterpret_cast( + createInfo->lpCreateParams); + SetWindowLong(hwnd, 0, reinterpret_cast(self)); + } + else { + // get the extra window data and forward the call + LONG data = GetWindowLong(hwnd, 0); + if (data != 0) { + self = reinterpret_cast( + reinterpret_cast(data)); + } + } + + // forward the message + if (self != NULL) { + return self->wndProc(hwnd, msg, wParam, lParam); + } + else { + return DefWindowProc(hwnd, msg, wParam, lParam); + } +} + +void +CArchTaskBarWindows::threadMainLoop() +{ + // register the task bar restart message + m_taskBarRestart = RegisterWindowMessage(TEXT("TaskbarCreated")); + + // register a window class + WNDCLASSEX classInfo; + classInfo.cbSize = sizeof(classInfo); + classInfo.style = CS_NOCLOSE; + classInfo.lpfnWndProc = &CArchTaskBarWindows::staticWndProc; + classInfo.cbClsExtra = 0; + classInfo.cbWndExtra = sizeof(CArchTaskBarWindows*); + classInfo.hInstance = instanceWin32(); + classInfo.hIcon = NULL; + classInfo.hCursor = NULL; + classInfo.hbrBackground = NULL; + classInfo.lpszMenuName = NULL; + classInfo.lpszClassName = TEXT("SynergyTaskBar"); + classInfo.hIconSm = NULL; + ATOM windowClass = RegisterClassEx(&classInfo); + + // create window + m_hwnd = CreateWindowEx(WS_EX_TOOLWINDOW, + reinterpret_cast(windowClass), + TEXT("Synergy Task Bar"), + WS_POPUP, + 0, 0, 1, 1, + NULL, + NULL, + instanceWin32(), + reinterpret_cast(this)); + + // signal ready + ARCH->lockMutex(m_mutex); + m_ready = true; + ARCH->broadcastCondVar(m_condVar); + ARCH->unlockMutex(m_mutex); + + // handle failure + if (m_hwnd == NULL) { + UnregisterClass(reinterpret_cast(windowClass), instanceWin32()); + return; + } + + // main loop + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + if (!processDialogs(&msg)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + // clean up + removeAllIcons(); + DestroyWindow(m_hwnd); + UnregisterClass(reinterpret_cast(windowClass), instanceWin32()); +} + +void* +CArchTaskBarWindows::threadEntry(void* self) +{ + reinterpret_cast(self)->threadMainLoop(); + return NULL; +} + +HINSTANCE CArchTaskBarWindows::instanceWin32() +{ + return CArchMiscWindows::instanceWin32(); +} \ No newline at end of file diff --git a/src/lib/arch/CArchTaskBarWindows.h b/src/lib/arch/CArchTaskBarWindows.h new file mode 100644 index 00000000..f5d7eff6 --- /dev/null +++ b/src/lib/arch/CArchTaskBarWindows.h @@ -0,0 +1,116 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHTASKBARWINDOWS_H +#define CARCHTASKBARWINDOWS_H + +#define WIN32_LEAN_AND_MEAN + +#include "IArchTaskBar.h" +#include "IArchMultithread.h" +#include "stdmap.h" +#include "stdvector.h" +#include + +#define ARCH_TASKBAR CArchTaskBarWindows + +//! Win32 implementation of IArchTaskBar +class CArchTaskBarWindows : public IArchTaskBar { +public: + CArchTaskBarWindows(); + virtual ~CArchTaskBarWindows(); + + virtual void init(); + + //! Add a dialog window + /*! + Tell the task bar event loop about a dialog. Win32 annoyingly + requires messages destined for modeless dialog boxes to be + dispatched differently than other messages. + */ + static void addDialog(HWND); + + //! Remove a dialog window + /*! + Remove a dialog window added via \c addDialog(). + */ + static void removeDialog(HWND); + + // IArchTaskBar overrides + virtual void addReceiver(IArchTaskBarReceiver*); + virtual void removeReceiver(IArchTaskBarReceiver*); + virtual void updateReceiver(IArchTaskBarReceiver*); + +private: + class CReceiverInfo { + public: + UINT m_id; + }; + + typedef std::map CReceiverToInfoMap; + typedef std::map CIDToReceiverMap; + typedef std::vector CIDStack; + typedef std::map CDialogs; + + UINT getNextID(); + void recycleID(UINT); + + void addIcon(UINT); + void removeIcon(UINT); + void updateIcon(UINT); + void addAllIcons(); + void removeAllIcons(); + void modifyIconNoLock(CReceiverToInfoMap::const_iterator, + DWORD taskBarMessage); + void removeIconNoLock(UINT id); + void handleIconMessage(IArchTaskBarReceiver*, LPARAM); + + bool processDialogs(MSG*); + LRESULT wndProc(HWND, UINT, WPARAM, LPARAM); + static LRESULT CALLBACK + staticWndProc(HWND, UINT, WPARAM, LPARAM); + void threadMainLoop(); + static void* threadEntry(void*); + + HINSTANCE instanceWin32(); + +private: + static CArchTaskBarWindows* s_instance; + + // multithread data + CArchMutex m_mutex; + CArchCond m_condVar; + bool m_ready; + int m_result; + CArchThread m_thread; + + // child thread data + HWND m_hwnd; + UINT m_taskBarRestart; + + // shared data + CReceiverToInfoMap m_receivers; + CIDToReceiverMap m_idTable; + CIDStack m_oldIDs; + UINT m_nextID; + + // dialogs + CDialogs m_dialogs; + CDialogs m_addedDialogs; +}; + +#endif diff --git a/src/lib/arch/CArchTaskBarXWindows.cpp b/src/lib/arch/CArchTaskBarXWindows.cpp new file mode 100644 index 00000000..d7166b54 --- /dev/null +++ b/src/lib/arch/CArchTaskBarXWindows.cpp @@ -0,0 +1,50 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchTaskBarXWindows.h" + +// +// CArchTaskBarXWindows +// + +CArchTaskBarXWindows::CArchTaskBarXWindows() +{ + // do nothing +} + +CArchTaskBarXWindows::~CArchTaskBarXWindows() +{ + // do nothing +} + +void +CArchTaskBarXWindows::addReceiver(IArchTaskBarReceiver* /*receiver*/) +{ + // do nothing +} + +void +CArchTaskBarXWindows::removeReceiver(IArchTaskBarReceiver* /*receiver*/) +{ + // do nothing +} + +void +CArchTaskBarXWindows::updateReceiver(IArchTaskBarReceiver* /*receiver*/) +{ + // do nothing +} diff --git a/src/lib/arch/CArchTaskBarXWindows.h b/src/lib/arch/CArchTaskBarXWindows.h new file mode 100644 index 00000000..7e195518 --- /dev/null +++ b/src/lib/arch/CArchTaskBarXWindows.h @@ -0,0 +1,37 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHTASKBARXWINDOWS_H +#define CARCHTASKBARXWINDOWS_H + +#include "IArchTaskBar.h" + +#define ARCH_TASKBAR CArchTaskBarXWindows + +//! X11 implementation of IArchTaskBar +class CArchTaskBarXWindows : public IArchTaskBar { +public: + CArchTaskBarXWindows(); + virtual ~CArchTaskBarXWindows(); + + // IArchTaskBar overrides + virtual void addReceiver(IArchTaskBarReceiver*); + virtual void removeReceiver(IArchTaskBarReceiver*); + virtual void updateReceiver(IArchTaskBarReceiver*); +}; + +#endif diff --git a/src/lib/arch/CArchTimeUnix.cpp b/src/lib/arch/CArchTimeUnix.cpp new file mode 100644 index 00000000..8ff4cb14 --- /dev/null +++ b/src/lib/arch/CArchTimeUnix.cpp @@ -0,0 +1,50 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CArchTimeUnix.h" +#if TIME_WITH_SYS_TIME +# include +# include +#else +# if HAVE_SYS_TIME_H +# include +# else +# include +# endif +#endif + +// +// CArchTimeUnix +// + +CArchTimeUnix::CArchTimeUnix() +{ + // do nothing +} + +CArchTimeUnix::~CArchTimeUnix() +{ + // do nothing +} + +double +CArchTimeUnix::time() +{ + struct timeval t; + gettimeofday(&t, NULL); + return (double)t.tv_sec + 1.0e-6 * (double)t.tv_usec; +} diff --git a/src/lib/arch/CArchTimeUnix.h b/src/lib/arch/CArchTimeUnix.h new file mode 100644 index 00000000..19df6c78 --- /dev/null +++ b/src/lib/arch/CArchTimeUnix.h @@ -0,0 +1,35 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHTIMEUNIX_H +#define CARCHTIMEUNIX_H + +#include "IArchTime.h" + +#define ARCH_TIME CArchTimeUnix + +//! Generic Unix implementation of IArchTime +class CArchTimeUnix : public IArchTime { +public: + CArchTimeUnix(); + virtual ~CArchTimeUnix(); + + // IArchTime overrides + virtual double time(); +}; + +#endif diff --git a/src/lib/arch/CArchTimeWindows.cpp b/src/lib/arch/CArchTimeWindows.cpp new file mode 100644 index 00000000..5fa86eda --- /dev/null +++ b/src/lib/arch/CArchTimeWindows.cpp @@ -0,0 +1,89 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +// avoid getting a lot a crap from mmsystem.h that we don't need +#define MMNODRV // Installable driver support +#define MMNOSOUND // Sound support +#define MMNOWAVE // Waveform support +#define MMNOMIDI // MIDI support +#define MMNOAUX // Auxiliary audio support +#define MMNOMIXER // Mixer support +#define MMNOJOY // Joystick support +#define MMNOMCI // MCI support +#define MMNOMMIO // Multimedia file I/O support +#define MMNOMMSYSTEM // General MMSYSTEM functions + +#define WIN32_LEAN_AND_MEAN + +#include "CArchTimeWindows.h" +#include +#include + +typedef WINMMAPI DWORD (WINAPI *PTimeGetTime)(void); + +static double s_freq = 0.0; +static HINSTANCE s_mmInstance = NULL; +static PTimeGetTime s_tgt = NULL; + + +// +// CArchTimeWindows +// + +CArchTimeWindows::CArchTimeWindows() +{ + assert(s_freq == 0.0 || s_mmInstance == NULL); + + LARGE_INTEGER freq; + if (QueryPerformanceFrequency(&freq) && freq.QuadPart != 0) { + s_freq = 1.0 / static_cast(freq.QuadPart); + } + else { + // load winmm.dll and get timeGetTime + s_mmInstance = LoadLibrary("winmm"); + if (s_mmInstance != NULL) { + s_tgt = (PTimeGetTime)GetProcAddress(s_mmInstance, "timeGetTime"); + } + } +} + +CArchTimeWindows::~CArchTimeWindows() +{ + s_freq = 0.0; + if (s_mmInstance == NULL) { + FreeLibrary(reinterpret_cast(s_mmInstance)); + s_tgt = NULL; + s_mmInstance = NULL; + } +} + +double +CArchTimeWindows::time() +{ + // get time. we try three ways, in order of descending precision + if (s_freq != 0.0) { + LARGE_INTEGER c; + QueryPerformanceCounter(&c); + return s_freq * static_cast(c.QuadPart); + } + else if (s_tgt != NULL) { + return 0.001 * static_cast(s_tgt()); + } + else { + return 0.001 * static_cast(GetTickCount()); + } +} diff --git a/src/lib/arch/CArchTimeWindows.h b/src/lib/arch/CArchTimeWindows.h new file mode 100644 index 00000000..c91f4744 --- /dev/null +++ b/src/lib/arch/CArchTimeWindows.h @@ -0,0 +1,35 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CARCHTIMEWINDOWS_H +#define CARCHTIMEWINDOWS_H + +#include "IArchTime.h" + +#define ARCH_TIME CArchTimeWindows + +//! Win32 implementation of IArchTime +class CArchTimeWindows : public IArchTime { +public: + CArchTimeWindows(); + virtual ~CArchTimeWindows(); + + // IArchTime overrides + virtual double time(); +}; + +#endif diff --git a/src/lib/arch/CMakeLists.txt b/src/lib/arch/CMakeLists.txt new file mode 100644 index 00000000..34337ae4 --- /dev/null +++ b/src/lib/arch/CMakeLists.txt @@ -0,0 +1,111 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(src + CArch.cpp + IArchString.cpp + XArch.cpp + CArchConsoleStd.cpp +) + +if (WIN32) + + set(inc + CArchConsoleWindows.h + CArchDaemonWindows.h + CArchFileWindows.h + CArchLogWindows.h + CArchIpcLogWindows.h + CArchMiscWindows.h + CArchMultithreadWindows.h + CArchNetworkWinsock.h + CArchSleepWindows.h + CArchStringWindows.h + CArchSystemWindows.h + CArchTaskBarWindows.h + CArchTimeWindows.h + CArchConsoleStd.h + XArchWindows.h + CMultibyte.h + vsnprintf.h + XArch.h + IArchPlugin.h + CArchPluginWindows.h + ) + + list(APPEND src + ${inc} + CArchConsoleWindows.cpp + CArchDaemonWindows.cpp + CArchFileWindows.cpp + CArchLogWindows.cpp + CArchIpcLogWindows.cpp + CArchMiscWindows.cpp + CArchMultithreadWindows.cpp + CArchNetworkWinsock.cpp + CArchSleepWindows.cpp + CArchStringWindows.cpp + CArchSystemWindows.cpp + CArchTaskBarWindows.cpp + CArchTimeWindows.cpp + XArchWindows.cpp + CArchPluginWindows.cpp + ) + +elseif (UNIX) + + list(APPEND src + CArchConsoleUnix.cpp + CArchDaemonUnix.cpp + CArchFileUnix.cpp + CArchLogUnix.cpp + CArchIpcLogUnix.cpp + CArchMultithreadPosix.cpp + CArchNetworkBSD.cpp + CArchSleepUnix.cpp + CArchStringUnix.cpp + CArchSystemUnix.cpp + CArchTaskBarXWindows.cpp + CArchTimeUnix.cpp + XArchUnix.cpp + CArchDaemonNone.cpp + CArchPluginUnix.cpp + ) + +endif() + +set(inc + ../base + ../common + ../mt + ../platform + ../synergy +) + +if (UNIX) + list(APPEND inc + ../../.. + ../arch + ) +endif() + +include_directories(${inc}) +add_library(arch STATIC ${src}) + +if (WIN32) + if (GAME_DEVICE_SUPPORT) + target_link_libraries(arch synxinhk) + endif() +endif() diff --git a/src/lib/arch/CMultibyte.h b/src/lib/arch/CMultibyte.h new file mode 100644 index 00000000..e8e1c4e1 --- /dev/null +++ b/src/lib/arch/CMultibyte.h @@ -0,0 +1,57 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMULTIBYTE_H +#define CMULTIBYTE_H + +#include "common.h" +#include "CArch.h" +#include +#include +#include +#if HAVE_LOCALE_H +# include +#endif +#if HAVE_WCHAR_H || defined(_MSC_VER) +# include +#elif __APPLE__ + // wtf? Darwin puts mbtowc() et al. in stdlib +# include +#else + // platform apparently has no wchar_t support. provide dummy + // implementations. hopefully at least the C++ compiler has + // a built-in wchar_t type. + +static inline +int +mbtowc(wchar_t* dst, const char* src, int n) +{ + *dst = static_cast(*src); + return 1; +} + +static inline +int +wctomb(char* dst, wchar_t src) +{ + *dst = static_cast(src); + return 1; +} + +#endif + +#endif diff --git a/src/lib/arch/IArchConsole.h b/src/lib/arch/IArchConsole.h new file mode 100644 index 00000000..f70efdfc --- /dev/null +++ b/src/lib/arch/IArchConsole.h @@ -0,0 +1,68 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IARCHCONSOLE_H +#define IARCHCONSOLE_H + +#include "IInterface.h" +#include "ELevel.h" + +//! Interface for architecture dependent console output +/*! +This interface defines the console operations required by +synergy. Each architecture must implement this interface. +*/ +class IArchConsole : public IInterface { +public: + //! @name manipulators + //@{ + + //! Open the console + /*! + Opens the console for writing. The console is opened automatically + on the first write so calling this method is optional. Uses \c title + for the console's title if appropriate for the architecture. Calling + this method on an already open console must have no effect. + */ + virtual void openConsole(const char* title) = 0; + + //! Close the console + /*! + Close the console. Calling this method on an already closed console + must have no effect. + */ + virtual void closeConsole() = 0; + + //! Show the console + /*! + Causes the console to become visible. This generally only makes sense + for a console in a graphical user interface. Other implementations + will do nothing. Iff \p showIfEmpty is \c false then the implementation + may optionally only show the console if it's not empty. + */ + virtual void showConsole(bool showIfEmpty) = 0; + + //! Write to the console + /*! + Writes the given string to the console, opening it if necessary. + */ + virtual void writeConsole(ELevel, const char*) = 0; + + //@} +}; + +#endif diff --git a/src/lib/arch/IArchDaemon.h b/src/lib/arch/IArchDaemon.h new file mode 100644 index 00000000..a8949690 --- /dev/null +++ b/src/lib/arch/IArchDaemon.h @@ -0,0 +1,131 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IARCHDAEMON_H +#define IARCHDAEMON_H + +#include "IInterface.h" +#include "CString.h" + +//! Interface for architecture dependent daemonizing +/*! +This interface defines the operations required by synergy for installing +uninstalling daeamons and daemonizing a process. Each architecture must +implement this interface. +*/ +class IArchDaemon : public IInterface { +public: + typedef int (*DaemonFunc)(int argc, const char** argv); + + //! @name manipulators + //@{ + + //! Install daemon + /*! + Install a daemon. \c name is the name of the daemon passed to the + system and \c description is a short human readable description of + the daemon. \c pathname is the path to the daemon executable. + \c commandLine should \b not include the name of program as the + first argument. If \c allUsers is true then the daemon will be + installed to start at boot time, otherwise it will be installed to + start when the current user logs in. If \p dependencies is not NULL + then it's a concatenation of NUL terminated other daemon names + followed by a NUL; the daemon will be configured to startup after + the listed daemons. Throws an \c XArchDaemon exception on failure. + */ + virtual void installDaemon(const char* name, + const char* description, + const char* pathname, + const char* commandLine, + const char* dependencies, + bool allUsers) = 0; + + //! Uninstall daemon + /*! + Uninstall a daemon. Throws an \c XArchDaemon on failure. + */ + virtual void uninstallDaemon(const char* name, bool allUsers) = 0; + + //! Install daemon + /*! + Installs the default daemon. + */ + virtual void installDaemon() = 0; + + //! Uninstall daemon + /*! + Uninstalls the default daemon. + */ + virtual void uninstallDaemon() = 0; + + //! Daemonize the process + /*! + Daemonize. Throw XArchDaemonFailed on error. \c name is the name + of the daemon. Once daemonized, \c func is invoked and daemonize + returns when and what it does. + + Exactly what happens when daemonizing depends on the platform. +
    +
  • unix: + Detaches from terminal. \c func gets passed one argument, the + name passed to daemonize(). +
  • win32: + Becomes a service. Argument 0 is the name of the service + and the rest are the arguments passed to StartService(). + \c func is only called when the service is actually started. + \c func must call \c CArchMiscWindows::runDaemon() to finally + becoming a service. The \c runFunc function passed to \c runDaemon() + must call \c CArchMiscWindows::daemonRunning(true) when it + enters the main loop (i.e. after initialization) and + \c CArchMiscWindows::daemonRunning(false) when it leaves + the main loop. The \c stopFunc function passed to \c runDaemon() + is called when the daemon must exit the main loop and it must cause + \c runFunc to return. \c func should return what \c runDaemon() + returns. \c func or \c runFunc can call + \c CArchMiscWindows::daemonFailed() to indicate startup failure. +
+ */ + virtual int daemonize(const char* name, DaemonFunc func) = 0; + + //! Check if user has permission to install the daemon + /*! + Returns true iff the caller has permission to install or + uninstall the daemon. Note that even if this method returns + true it's possible that installing/uninstalling the service + may still fail. This method ignores whether or not the + service is already installed. + */ + virtual bool canInstallDaemon(const char* name, bool allUsers) = 0; + + //! Check if the daemon is installed + /*! + Returns true iff the daemon is installed. + */ + virtual bool isDaemonInstalled(const char* name, bool allUsers) = 0; + + //@} + + //! Get the command line + /*! + Gets the command line with which the application was started. + */ + virtual std::string commandLine() const = 0; + + //@} +}; + +#endif diff --git a/src/lib/arch/IArchFile.h b/src/lib/arch/IArchFile.h new file mode 100644 index 00000000..ffd74819 --- /dev/null +++ b/src/lib/arch/IArchFile.h @@ -0,0 +1,67 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IARCHFILE_H +#define IARCHFILE_H + +#include "IInterface.h" +#include "stdstring.h" + +//! Interface for architecture dependent file system operations +/*! +This interface defines the file system operations required by +synergy. Each architecture must implement this interface. +*/ +class IArchFile : public IInterface { +public: + //! @name manipulators + //@{ + + //! Extract base name + /*! + Find the base name in the given \c pathname. + */ + virtual const char* getBasename(const char* pathname) = 0; + + //! Get user's home directory + /*! + Returns the user's home directory. Returns the empty string if + this cannot be determined. + */ + virtual std::string getUserDirectory() = 0; + + //! Get system directory + /*! + Returns the ussystem configuration file directory. + */ + virtual std::string getSystemDirectory() = 0; + + //! Concatenate path components + /*! + Concatenate pathname components with a directory separator + between them. This should not check if the resulting path + is longer than allowed by the system; we'll rely on the + system calls to tell us that. + */ + virtual std::string concatPath( + const std::string& prefix, + const std::string& suffix) = 0; + + //@} +}; + +#endif diff --git a/src/lib/arch/IArchLog.h b/src/lib/arch/IArchLog.h new file mode 100644 index 00000000..12fd4731 --- /dev/null +++ b/src/lib/arch/IArchLog.h @@ -0,0 +1,65 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IARCHLOG_H +#define IARCHLOG_H + +#include "IInterface.h" +#include "ELevel.h" + +//! Interface for architecture dependent logging +/*! +This interface defines the logging operations required by +synergy. Each architecture must implement this interface. +*/ +class IArchLog : public IInterface { +public: + //! @name manipulators + //@{ + + //! Open the log + /*! + Opens the log for writing. The log must be opened before being + written to. + */ + virtual void openLog(const char* name) = 0; + + //! Close the log + /*! + Close the log. + */ + virtual void closeLog() = 0; + + //! Show the log + /*! + Causes the log to become visible. This generally only makes sense + for a log in a graphical user interface. Other implementations + will do nothing. Iff \p showIfEmpty is \c false then the implementation + may optionally only show the log if it's not empty. + */ + virtual void showLog(bool showIfEmpty) = 0; + + //! Write to the log + /*! + Writes the given string to the log with the given level. + */ + virtual void writeLog(ELevel, const char*) = 0; + + //@} +}; + +#endif diff --git a/src/lib/arch/IArchMultithread.h b/src/lib/arch/IArchMultithread.h new file mode 100644 index 00000000..b7047bea --- /dev/null +++ b/src/lib/arch/IArchMultithread.h @@ -0,0 +1,275 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IARCHMULTITHREAD_H +#define IARCHMULTITHREAD_H + +#include "IInterface.h" + +/*! +\class CArchCondImpl +\brief Internal condition variable data. +An architecture dependent type holding the necessary data for a +condition variable. +*/ +class CArchCondImpl; + +/*! +\var CArchCond +\brief Opaque condition variable type. +An opaque type representing a condition variable. +*/ +typedef CArchCondImpl* CArchCond; + +/*! +\class CArchMutexImpl +\brief Internal mutex data. +An architecture dependent type holding the necessary data for a mutex. +*/ +class CArchMutexImpl; + +/*! +\var CArchMutex +\brief Opaque mutex type. +An opaque type representing a mutex. +*/ +typedef CArchMutexImpl* CArchMutex; + +/*! +\class CArchThreadImpl +\brief Internal thread data. +An architecture dependent type holding the necessary data for a thread. +*/ +class CArchThreadImpl; + +/*! +\var CArchThread +\brief Opaque thread type. +An opaque type representing a thread. +*/ +typedef CArchThreadImpl* CArchThread; + +//! Interface for architecture dependent multithreading +/*! +This interface defines the multithreading operations required by +synergy. Each architecture must implement this interface. +*/ +class IArchMultithread : public IInterface { +public: + //! Type of thread entry point + typedef void* (*ThreadFunc)(void*); + //! Type of thread identifier + typedef unsigned int ThreadID; + //! Types of signals + /*! + Not all platforms support all signals. Unsupported signals are + ignored. + */ + enum ESignal { + kINTERRUPT, //!< Interrupt (e.g. Ctrl+C) + kTERMINATE, //!< Terminate (e.g. Ctrl+Break) + kHANGUP, //!< Hangup (SIGHUP) + kUSER, //!< User (SIGUSR2) + kNUM_SIGNALS + }; + //! Type of signal handler function + typedef void (*SignalFunc)(ESignal, void* userData); + + //! @name manipulators + //@{ + + // + // condition variable methods + // + + //! Create a condition variable + /*! + The condition variable is an opaque data type. + */ + virtual CArchCond newCondVar() = 0; + + //! Destroy a condition variable + virtual void closeCondVar(CArchCond) = 0; + + //! Signal a condition variable + /*! + Signalling a condition variable releases one waiting thread. + */ + virtual void signalCondVar(CArchCond) = 0; + + //! Broadcast a condition variable + /*! + Broadcasting a condition variable releases all waiting threads. + */ + virtual void broadcastCondVar(CArchCond) = 0; + + //! Wait on a condition variable + /*! + Wait on a conditation variable for up to \c timeout seconds. + If \c timeout is < 0 then there is no timeout. The mutex must + be locked when this method is called. The mutex is unlocked + during the wait and locked again before returning. Returns + true if the condition variable was signalled and false on + timeout. + + (Cancellation point) + */ + virtual bool waitCondVar(CArchCond, CArchMutex, double timeout) = 0; + + // + // mutex methods + // + + //! Create a recursive mutex + /*! + Creates a recursive mutex. A thread may lock a recursive mutex + when it already holds a lock on that mutex. The mutex is an + opaque data type. + */ + virtual CArchMutex newMutex() = 0; + + //! Destroy a mutex + virtual void closeMutex(CArchMutex) = 0; + + //! Lock a mutex + virtual void lockMutex(CArchMutex) = 0; + + //! Unlock a mutex + virtual void unlockMutex(CArchMutex) = 0; + + // + // thread methods + // + + //! Start a new thread + /*! + Creates and starts a new thread, using \c func as the entry point + and passing it \c userData. The thread is an opaque data type. + */ + virtual CArchThread newThread(ThreadFunc func, void* userData) = 0; + + //! Get a reference to the calling thread + /*! + Returns a thread representing the current (i.e. calling) thread. + */ + virtual CArchThread newCurrentThread() = 0; + + //! Copy a thread object + /*! + Returns a reference to to thread referred to by \c thread. + */ + virtual CArchThread copyThread(CArchThread thread) = 0; + + //! Release a thread reference + /*! + Deletes the given thread object. This does not destroy the thread + the object referred to, even if there are no remaining references. + Use cancelThread() and waitThread() to stop a thread and wait for + it to exit. + */ + virtual void closeThread(CArchThread) = 0; + + //! Force a thread to exit + /*! + Causes \c thread to exit when it next calls a cancellation point. + A thread avoids cancellation as long as it nevers calls a + cancellation point. Once it begins the cancellation process it + must always let cancellation go to completion but may take as + long as necessary to clean up. + */ + virtual void cancelThread(CArchThread thread) = 0; + + //! Change thread priority + /*! + Changes the priority of \c thread by \c n. If \c n is positive + the thread has a lower priority and if negative a higher priority. + Some architectures may not support either or both directions. + */ + virtual void setPriorityOfThread(CArchThread, int n) = 0; + + //! Cancellation point + /*! + This method does nothing but is a cancellation point. Clients + can make their own functions cancellation points by calling this + method at appropriate times. + + (Cancellation point) + */ + virtual void testCancelThread() = 0; + + //! Wait for a thread to exit + /*! + Waits for up to \c timeout seconds for \c thread to exit (normally + or by cancellation). Waits forever if \c timeout < 0. Returns + true if the thread exited, false otherwise. Waiting on the current + thread returns immediately with false. + + (Cancellation point) + */ + virtual bool wait(CArchThread thread, double timeout) = 0; + + //! Compare threads + /*! + Returns true iff two thread objects refer to the same thread. + Note that comparing thread objects directly is meaningless. + */ + virtual bool isSameThread(CArchThread, CArchThread) = 0; + + //! Test if thread exited + /*! + Returns true iff \c thread has exited. + */ + virtual bool isExitedThread(CArchThread thread) = 0; + + //! Returns the exit code of a thread + /*! + Waits indefinitely for \c thread to exit (if it hasn't yet) then + returns the thread's exit code. + + (Cancellation point) + */ + virtual void* getResultOfThread(CArchThread thread) = 0; + + //! Returns an ID for a thread + /*! + Returns some ID number for \c thread. This is for logging purposes. + All thread objects referring to the same thread return the same ID. + However, clients should us isSameThread() to compare thread objects + instead of comparing IDs. + */ + virtual ThreadID getIDOfThread(CArchThread thread) = 0; + + //! Set the interrupt handler + /*! + Sets the function to call on receipt of an external interrupt. + By default and when \p func is NULL, the main thread is cancelled. + */ + virtual void setSignalHandler(ESignal, SignalFunc func, + void* userData) = 0; + + //! Invoke the signal handler + /*! + Invokes the signal handler for \p signal, if any. If no handler + cancels the main thread for \c kINTERRUPT and \c kTERMINATE and + ignores the call otherwise. + */ + virtual void raiseSignal(ESignal signal) = 0; + + //@} +}; + +#endif diff --git a/src/lib/arch/IArchNetwork.h b/src/lib/arch/IArchNetwork.h new file mode 100644 index 00000000..cfc17ffc --- /dev/null +++ b/src/lib/arch/IArchNetwork.h @@ -0,0 +1,284 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IARCHNETWORK_H +#define IARCHNETWORK_H + +#include "IInterface.h" +#include "stdstring.h" + +class CArchThreadImpl; +typedef CArchThreadImpl* CArchThread; + +/*! +\class CArchSocketImpl +\brief Internal socket data. +An architecture dependent type holding the necessary data for a socket. +*/ +class CArchSocketImpl; + +/*! +\var CArchSocket +\brief Opaque socket type. +An opaque type representing a socket. +*/ +typedef CArchSocketImpl* CArchSocket; + +/*! +\class CArchNetAddressImpl +\brief Internal network address data. +An architecture dependent type holding the necessary data for a network +address. +*/ +class CArchNetAddressImpl; + +/*! +\var CArchNetAddress +\brief Opaque network address type. +An opaque type representing a network address. +*/ +typedef CArchNetAddressImpl* CArchNetAddress; + +//! Interface for architecture dependent networking +/*! +This interface defines the networking operations required by +synergy. Each architecture must implement this interface. +*/ +class IArchNetwork : public IInterface { +public: + //! Supported address families + enum EAddressFamily { + kUNKNOWN, + kINET, + }; + + //! Supported socket types + enum ESocketType { + kDGRAM, + kSTREAM + }; + + //! Events for \c poll() + /*! + Events for \c poll() are bitmasks and can be combined using the + bitwise operators. + */ + enum { + kPOLLIN = 1, //!< Socket is readable + kPOLLOUT = 2, //!< Socket is writable + kPOLLERR = 4, //!< The socket is in an error state + kPOLLNVAL = 8 //!< The socket is invalid + }; + + //! A socket query for \c poll() + class CPollEntry { + public: + //! The socket to query + CArchSocket m_socket; + + //! The events to query for + /*! + The events to query for can be any combination of kPOLLIN and + kPOLLOUT. + */ + unsigned short m_events; + + //! The result events + unsigned short m_revents; + }; + + //! @name manipulators + //@{ + + //! Create a new socket + /*! + The socket is an opaque data type. + */ + virtual CArchSocket newSocket(EAddressFamily, ESocketType) = 0; + + //! Copy a socket object + /*! + Returns a reference to to socket referred to by \c s. + */ + virtual CArchSocket copySocket(CArchSocket s) = 0; + + //! Release a socket reference + /*! + Deletes the given socket object. This does not destroy the socket + the object referred to until there are no remaining references for + the socket. + */ + virtual void closeSocket(CArchSocket s) = 0; + + //! Close socket for further reads + /*! + Calling this disallows future reads on socket \c s. + */ + virtual void closeSocketForRead(CArchSocket s) = 0; + + //! Close socket for further writes + /*! + Calling this disallows future writes on socket \c s. + */ + virtual void closeSocketForWrite(CArchSocket s) = 0; + + //! Bind socket to address + /*! + Binds socket \c s to the address \c addr. + */ + virtual void bindSocket(CArchSocket s, CArchNetAddress addr) = 0; + + //! Listen for connections on socket + /*! + Causes the socket \c s to begin listening for incoming connections. + */ + virtual void listenOnSocket(CArchSocket s) = 0; + + //! Accept connection on socket + /*! + Accepts a connection on socket \c s, returning a new socket for the + connection and filling in \c addr with the address of the remote + end. \c addr may be NULL if the remote address isn't required. + The original socket \c s is unaffected and remains in the listening + state. The new socket shares most of the properties of \c s except + it's not in the listening state and it's connected. Returns NULL + if there are no pending connection requests. + */ + virtual CArchSocket acceptSocket(CArchSocket s, CArchNetAddress* addr) = 0; + + //! Connect socket + /*! + Connects the socket \c s to the remote address \c addr. Returns + true if the connection succeed immediately, false if the connection + is in progress, and throws if the connection failed immediately. + If it returns false, \c pollSocket() can be used to wait on the + socket for writing to detect when the connection finally succeeds + or fails. + */ + virtual bool connectSocket(CArchSocket s, CArchNetAddress addr) = 0; + + //! Check socket state + /*! + Tests the state of \c num sockets for readability and/or writability. + Waits up to \c timeout seconds for some socket to become readable + and/or writable (or indefinitely if \c timeout < 0). Returns the + number of sockets that were readable (if readability was being + queried) or writable (if writablility was being queried) and sets + the \c m_revents members of the entries. \c kPOLLERR and \c kPOLLNVAL + are set in \c m_revents as appropriate. If a socket indicates + \c kPOLLERR then \c throwErrorOnSocket() can be used to determine + the type of error. Returns 0 immediately regardless of the \c timeout + if no valid sockets are selected for testing. + + (Cancellation point) + */ + virtual int pollSocket(CPollEntry[], int num, double timeout) = 0; + + //! Unblock thread in pollSocket() + /*! + Cause a thread that's in a pollSocket() call to return. This + call may return before the thread is unblocked. If the thread is + not in a pollSocket() call this call has no effect. + */ + virtual void unblockPollSocket(CArchThread thread) = 0; + + //! Read data from socket + /*! + Read up to \c len bytes from socket \c s in \c buf and return the + number of bytes read. The number of bytes can be less than \c len + if not enough data is available. Returns 0 if the remote end has + disconnected and/or there is no more queued received data. + */ + virtual size_t readSocket(CArchSocket s, void* buf, size_t len) = 0; + + //! Write data from socket + /*! + Write up to \c len bytes to socket \c s from \c buf and return the + number of bytes written. The number of bytes can be less than + \c len if the remote end disconnected or the internal buffers fill + up. + */ + virtual size_t writeSocket(CArchSocket s, + const void* buf, size_t len) = 0; + + //! Check error on socket + /*! + If the socket \c s is in an error state then throws an appropriate + XArchNetwork exception. + */ + virtual void throwErrorOnSocket(CArchSocket s) = 0; + + //! Turn Nagle algorithm on or off on socket + /*! + Set socket to send messages immediately (true) or to collect small + messages into one packet (false). Returns the previous state. + */ + virtual bool setNoDelayOnSocket(CArchSocket, bool noDelay) = 0; + + //! Turn address reuse on or off on socket + /*! + Allows the address this socket is bound to to be reused while in the + TIME_WAIT state. Returns the previous state. + */ + virtual bool setReuseAddrOnSocket(CArchSocket, bool reuse) = 0; + + //! Return local host's name + virtual std::string getHostName() = 0; + + //! Create an "any" network address + virtual CArchNetAddress newAnyAddr(EAddressFamily) = 0; + + //! Copy a network address + virtual CArchNetAddress copyAddr(CArchNetAddress) = 0; + + //! Convert a name to a network address + virtual CArchNetAddress nameToAddr(const std::string&) = 0; + + //! Destroy a network address + virtual void closeAddr(CArchNetAddress) = 0; + + //! Convert an address to a host name + virtual std::string addrToName(CArchNetAddress) = 0; + + //! Convert an address to a string + virtual std::string addrToString(CArchNetAddress) = 0; + + //! Get an address's family + virtual EAddressFamily getAddrFamily(CArchNetAddress) = 0; + + //! Set the port of an address + virtual void setAddrPort(CArchNetAddress, int port) = 0; + + //! Get the port of an address + virtual int getAddrPort(CArchNetAddress) = 0; + + //! Test addresses for equality + virtual bool isEqualAddr(CArchNetAddress, CArchNetAddress) = 0; + + //! Test for the "any" address + /*! + Returns true if \c addr is the "any" address. \c newAnyAddr() + returns an "any" address. + */ + virtual bool isAnyAddr(CArchNetAddress addr) = 0; + + //@} + + virtual void init() = 0; +}; + +#endif diff --git a/src/lib/arch/IArchPlugin.h b/src/lib/arch/IArchPlugin.h new file mode 100644 index 00000000..2214b0d8 --- /dev/null +++ b/src/lib/arch/IArchPlugin.h @@ -0,0 +1,41 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 + +#define PLUGINS_DIR "plugins" + +#include "IInterface.h" + +//! Interface for plugin manager. +/*! +A plugin manager should load all 3rd party plugins from the plugins dir, +and then look for common function names in the plugins. +*/ +class IArchPlugin : public IInterface { +public: + //! @name manipulators + //@{ + + //! Load plugins + /*! + Scan the plugins dir and load plugins. + */ + virtual void init(void* eventTarget) = 0; + + //@} +}; diff --git a/src/lib/arch/IArchSleep.h b/src/lib/arch/IArchSleep.h new file mode 100644 index 00000000..e89cc709 --- /dev/null +++ b/src/lib/arch/IArchSleep.h @@ -0,0 +1,46 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IARCHSLEEP_H +#define IARCHSLEEP_H + +#include "IInterface.h" + +//! Interface for architecture dependent sleeping +/*! +This interface defines the sleep operations required by +synergy. Each architecture must implement this interface. +*/ +class IArchSleep : public IInterface { +public: + //! @name manipulators + //@{ + + //! Sleep + /*! + Blocks the calling thread for \c timeout seconds. If + \c timeout < 0.0 then the call returns immediately. If \c timeout + == 0.0 then the calling thread yields the CPU. + + (cancellation point) + */ + virtual void sleep(double timeout) = 0; + + //@} +}; + +#endif diff --git a/src/lib/arch/IArchString.cpp b/src/lib/arch/IArchString.cpp new file mode 100644 index 00000000..21d56faa --- /dev/null +++ b/src/lib/arch/IArchString.cpp @@ -0,0 +1,187 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IArchString.h" +#include "common.h" +#include "CArch.h" +#include +#include +#include + +static CArchMutex s_mutex = NULL; + +// +// use C library non-reentrant multibyte conversion with mutex +// + +IArchString::~IArchString() +{ + if (s_mutex != NULL) { + ARCH->closeMutex(s_mutex); + s_mutex = NULL; + } +} + +int +IArchString::convStringWCToMB(char* dst, + const wchar_t* src, UInt32 n, bool* errors) +{ + int len = 0; + + bool dummyErrors; + if (errors == NULL) { + errors = &dummyErrors; + } + + if (s_mutex == NULL) { + s_mutex = ARCH->newMutex(); + } + + ARCH->lockMutex(s_mutex); + + if (dst == NULL) { + char dummy[MB_LEN_MAX]; + for (const wchar_t* scan = src; n > 0; ++scan, --n) { + int mblen = wctomb(dummy, *scan); + if (mblen == -1) { + *errors = true; + mblen = 1; + } + len += mblen; + } + int mblen = wctomb(dummy, L'\0'); + if (mblen != -1) { + len += mblen - 1; + } + } + else { + char* dst0 = dst; + for (const wchar_t* scan = src; n > 0; ++scan, --n) { + int mblen = wctomb(dst, *scan); + if (mblen == -1) { + *errors = true; + *dst++ = '?'; + } + else { + dst += mblen; + } + } + int mblen = wctomb(dst, L'\0'); + if (mblen != -1) { + // don't include nul terminator + dst += mblen - 1; + } + len = (int)(dst - dst0); + } + ARCH->unlockMutex(s_mutex); + + return len; +} + +int +IArchString::convStringMBToWC(wchar_t* dst, + const char* src, UInt32 n, bool* errors) +{ + int len = 0; + wchar_t dummy; + + bool dummyErrors; + if (errors == NULL) { + errors = &dummyErrors; + } + + if (s_mutex == NULL) { + s_mutex = ARCH->newMutex(); + } + + ARCH->lockMutex(s_mutex); + + if (dst == NULL) { + for (const char* scan = src; n > 0; ) { + int mblen = mbtowc(&dummy, scan, n); + switch (mblen) { + case -2: + // incomplete last character. convert to unknown character. + *errors = true; + len += 1; + n = 0; + break; + + case -1: + // invalid character. count one unknown character and + // start at the next byte. + *errors = true; + len += 1; + scan += 1; + n -= 1; + break; + + case 0: + len += 1; + scan += 1; + n -= 1; + break; + + default: + // normal character + len += 1; + scan += mblen; + n -= mblen; + break; + } + } + } + else { + wchar_t* dst0 = dst; + for (const char* scan = src; n > 0; ++dst) { + int mblen = mbtowc(dst, scan, n); + switch (mblen) { + case -2: + // incomplete character. convert to unknown character. + *errors = true; + *dst = (wchar_t)0xfffd; + n = 0; + break; + + case -1: + // invalid character. count one unknown character and + // start at the next byte. + *errors = true; + *dst = (wchar_t)0xfffd; + scan += 1; + n -= 1; + break; + + case 0: + *dst = (wchar_t)0x0000; + scan += 1; + n -= 1; + break; + + default: + // normal character + scan += mblen; + n -= mblen; + break; + } + } + len = (int)(dst - dst0); + } + ARCH->unlockMutex(s_mutex); + + return len; +} diff --git a/src/lib/arch/IArchString.h b/src/lib/arch/IArchString.h new file mode 100644 index 00000000..c180bce9 --- /dev/null +++ b/src/lib/arch/IArchString.h @@ -0,0 +1,73 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IARCHSTRING_H +#define IARCHSTRING_H + +#include "IInterface.h" +#include "BasicTypes.h" +#include + +//! Interface for architecture dependent string operations +/*! +This interface defines the string operations required by +synergy. Each architecture must implement this interface. +*/ +class IArchString : public IInterface { +public: + virtual ~IArchString(); + + //! Wide character encodings + /*! + The known wide character encodings + */ + enum EWideCharEncoding { + kUCS2, //!< The UCS-2 encoding + kUCS4, //!< The UCS-4 encoding + kUTF16, //!< The UTF-16 encoding + kUTF32 //!< The UTF-32 encoding + }; + + //! @name manipulators + //@{ + + //! printf() to limited size buffer with va_list + /*! + This method is equivalent to vsprintf() except it will not write + more than \c n bytes to the buffer, returning -1 if the output + was truncated and the number of bytes written not including the + trailing NUL otherwise. + */ + virtual int vsnprintf(char* str, + int size, const char* fmt, va_list ap); + + //! Convert multibyte string to wide character string + virtual int convStringMBToWC(wchar_t*, + const char*, UInt32 n, bool* errors); + + //! Convert wide character string to multibyte string + virtual int convStringWCToMB(char*, + const wchar_t*, UInt32 n, bool* errors); + + //! Return the architecture's native wide character encoding + virtual EWideCharEncoding + getWideCharEncoding() = 0; + + //@} +}; + +#endif diff --git a/src/lib/arch/IArchSystem.h b/src/lib/arch/IArchSystem.h new file mode 100644 index 00000000..2c10bf03 --- /dev/null +++ b/src/lib/arch/IArchSystem.h @@ -0,0 +1,61 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IARCHSYSTEM_H +#define IARCHSYSTEM_H + +#include "IInterface.h" +#include "stdstring.h" + +//! Interface for architecture dependent system queries +/*! +This interface defines operations for querying system info. +*/ +class IArchSystem : public IInterface { +public: + //! @name accessors + //@{ + + //! Identify the OS + /*! + Returns a string identifying the operating system. + */ + virtual std::string getOSName() const = 0; + + //! Identify the platform + /*! + Returns a string identifying the platform this OS is running on. + */ + virtual std::string getPlatformName() const = 0; + //@} + + //! Get a Synergy setting + /*! + Reads a Synergy setting from the system. + */ + virtual std::string setting(const std::string& valueName) const = 0; + //@} + + //! Set a Synergy setting + /*! + Writes a Synergy setting from the system. + */ + virtual void setting(const std::string& valueName, const std::string& valueString) const = 0; + //@} +}; + +#endif diff --git a/src/lib/arch/IArchTaskBar.h b/src/lib/arch/IArchTaskBar.h new file mode 100644 index 00000000..1398a88a --- /dev/null +++ b/src/lib/arch/IArchTaskBar.h @@ -0,0 +1,65 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IARCHTASKBAR_H +#define IARCHTASKBAR_H + +#include "IInterface.h" + +class IArchTaskBarReceiver; + +//! Interface for architecture dependent task bar control +/*! +This interface defines the task bar icon operations required +by synergy. Each architecture must implement this interface +though each operation can be a no-op. +*/ +class IArchTaskBar : public IInterface { +public: + //! @name manipulators + //@{ + + //! Add a receiver + /*! + Add a receiver object to be notified of user and application + events. This should be called before other methods. When + the receiver is added to the task bar, its icon appears on + the task bar. + */ + virtual void addReceiver(IArchTaskBarReceiver*) = 0; + + //! Remove a receiver + /*! + Remove a receiver object from the task bar. This removes the + icon from the task bar. + */ + virtual void removeReceiver(IArchTaskBarReceiver*) = 0; + + //! Update a receiver + /*! + Updates the display of the receiver on the task bar. This + should be called when the receiver appearance may have changed + (e.g. it's icon or tool tip has changed). + */ + virtual void updateReceiver(IArchTaskBarReceiver*) = 0; + + //@} + + virtual void init() = 0; +}; + +#endif diff --git a/src/lib/arch/IArchTaskBarReceiver.h b/src/lib/arch/IArchTaskBarReceiver.h new file mode 100644 index 00000000..0cf1a338 --- /dev/null +++ b/src/lib/arch/IArchTaskBarReceiver.h @@ -0,0 +1,100 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IARCHTASKBARRECEIVER_H +#define IARCHTASKBARRECEIVER_H + +#include "IInterface.h" +#include "stdstring.h" + +class IScreen; +class INode; + +//! Interface for architecture dependent task bar event handling +/*! +This interface defines the task bar icon event handlers required +by synergy. Each architecture must implement this interface +though each operation can be a no-op. +*/ +class IArchTaskBarReceiver : public IInterface { +public: + // Icon data is architecture dependent + typedef void* Icon; + + //! @name manipulators + //@{ + + //! Show status window + /*! + Open a window displaying current status. This should return + immediately without waiting for the window to be closed. + */ + virtual void showStatus() = 0; + + //! Popup menu + /*! + Popup a menu of operations at or around \c x,y and perform the + chosen operation. + */ + virtual void runMenu(int x, int y) = 0; + + //! Perform primary action + /*! + Perform the primary (default) action. + */ + virtual void primaryAction() = 0; + + //@} + //! @name accessors + //@{ + + //! Lock receiver + /*! + Locks the receiver from changing state. The receiver should be + locked when querying it's state to ensure consistent results. + Each call to \c lock() must have a matching \c unlock() and + locks cannot be nested. + */ + virtual void lock() const = 0; + + //! Unlock receiver + virtual void unlock() const = 0; + + //! Get icon + /*! + Returns the icon to display in the task bar. The interface + to set the icon is left to subclasses. Getting and setting + the icon must be thread safe. + */ + virtual const Icon getIcon() const = 0; + + //! Get tooltip + /*! + Returns the tool tip to display in the task bar. The interface + to set the tooltip is left to sublclasses. Getting and setting + the icon must be thread safe. + */ + virtual std::string getToolTip() const = 0; + + virtual void updateStatus(INode*, const CString& errorMsg) = 0; + + virtual void cleanup() {} + + //@} +}; + +#endif diff --git a/src/lib/arch/IArchTime.h b/src/lib/arch/IArchTime.h new file mode 100644 index 00000000..6d48b7d5 --- /dev/null +++ b/src/lib/arch/IArchTime.h @@ -0,0 +1,43 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IARCHTIME_H +#define IARCHTIME_H + +#include "IInterface.h" + +//! Interface for architecture dependent time operations +/*! +This interface defines the time operations required by +synergy. Each architecture must implement this interface. +*/ +class IArchTime : public IInterface { +public: + //! @name manipulators + //@{ + + //! Get the current time + /*! + Returns the number of seconds since some arbitrary starting time. + This should return as high a precision as reasonable. + */ + virtual double time() = 0; + + //@} +}; + +#endif diff --git a/src/lib/arch/XArch.cpp b/src/lib/arch/XArch.cpp new file mode 100644 index 00000000..e322ddc6 --- /dev/null +++ b/src/lib/arch/XArch.cpp @@ -0,0 +1,36 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "XArch.h" + +// +// XArch +// + +std::string +XArch::what() const throw() +{ + try { + if (m_what.empty() && m_eval != NULL) { + m_what = m_eval->eval(); + } + } + catch (...) { + // ignore + } + return m_what; +} diff --git a/src/lib/arch/XArch.h b/src/lib/arch/XArch.h new file mode 100644 index 00000000..7541f76f --- /dev/null +++ b/src/lib/arch/XArch.h @@ -0,0 +1,173 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef XARCH_H +#define XARCH_H + +#include "common.h" +#include "stdstring.h" + +//! Generic thread exception +/*! +Exceptions derived from this class are used by the multithreading +library to perform stack unwinding when a thread terminates. These +exceptions must always be rethrown by clients when caught. +*/ +class XThread { }; + +//! Thread exception to cancel +/*! +Thrown to cancel a thread. Clients must not throw this type, but +must rethrow it if caught (by XThreadCancel, XThread, or ...). +*/ +class XThreadCancel : public XThread { }; + +/*! +\def RETHROW_XTHREAD +Convenience macro to rethrow an XThread exception but ignore other +exceptions. Put this in your catch (...) handler after necessary +cleanup but before leaving or returning from the handler. +*/ +#define RETHROW_XTHREAD \ + try { throw; } catch (XThread&) { throw; } catch (...) { } + +//! Lazy error message string evaluation +/*! +This class encapsulates platform dependent error string lookup. +Platforms subclass this type, taking an appropriate error code +type in the c'tor and overriding eval() to return the error +string for that error code. +*/ +class XArchEval { +public: + XArchEval() { } + virtual ~XArchEval() { } + + virtual XArchEval* clone() const throw() = 0; + + virtual std::string eval() const throw() = 0; +}; + +//! Generic exception architecture dependent library +class XArch { +public: + XArch(XArchEval* adoptedEvaluator) : m_eval(adoptedEvaluator) { } + XArch(const std::string& msg) : m_eval(NULL), m_what(msg) { } + XArch(const XArch& e) : m_eval(e.m_eval != NULL ? e.m_eval->clone() : NULL), + m_what(e.m_what) { } + ~XArch() { delete m_eval; } + + std::string what() const throw(); + +private: + XArchEval* m_eval; + mutable std::string m_what; +}; + +// Macro to declare XArch derived types +#define XARCH_SUBCLASS(name_, super_) \ +class name_ : public super_ { \ +public: \ + name_(XArchEval* adoptedEvaluator) : super_(adoptedEvaluator) { } \ + name_(const std::string& msg) : super_(msg) { } \ +} + +//! Generic network exception +/*! +Exceptions derived from this class are used by the networking +library to indicate various errors. +*/ +XARCH_SUBCLASS(XArchNetwork, XArch); + +//! Operation was interrupted +XARCH_SUBCLASS(XArchNetworkInterrupted, XArchNetwork); + +//! Network insufficient permission +XARCH_SUBCLASS(XArchNetworkAccess, XArchNetwork); + +//! Network insufficient resources +XARCH_SUBCLASS(XArchNetworkResource, XArchNetwork); + +//! No support for requested network resource/service +XARCH_SUBCLASS(XArchNetworkSupport, XArchNetwork); + +//! Network I/O error +XARCH_SUBCLASS(XArchNetworkIO, XArchNetwork); + +//! Network address is unavailable or not local +XARCH_SUBCLASS(XArchNetworkNoAddress, XArchNetwork); + +//! Network address in use +XARCH_SUBCLASS(XArchNetworkAddressInUse, XArchNetwork); + +//! No route to address +XARCH_SUBCLASS(XArchNetworkNoRoute, XArchNetwork); + +//! Socket not connected +XARCH_SUBCLASS(XArchNetworkNotConnected, XArchNetwork); + +//! Remote read end of socket has closed +XARCH_SUBCLASS(XArchNetworkShutdown, XArchNetwork); + +//! Remote end of socket has disconnected +XARCH_SUBCLASS(XArchNetworkDisconnected, XArchNetwork); + +//! Remote end of socket refused connection +XARCH_SUBCLASS(XArchNetworkConnectionRefused, XArchNetwork); + +//! Remote end of socket is not responding +XARCH_SUBCLASS(XArchNetworkTimedOut, XArchNetwork); + +//! Generic network name lookup erros +XARCH_SUBCLASS(XArchNetworkName, XArchNetwork); + +//! The named host is unknown +XARCH_SUBCLASS(XArchNetworkNameUnknown, XArchNetworkName); + +//! The named host is known but has no address +XARCH_SUBCLASS(XArchNetworkNameNoAddress, XArchNetworkName); + +//! Non-recoverable name server error +XARCH_SUBCLASS(XArchNetworkNameFailure, XArchNetworkName); + +//! Temporary name server error +XARCH_SUBCLASS(XArchNetworkNameUnavailable, XArchNetworkName); + +//! The named host is known but no supported address +XARCH_SUBCLASS(XArchNetworkNameUnsupported, XArchNetworkName); + +//! Generic daemon exception +/*! +Exceptions derived from this class are used by the daemon +library to indicate various errors. +*/ +XARCH_SUBCLASS(XArchDaemon, XArch); + +//! Could not daemonize +XARCH_SUBCLASS(XArchDaemonFailed, XArchDaemon); + +//! Could not install daemon +XARCH_SUBCLASS(XArchDaemonInstallFailed, XArchDaemon); + +//! Could not uninstall daemon +XARCH_SUBCLASS(XArchDaemonUninstallFailed, XArchDaemon); + +//! Attempted to uninstall a daemon that was not installed +XARCH_SUBCLASS(XArchDaemonUninstallNotInstalled, XArchDaemonUninstallFailed); + + +#endif diff --git a/src/lib/arch/XArchUnix.cpp b/src/lib/arch/XArchUnix.cpp new file mode 100644 index 00000000..0cd587a5 --- /dev/null +++ b/src/lib/arch/XArchUnix.cpp @@ -0,0 +1,36 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "XArchUnix.h" +#include + +// +// XArchEvalUnix +// + +XArchEval* +XArchEvalUnix::clone() const throw() +{ + return new XArchEvalUnix(m_errno); +} + +std::string +XArchEvalUnix::eval() const throw() +{ + // FIXME -- not thread safe + return strerror(m_errno); +} diff --git a/src/lib/arch/XArchUnix.h b/src/lib/arch/XArchUnix.h new file mode 100644 index 00000000..16128b06 --- /dev/null +++ b/src/lib/arch/XArchUnix.h @@ -0,0 +1,37 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef XARCHUNIX_H +#define XARCHUNIX_H + +#include "XArch.h" + +//! Lazy error message string evaluation for unix +class XArchEvalUnix : public XArchEval { +public: + XArchEvalUnix(int err) : m_errno(err) { } + virtual ~XArchEvalUnix() { } + + // XArchEval overrides + virtual XArchEval* clone() const throw(); + virtual std::string eval() const throw(); + +private: + int m_errno; +}; + +#endif diff --git a/src/lib/arch/XArchWindows.cpp b/src/lib/arch/XArchWindows.cpp new file mode 100644 index 00000000..425a9edf --- /dev/null +++ b/src/lib/arch/XArchWindows.cpp @@ -0,0 +1,130 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "XArchWindows.h" +#include "CArchNetworkWinsock.h" + +// +// XArchEvalWindows +// + +XArchEval* +XArchEvalWindows::clone() const throw() +{ + return new XArchEvalWindows(m_errno); +} + +std::string +XArchEvalWindows::eval() const throw() +{ + char* cmsg; + if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_IGNORE_INSERTS | + FORMAT_MESSAGE_FROM_SYSTEM, + 0, + m_errno, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR)&cmsg, + 0, + NULL) == 0) { + cmsg = NULL; + return "Unknown error"; + } + std::string smsg(cmsg); + LocalFree(cmsg); + return smsg; +} + + +// +// XArchEvalWinsock +// + +XArchEval* +XArchEvalWinsock::clone() const throw() +{ + return new XArchEvalWinsock(m_errno); +} + +std::string +XArchEvalWinsock::eval() const throw() +{ + // built-in windows function for looking up error message strings + // may not look up network error messages correctly. we'll have + // to do it ourself. + static const struct { int m_code; const char* m_msg; } s_netErrorCodes[] = { + /* 10004 */{WSAEINTR, "The (blocking) call was canceled via WSACancelBlockingCall"}, + /* 10009 */{WSAEBADF, "Bad file handle"}, + /* 10013 */{WSAEACCES, "The requested address is a broadcast address, but the appropriate flag was not set"}, + /* 10014 */{WSAEFAULT, "WSAEFAULT"}, + /* 10022 */{WSAEINVAL, "WSAEINVAL"}, + /* 10024 */{WSAEMFILE, "No more file descriptors available"}, + /* 10035 */{WSAEWOULDBLOCK, "Socket is marked as non-blocking and no connections are present or the receive operation would block"}, + /* 10036 */{WSAEINPROGRESS, "A blocking Windows Sockets operation is in progress"}, + /* 10037 */{WSAEALREADY, "The asynchronous routine being canceled has already completed"}, + /* 10038 */{WSAENOTSOCK, "At least on descriptor is not a socket"}, + /* 10039 */{WSAEDESTADDRREQ, "A destination address is required"}, + /* 10040 */{WSAEMSGSIZE, "The datagram was too large to fit into the specified buffer and was truncated"}, + /* 10041 */{WSAEPROTOTYPE, "The specified protocol is the wrong type for this socket"}, + /* 10042 */{WSAENOPROTOOPT, "The option is unknown or unsupported"}, + /* 10043 */{WSAEPROTONOSUPPORT,"The specified protocol is not supported"}, + /* 10044 */{WSAESOCKTNOSUPPORT,"The specified socket type is not supported by this address family"}, + /* 10045 */{WSAEOPNOTSUPP, "The referenced socket is not a type that supports that operation"}, + /* 10046 */{WSAEPFNOSUPPORT, "BSD: Protocol family not supported"}, + /* 10047 */{WSAEAFNOSUPPORT, "The specified address family is not supported"}, + /* 10048 */{WSAEADDRINUSE, "The specified address is already in use"}, + /* 10049 */{WSAEADDRNOTAVAIL, "The specified address is not available from the local machine"}, + /* 10050 */{WSAENETDOWN, "The Windows Sockets implementation has detected that the network subsystem has failed"}, + /* 10051 */{WSAENETUNREACH, "The network can't be reached from this host at this time"}, + /* 10052 */{WSAENETRESET, "The connection must be reset because the Windows Sockets implementation dropped it"}, + /* 10053 */{WSAECONNABORTED, "The virtual circuit was aborted due to timeout or other failure"}, + /* 10054 */{WSAECONNRESET, "The virtual circuit was reset by the remote side"}, + /* 10055 */{WSAENOBUFS, "No buffer space is available or a buffer deadlock has occured. The socket cannot be created"}, + /* 10056 */{WSAEISCONN, "The socket is already connected"}, + /* 10057 */{WSAENOTCONN, "The socket is not connected"}, + /* 10058 */{WSAESHUTDOWN, "The socket has been shutdown"}, + /* 10059 */{WSAETOOMANYREFS, "BSD: Too many references"}, + /* 10060 */{WSAETIMEDOUT, "Attempt to connect timed out without establishing a connection"}, + /* 10061 */{WSAECONNREFUSED, "Connection was refused"}, + /* 10062 */{WSAELOOP, "Undocumented WinSock error code used in BSD"}, + /* 10063 */{WSAENAMETOOLONG, "Undocumented WinSock error code used in BSD"}, + /* 10064 */{WSAEHOSTDOWN, "Undocumented WinSock error code used in BSD"}, + /* 10065 */{WSAEHOSTUNREACH, "No route to host"}, + /* 10066 */{WSAENOTEMPTY, "Undocumented WinSock error code"}, + /* 10067 */{WSAEPROCLIM, "Undocumented WinSock error code"}, + /* 10068 */{WSAEUSERS, "Undocumented WinSock error code"}, + /* 10069 */{WSAEDQUOT, "Undocumented WinSock error code"}, + /* 10070 */{WSAESTALE, "Undocumented WinSock error code"}, + /* 10071 */{WSAEREMOTE, "Undocumented WinSock error code"}, + /* 10091 */{WSASYSNOTREADY, "Underlying network subsytem is not ready for network communication"}, + /* 10092 */{WSAVERNOTSUPPORTED, "The version of WinSock API support requested is not provided in this implementation"}, + /* 10093 */{WSANOTINITIALISED, "WinSock subsystem not properly initialized"}, + /* 10101 */{WSAEDISCON, "Virtual circuit has gracefully terminated connection"}, + /* 11001 */{WSAHOST_NOT_FOUND, "The specified host is unknown"}, + /* 11002 */{WSATRY_AGAIN, "A temporary error occurred on an authoritative name server"}, + /* 11003 */{WSANO_RECOVERY, "A non-recoverable name server error occurred"}, + /* 11004 */{WSANO_DATA, "The requested name is valid but does not have an IP address"}, + /* end */{0, NULL} + }; + + for (unsigned int i = 0; s_netErrorCodes[i].m_code != 0; ++i) { + if (s_netErrorCodes[i].m_code == m_errno) { + return s_netErrorCodes[i].m_msg; + } + } + return "Unknown error"; +} diff --git a/src/lib/arch/XArchWindows.h b/src/lib/arch/XArchWindows.h new file mode 100644 index 00000000..186c5238 --- /dev/null +++ b/src/lib/arch/XArchWindows.h @@ -0,0 +1,55 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef XARCHWINDOWS_H +#define XARCHWINDOWS_H + +#define WIN32_LEAN_AND_MEAN + +#include "XArch.h" +#include + +//! Lazy error message string evaluation for windows +class XArchEvalWindows : public XArchEval { +public: + XArchEvalWindows() : m_errno(GetLastError()) { } + XArchEvalWindows(DWORD err) : m_errno(err) { } + virtual ~XArchEvalWindows() { } + + // XArchEval overrides + virtual XArchEval* clone() const throw(); + virtual std::string eval() const throw(); + +private: + DWORD m_errno; +}; + +//! Lazy error message string evaluation for winsock +class XArchEvalWinsock : public XArchEval { +public: + XArchEvalWinsock(int err) : m_errno(err) { } + virtual ~XArchEvalWinsock() { } + + // XArchEval overrides + virtual XArchEval* clone() const throw(); + virtual std::string eval() const throw(); + +private: + int m_errno; +}; + +#endif diff --git a/src/lib/arch/vsnprintf.h b/src/lib/arch/vsnprintf.h new file mode 100644 index 00000000..c8e1a471 --- /dev/null +++ b/src/lib/arch/vsnprintf.h @@ -0,0 +1,66 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IArchString.h" + +#if HAVE_VSNPRINTF + +#if !defined(ARCH_VSNPRINTF) +# define ARCH_VSNPRINTF vsnprintf +#endif + +int +IArchString::vsnprintf(char* str, int size, const char* fmt, va_list ap) +{ + int n = ::ARCH_VSNPRINTF(str, size, fmt, ap); + if (n > size) { + n = -1; + } + return n; +} + +#elif SYSAPI_UNIX // !HAVE_VSNPRINTF + +#include + +int +IArchString::vsnprintf(char* str, int size, const char* fmt, va_list ap) +{ + static FILE* bitbucket = fopen("/dev/null", "w"); + if (bitbucket == NULL) { + // uh oh + if (size > 0) { + str[0] = '\0'; + } + return 0; + } + else { + // count the characters using the bitbucket + int n = vfprintf(bitbucket, fmt, ap); + if (n + 1 <= size) { + // it'll fit so print it into str + vsprintf(str, fmt, ap); + } + return n; + } +} + +#else // !HAVE_VSNPRINTF && !SYSAPI_UNIX + +#error vsnprintf not implemented + +#endif // !HAVE_VSNPRINTF diff --git a/src/lib/base/CEvent.cpp b/src/lib/base/CEvent.cpp new file mode 100644 index 00000000..b2e5cd86 --- /dev/null +++ b/src/lib/base/CEvent.cpp @@ -0,0 +1,83 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CEvent.h" +#include "CEventQueue.h" + +// +// CEvent +// + +CEvent::CEvent() : + m_type(kUnknown), + m_target(NULL), + m_data(NULL), + m_flags(0) +{ + // do nothing +} + +CEvent::CEvent(Type type, void* target, void* data, Flags flags) : + m_type(type), + m_target(target), + m_data(data), + m_flags(flags) +{ + // do nothing +} + +CEvent::Type +CEvent::getType() const +{ + return m_type; +} + +void* +CEvent::getTarget() const +{ + return m_target; +} + +void* +CEvent::getData() const +{ + return m_data; +} + +CEvent::Flags +CEvent::getFlags() const +{ + return m_flags; +} + +void +CEvent::deleteData(const CEvent& event) +{ + switch (event.getType()) { + case kUnknown: + case kQuit: + case kSystem: + case kTimer: + break; + + default: + if ((event.getFlags() & kDontFreeData) == 0) { + free(event.getData()); + } + break; + } +} diff --git a/src/lib/base/CEvent.h b/src/lib/base/CEvent.h new file mode 100644 index 00000000..37f322e4 --- /dev/null +++ b/src/lib/base/CEvent.h @@ -0,0 +1,105 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CEVENT_H +#define CEVENT_H + +#include "BasicTypes.h" +#include "stdmap.h" + +//! Event +/*! +A \c CEvent holds an event type and a pointer to event data. +*/ +class CEvent { +public: + typedef UInt32 Type; + enum { + kUnknown, //!< The event type is unknown + kQuit, //!< The quit event + kSystem, //!< The data points to a system event type + kTimer, //!< The data points to timer info + kLast //!< Must be last + }; + + typedef UInt32 Flags; + enum { + kNone = 0x00, //!< No flags + kDeliverImmediately = 0x01, //!< Dispatch and free event immediately + kDontFreeData = 0x02 //!< Don't free data in deleteData + }; + + CEvent(); + + //! Create \c CEvent with data + /*! + The \p type must have been registered using \c registerType(). + The \p data must be POD (plain old data) allocated by malloc(), + which means it cannot have a constructor, destructor or be + composed of any types that do. \p target is the intended + recipient of the event. \p flags is any combination of \c Flags. + */ + CEvent(Type type, void* target = NULL, void* data = NULL, + Flags flags = kNone); + + //! @name manipulators + //@{ + + //! Release event data + /*! + Deletes event data for the given event (using free()). + */ + static void deleteData(const CEvent&); + + //@} + //! @name accessors + //@{ + + //! Get event type + /*! + Returns the event type. + */ + Type getType() const; + + //! Get the event target + /*! + Returns the event target. + */ + void* getTarget() const; + + //! Get the event data + /*! + Returns the event data. + */ + void* getData() const; + + //! Get event flags + /*! + Returns the event flags. + */ + Flags getFlags() const; + + //@} + +private: + Type m_type; + void* m_target; + void* m_data; + Flags m_flags; +}; + +#endif diff --git a/src/lib/base/CEventQueue.cpp b/src/lib/base/CEventQueue.cpp new file mode 100644 index 00000000..639e8bac --- /dev/null +++ b/src/lib/base/CEventQueue.cpp @@ -0,0 +1,541 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CEventQueue.h" +#include "CLog.h" +#include "CSimpleEventQueueBuffer.h" +#include "CStopwatch.h" +#include "IEventJob.h" +#include "CArch.h" + +// interrupt handler. this just adds a quit event to the queue. +static +void +interrupt(CArch::ESignal, void*) +{ + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); +} + + +// +// CEventQueue +// + +CEventQueue::CEventQueue() : + m_nextType(CEvent::kLast) +{ + setInstance(this); + m_mutex = ARCH->newMutex(); + ARCH->setSignalHandler(CArch::kINTERRUPT, &interrupt, NULL); + ARCH->setSignalHandler(CArch::kTERMINATE, &interrupt, NULL); + m_buffer = new CSimpleEventQueueBuffer; +} + +CEventQueue::~CEventQueue() +{ + delete m_buffer; + ARCH->setSignalHandler(CArch::kINTERRUPT, NULL, NULL); + ARCH->setSignalHandler(CArch::kTERMINATE, NULL, NULL); + ARCH->closeMutex(m_mutex); + setInstance(NULL); +} + +CEvent::Type +CEventQueue::registerType(const char* name) +{ + CArchMutexLock lock(m_mutex); + m_typeMap.insert(std::make_pair(m_nextType, name)); + m_nameMap.insert(std::make_pair(name, m_nextType)); + LOG((CLOG_DEBUG1 "registered event type %s as %d", name, m_nextType)); + return m_nextType++; +} + +CEvent::Type +CEventQueue::registerTypeOnce(CEvent::Type& type, const char* name) +{ + CArchMutexLock lock(m_mutex); + if (type == CEvent::kUnknown) { + m_typeMap.insert(std::make_pair(m_nextType, name)); + m_nameMap.insert(std::make_pair(name, m_nextType)); + LOG((CLOG_DEBUG1 "registered event type %s as %d", name, m_nextType)); + type = m_nextType++; + } + return type; +} + +const char* +CEventQueue::getTypeName(CEvent::Type type) +{ + switch (type) { + case CEvent::kUnknown: + return "nil"; + + case CEvent::kQuit: + return "quit"; + + case CEvent::kSystem: + return "system"; + + case CEvent::kTimer: + return "timer"; + + default: + CTypeMap::const_iterator i = m_typeMap.find(type); + if (i == m_typeMap.end()) { + return ""; + } + else { + return i->second; + } + } +} + +void +CEventQueue::adoptBuffer(IEventQueueBuffer* buffer) +{ + CArchMutexLock lock(m_mutex); + + // discard old buffer and old events + delete m_buffer; + for (CEventTable::iterator i = m_events.begin(); i != m_events.end(); ++i) { + CEvent::deleteData(i->second); + } + m_events.clear(); + m_oldEventIDs.clear(); + + // use new buffer + m_buffer = buffer; + if (m_buffer == NULL) { + m_buffer = new CSimpleEventQueueBuffer; + } +} + +bool +CEventQueue::getEvent(CEvent& event, double timeout) +{ + CStopwatch timer(true); +retry: + // if no events are waiting then handle timers and then wait + while (m_buffer->isEmpty()) { + // handle timers first + if (hasTimerExpired(event)) { + return true; + } + + // get time remaining in timeout + double timeLeft = timeout - timer.getTime(); + if (timeout >= 0.0 && timeLeft <= 0.0) { + return false; + } + + // get time until next timer expires. if there is a timer + // and it'll expire before the client's timeout then use + // that duration for our timeout instead. + double timerTimeout = getNextTimerTimeout(); + if (timeout < 0.0 || (timerTimeout >= 0.0 && timerTimeout < timeLeft)) { + timeLeft = timerTimeout; + } + + // wait for an event + m_buffer->waitForEvent(timeLeft); + } + + // get the event + UInt32 dataID; + IEventQueueBuffer::Type type = m_buffer->getEvent(event, dataID); + switch (type) { + case IEventQueueBuffer::kNone: + if (timeout < 0.0 || timeout <= timer.getTime()) { + // don't want to fail if client isn't expecting that + // so if getEvent() fails with an infinite timeout + // then just try getting another event. + goto retry; + } + return false; + + case IEventQueueBuffer::kSystem: + return true; + + case IEventQueueBuffer::kUser: + { + CArchMutexLock lock(m_mutex); + event = removeEvent(dataID); + return true; + } + + default: + assert(0 && "invalid event type"); + return false; + } +} + +bool +CEventQueue::dispatchEvent(const CEvent& event) +{ + void* target = event.getTarget(); + IEventJob* job = getHandler(event.getType(), target); + if (job == NULL) { + job = getHandler(CEvent::kUnknown, target); + } + if (job != NULL) { + job->run(event); + return true; + } + return false; +} + +void +CEventQueue::addEvent(const CEvent& event) +{ + // discard bogus event types + switch (event.getType()) { + case CEvent::kUnknown: + case CEvent::kSystem: + case CEvent::kTimer: + return; + + default: + break; + } + + if ((event.getFlags() & CEvent::kDeliverImmediately) != 0) { + dispatchEvent(event); + CEvent::deleteData(event); + } + else { + CArchMutexLock lock(m_mutex); + + // store the event's data locally + UInt32 eventID = saveEvent(event); + + // add it + if (!m_buffer->addEvent(eventID)) { + // failed to send event + removeEvent(eventID); + CEvent::deleteData(event); + } + } +} + +CEventQueueTimer* +CEventQueue::newTimer(double duration, void* target) +{ + assert(duration > 0.0); + + CEventQueueTimer* timer = m_buffer->newTimer(duration, false); + if (target == NULL) { + target = timer; + } + CArchMutexLock lock(m_mutex); + m_timers.insert(timer); + // initial duration is requested duration plus whatever's on + // the clock currently because the latter will be subtracted + // the next time we check for timers. + m_timerQueue.push(CTimer(timer, duration, + duration + m_time.getTime(), target, false)); + return timer; +} + +CEventQueueTimer* +CEventQueue::newOneShotTimer(double duration, void* target) +{ + assert(duration > 0.0); + + CEventQueueTimer* timer = m_buffer->newTimer(duration, true); + if (target == NULL) { + target = timer; + } + CArchMutexLock lock(m_mutex); + m_timers.insert(timer); + // initial duration is requested duration plus whatever's on + // the clock currently because the latter will be subtracted + // the next time we check for timers. + m_timerQueue.push(CTimer(timer, duration, + duration + m_time.getTime(), target, true)); + return timer; +} + +void +CEventQueue::deleteTimer(CEventQueueTimer* timer) +{ + CArchMutexLock lock(m_mutex); + for (CTimerQueue::iterator index = m_timerQueue.begin(); + index != m_timerQueue.end(); ++index) { + if (index->getTimer() == timer) { + m_timerQueue.erase(index); + break; + } + } + CTimers::iterator index = m_timers.find(timer); + if (index != m_timers.end()) { + m_timers.erase(index); + } + m_buffer->deleteTimer(timer); +} + +void +CEventQueue::adoptHandler(CEvent::Type type, void* target, IEventJob* handler) +{ + CArchMutexLock lock(m_mutex); + IEventJob*& job = m_handlers[target][type]; + delete job; + job = handler; +} + +void +CEventQueue::removeHandler(CEvent::Type type, void* target) +{ + IEventJob* handler = NULL; + { + CArchMutexLock lock(m_mutex); + CHandlerTable::iterator index = m_handlers.find(target); + if (index != m_handlers.end()) { + CTypeHandlerTable& typeHandlers = index->second; + CTypeHandlerTable::iterator index2 = typeHandlers.find(type); + if (index2 != typeHandlers.end()) { + handler = index2->second; + typeHandlers.erase(index2); + } + } + } + delete handler; +} + +void +CEventQueue::removeHandlers(void* target) +{ + std::vector handlers; + { + CArchMutexLock lock(m_mutex); + CHandlerTable::iterator index = m_handlers.find(target); + if (index != m_handlers.end()) { + // copy to handlers array and clear table for target + CTypeHandlerTable& typeHandlers = index->second; + for (CTypeHandlerTable::iterator index2 = typeHandlers.begin(); + index2 != typeHandlers.end(); ++index2) { + handlers.push_back(index2->second); + } + typeHandlers.clear(); + } + } + + // delete handlers + for (std::vector::iterator index = handlers.begin(); + index != handlers.end(); ++index) { + delete *index; + } +} + +bool +CEventQueue::isEmpty() const +{ + return (m_buffer->isEmpty() && getNextTimerTimeout() != 0.0); +} + +IEventJob* +CEventQueue::getHandler(CEvent::Type type, void* target) const +{ + CArchMutexLock lock(m_mutex); + CHandlerTable::const_iterator index = m_handlers.find(target); + if (index != m_handlers.end()) { + const CTypeHandlerTable& typeHandlers = index->second; + CTypeHandlerTable::const_iterator index2 = typeHandlers.find(type); + if (index2 != typeHandlers.end()) { + return index2->second; + } + } + return NULL; +} + +UInt32 +CEventQueue::saveEvent(const CEvent& event) +{ + // choose id + UInt32 id; + if (!m_oldEventIDs.empty()) { + // reuse an id + id = m_oldEventIDs.back(); + m_oldEventIDs.pop_back(); + } + else { + // make a new id + id = static_cast(m_events.size()); + } + + // save data + m_events[id] = event; + return id; +} + +CEvent +CEventQueue::removeEvent(UInt32 eventID) +{ + // look up id + CEventTable::iterator index = m_events.find(eventID); + if (index == m_events.end()) { + return CEvent(); + } + + // get data + CEvent event = index->second; + m_events.erase(index); + + // save old id for reuse + m_oldEventIDs.push_back(eventID); + + return event; +} + +bool +CEventQueue::hasTimerExpired(CEvent& event) +{ + // return true if there's a timer in the timer priority queue that + // has expired. if returning true then fill in event appropriately + // and reset and reinsert the timer. + if (m_timerQueue.empty()) { + return false; + } + + // get time elapsed since last check + const double time = m_time.getTime(); + m_time.reset(); + + // countdown elapsed time + for (CTimerQueue::iterator index = m_timerQueue.begin(); + index != m_timerQueue.end(); ++index) { + (*index) -= time; + } + + // done if no timers are expired + if (m_timerQueue.top() > 0.0) { + return false; + } + + // remove timer from queue + CTimer timer = m_timerQueue.top(); + m_timerQueue.pop(); + + // prepare event and reset the timer's clock + timer.fillEvent(m_timerEvent); + event = CEvent(CEvent::kTimer, timer.getTarget(), &m_timerEvent); + timer.reset(); + + // reinsert timer into queue if it's not a one-shot + if (!timer.isOneShot()) { + m_timerQueue.push(timer); + } + + return true; +} + +double +CEventQueue::getNextTimerTimeout() const +{ + // return -1 if no timers, 0 if the top timer has expired, otherwise + // the time until the top timer in the timer priority queue will + // expire. + if (m_timerQueue.empty()) { + return -1.0; + } + if (m_timerQueue.top() <= 0.0) { + return 0.0; + } + return m_timerQueue.top(); +} + +CEvent::Type +CEventQueue::getRegisteredType(const CString& name) const +{ + CNameMap::const_iterator found = m_nameMap.find(name); + if (found != m_nameMap.end()) + return found->second; + + return CEvent::kUnknown; +} + + +// +// CEventQueue::CTimer +// + +CEventQueue::CTimer::CTimer(CEventQueueTimer* timer, double timeout, + double initialTime, void* target, bool oneShot) : + m_timer(timer), + m_timeout(timeout), + m_target(target), + m_oneShot(oneShot), + m_time(initialTime) +{ + assert(m_timeout > 0.0); +} + +CEventQueue::CTimer::~CTimer() +{ + // do nothing +} + +void +CEventQueue::CTimer::reset() +{ + m_time = m_timeout; +} + +CEventQueue::CTimer& +CEventQueue::CTimer::operator-=(double dt) +{ + m_time -= dt; + return *this; +} + +CEventQueue::CTimer::operator double() const +{ + return m_time; +} + +bool +CEventQueue::CTimer::isOneShot() const +{ + return m_oneShot; +} + +CEventQueueTimer* +CEventQueue::CTimer::getTimer() const +{ + return m_timer; +} + +void* +CEventQueue::CTimer::getTarget() const +{ + return m_target; +} + +void +CEventQueue::CTimer::fillEvent(CTimerEvent& event) const +{ + event.m_timer = m_timer; + event.m_count = 0; + if (m_time <= 0.0) { + event.m_count = static_cast((m_timeout - m_time) / m_timeout); + } +} + +bool +CEventQueue::CTimer::operator<(const CTimer& t) const +{ + return m_time < t.m_time; +} diff --git a/src/lib/base/CEventQueue.h b/src/lib/base/CEventQueue.h new file mode 100644 index 00000000..f23bd563 --- /dev/null +++ b/src/lib/base/CEventQueue.h @@ -0,0 +1,130 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CEVENTQUEUE_H +#define CEVENTQUEUE_H + +#include "IEventQueue.h" +#include "CEvent.h" +#include "CPriorityQueue.h" +#include "CStopwatch.h" +#include "IArchMultithread.h" +#include "stdmap.h" +#include "stdset.h" + +//! Event queue +/*! +An event queue that implements the platform independent parts and +delegates the platform dependent parts to a subclass. +*/ +class CEventQueue : public IEventQueue { +public: + CEventQueue(); + virtual ~CEventQueue(); + + // IEventQueue overrides + virtual void adoptBuffer(IEventQueueBuffer*); + virtual bool getEvent(CEvent& event, double timeout = -1.0); + virtual bool dispatchEvent(const CEvent& event); + virtual void addEvent(const CEvent& event); + virtual CEventQueueTimer* + newTimer(double duration, void* target); + virtual CEventQueueTimer* + newOneShotTimer(double duration, void* target); + virtual void deleteTimer(CEventQueueTimer*); + virtual void adoptHandler(CEvent::Type type, + void* target, IEventJob* handler); + virtual void removeHandler(CEvent::Type type, void* target); + virtual void removeHandlers(void* target); + virtual CEvent::Type + registerType(const char* name); + virtual CEvent::Type + registerTypeOnce(CEvent::Type& type, const char* name); + virtual bool isEmpty() const; + virtual IEventJob* getHandler(CEvent::Type type, void* target) const; + virtual const char* getTypeName(CEvent::Type type); + virtual CEvent::Type + getRegisteredType(const CString& name) const; + +private: + UInt32 saveEvent(const CEvent& event); + CEvent removeEvent(UInt32 eventID); + bool hasTimerExpired(CEvent& event); + double getNextTimerTimeout() const; + +private: + class CTimer { + public: + CTimer(CEventQueueTimer*, double timeout, double initialTime, + void* target, bool oneShot); + ~CTimer(); + + void reset(); + + CTimer& operator-=(double); + + operator double() const; + + bool isOneShot() const; + CEventQueueTimer* + getTimer() const; + void* getTarget() const; + void fillEvent(CTimerEvent&) const; + + bool operator<(const CTimer&) const; + + private: + CEventQueueTimer* m_timer; + double m_timeout; + void* m_target; + bool m_oneShot; + double m_time; + }; + typedef std::set CTimers; + typedef CPriorityQueue CTimerQueue; + typedef std::map CEventTable; + typedef std::vector CEventIDList; + typedef std::map CTypeMap; + typedef std::map CNameMap; + typedef std::map CTypeHandlerTable; + typedef std::map CHandlerTable; + + CArchMutex m_mutex; + + // registered events + CEvent::Type m_nextType; + CTypeMap m_typeMap; + CNameMap m_nameMap; + + // buffer of events + IEventQueueBuffer* m_buffer; + + // saved events + CEventTable m_events; + CEventIDList m_oldEventIDs; + + // timers + CStopwatch m_time; + CTimers m_timers; + CTimerQueue m_timerQueue; + CTimerEvent m_timerEvent; + + // event handlers + CHandlerTable m_handlers; +}; + +#endif diff --git a/src/lib/base/CFunctionEventJob.cpp b/src/lib/base/CFunctionEventJob.cpp new file mode 100644 index 00000000..5a666d59 --- /dev/null +++ b/src/lib/base/CFunctionEventJob.cpp @@ -0,0 +1,43 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CFunctionEventJob.h" + +// +// CFunctionEventJob +// + +CFunctionEventJob::CFunctionEventJob( + void (*func)(const CEvent&, void*), void* arg) : + m_func(func), + m_arg(arg) +{ + // do nothing +} + +CFunctionEventJob::~CFunctionEventJob() +{ + // do nothing +} + +void +CFunctionEventJob::run(const CEvent& event) +{ + if (m_func != NULL) { + m_func(event, m_arg); + } +} diff --git a/src/lib/base/CFunctionEventJob.h b/src/lib/base/CFunctionEventJob.h new file mode 100644 index 00000000..97de67ec --- /dev/null +++ b/src/lib/base/CFunctionEventJob.h @@ -0,0 +1,41 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CFUNCTIONEVENTJOB_H +#define CFUNCTIONEVENTJOB_H + +#include "IEventJob.h" + +//! Use a function as an event job +/*! +An event job class that invokes a function. +*/ +class CFunctionEventJob : public IEventJob { +public: + //! run() invokes \c func(arg) + CFunctionEventJob(void (*func)(const CEvent&, void*), void* arg = NULL); + virtual ~CFunctionEventJob(); + + // IEventJob overrides + virtual void run(const CEvent&); + +private: + void (*m_func)(const CEvent&, void*); + void* m_arg; +}; + +#endif diff --git a/src/lib/base/CFunctionJob.cpp b/src/lib/base/CFunctionJob.cpp new file mode 100644 index 00000000..d55d4409 --- /dev/null +++ b/src/lib/base/CFunctionJob.cpp @@ -0,0 +1,42 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CFunctionJob.h" + +// +// CFunctionJob +// + +CFunctionJob::CFunctionJob(void (*func)(void*), void* arg) : + m_func(func), + m_arg(arg) +{ + // do nothing +} + +CFunctionJob::~CFunctionJob() +{ + // do nothing +} + +void +CFunctionJob::run() +{ + if (m_func != NULL) { + m_func(m_arg); + } +} diff --git a/src/lib/base/CFunctionJob.h b/src/lib/base/CFunctionJob.h new file mode 100644 index 00000000..c33e9d91 --- /dev/null +++ b/src/lib/base/CFunctionJob.h @@ -0,0 +1,41 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CFUNCTIONJOB_H +#define CFUNCTIONJOB_H + +#include "IJob.h" + +//! Use a function as a job +/*! +A job class that invokes a function. +*/ +class CFunctionJob : public IJob { +public: + //! run() invokes \c func(arg) + CFunctionJob(void (*func)(void*), void* arg = NULL); + virtual ~CFunctionJob(); + + // IJob overrides + virtual void run(); + +private: + void (*m_func)(void*); + void* m_arg; +}; + +#endif diff --git a/src/lib/base/CLog.cpp b/src/lib/base/CLog.cpp new file mode 100644 index 00000000..1b670cc2 --- /dev/null +++ b/src/lib/base/CLog.cpp @@ -0,0 +1,297 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CLog.h" +#include "CString.h" +#include "CStringUtil.h" +#include "LogOutputters.h" +#include "CArch.h" +#include "Version.h" +#include "XArch.h" +#include +#include +#include +#include + +// names of priorities +static const char* g_priority[] = { + "FATAL", + "ERROR", + "WARNING", + "NOTE", + "INFO", + "DEBUG", + "DEBUG1", + "DEBUG2", + "DEBUG3", + "DEBUG4", + "DEBUG5" + }; + +// number of priorities +static const int g_numPriority = (int)(sizeof(g_priority) / sizeof(g_priority[0])); + +// the default priority +#if defined(_DEBUG) || defined(DEBUG) +static const int g_defaultMaxPriority = kDEBUG; +#else +static const int g_defaultMaxPriority = kINFO; +#endif + +// length of longest string in g_priority +static const int g_maxPriorityLength = 7; + +// length of suffix string (": ") +static const int g_prioritySuffixLength = 2; + +// amount of padded required to fill in the priority prefix +static const int g_priorityPad = g_maxPriorityLength + + g_prioritySuffixLength; + + +// +// CLog +// + +CLog* CLog::s_log = NULL; + +CLog::CLog() +{ + // create mutex for multithread safe operation + m_mutex = ARCH->newMutex(); + + // other initalization + m_maxPriority = g_defaultMaxPriority; + m_maxNewlineLength = 0; + insert(new CConsoleLogOutputter); +} + +CLog::~CLog() +{ + // clean up + for (COutputterList::iterator index = m_outputters.begin(); + index != m_outputters.end(); ++index) { + delete *index; + } + for (COutputterList::iterator index = m_alwaysOutputters.begin(); + index != m_alwaysOutputters.end(); ++index) { + delete *index; + } + ARCH->closeMutex(m_mutex); +} + +CLog* +CLog::getInstance() +{ + if (s_log == NULL) + s_log = new CLog(); + + return s_log; +} + +const char* +CLog::getFilterName() const +{ + return getFilterName(getFilter()); +} + +const char* +CLog::getFilterName(int level) const +{ + return g_priority[level]; +} + +void +CLog::print(const char* file, int line, const char* fmt, ...) +{ + // check if fmt begins with a priority argument + ELevel priority = kINFO; + if ((strlen(fmt) > 2) && (fmt[0] == '%' && fmt[1] == 'z')) { + + // 060 in octal is 0 (48 in decimal), so subtracting this converts ascii + // number it a true number. we could use atoi instead, but this is how + // it was done originally. + priority = (ELevel)(fmt[2] - '\060'); + + // move the pointer on past the debug priority char + fmt += 3; + } + + // done if below priority threshold + if (priority > getFilter()) { + return; + } + + // compute prefix padding length + char stack[1024]; + + // compute suffix padding length + int sPad = m_maxNewlineLength; + + // print to buffer, leaving space for a newline at the end and prefix + // at the beginning. + char* buffer = stack; + int len = (int)(sizeof(stack) / sizeof(stack[0])); + while (true) { + // try printing into the buffer + va_list args; + va_start(args, fmt); + int n = ARCH->vsnprintf(buffer, len - sPad, fmt, args); + va_end(args); + + // if the buffer wasn't big enough then make it bigger and try again + if (n < 0 || n > (int)len) { + if (buffer != stack) { + delete[] buffer; + } + len *= 2; + buffer = new char[len]; + } + + // if the buffer was big enough then continue + else { + break; + } + } + + // print the prefix to the buffer. leave space for priority label. + // do not prefix time and file for kPRINT (CLOG_PRINT) + if (priority != kPRINT) { + + char message[2048]; + +#if defined(_DEBUG) || defined(DEBUG) + struct tm *tm; + char tmp[220]; + time_t t; + time(&t); + tm = localtime(&t); + sprintf(tmp, "%04i-%02i-%02iT%02i:%02i:%02i", tm->tm_year + 1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); + sprintf(message, "%s %s: %s\n\t%s,%d", tmp, g_priority[priority], buffer, file, line); +#else + sprintf(message, "%s: %s", g_priority[priority], buffer); +#endif + + output(priority, message); + } else { + output(priority, buffer); + } + + // clean up + if (buffer != stack) { + delete[] buffer; + } +} + +void +CLog::insert(ILogOutputter* outputter, bool alwaysAtHead) +{ + assert(outputter != NULL); + + CArchMutexLock lock(m_mutex); + if (alwaysAtHead) { + m_alwaysOutputters.push_front(outputter); + } + else { + m_outputters.push_front(outputter); + } + + outputter->open(kAppVersion); + + // Issue 41 + // don't show log unless user requests it, as some users find this + // feature irritating (i.e. when they lose network connectivity). + // in windows the log window can be displayed by selecting "show log" + // from the synergy system tray icon. + // if this causes problems for other architectures, then a different + // work around should be attempted. + //outputter->show(false); +} + +void +CLog::remove(ILogOutputter* outputter) +{ + CArchMutexLock lock(m_mutex); + m_outputters.remove(outputter); + m_alwaysOutputters.remove(outputter); +} + +void +CLog::pop_front(bool alwaysAtHead) +{ + CArchMutexLock lock(m_mutex); + COutputterList* list = alwaysAtHead ? &m_alwaysOutputters : &m_outputters; + if (!list->empty()) { + delete list->front(); + list->pop_front(); + } +} + +bool +CLog::setFilter(const char* maxPriority) +{ + if (maxPriority != NULL) { + for (int i = 0; i < g_numPriority; ++i) { + if (strcmp(maxPriority, g_priority[i]) == 0) { + setFilter(i); + return true; + } + } + return false; + } + return true; +} + +void +CLog::setFilter(int maxPriority) +{ + CArchMutexLock lock(m_mutex); + m_maxPriority = maxPriority; +} + +int +CLog::getFilter() const +{ + CArchMutexLock lock(m_mutex); + return m_maxPriority; +} + +void +CLog::output(ELevel priority, char* msg) +{ + assert(priority >= -1 && priority < g_numPriority); + assert(msg != NULL); + if (!msg) return; + + CArchMutexLock lock(m_mutex); + + COutputterList::const_iterator i; + + for (i = m_alwaysOutputters.begin(); i != m_alwaysOutputters.end(); ++i) { + + // write to outputter + (*i)->write(priority, msg); + } + + for (i = m_outputters.begin(); i != m_outputters.end(); ++i) { + + // write to outputter and break out of loop if it returns false + if (!(*i)->write(priority, msg)) { + break; + } + } +} diff --git a/src/lib/base/CLog.h b/src/lib/base/CLog.h new file mode 100644 index 00000000..928d156d --- /dev/null +++ b/src/lib/base/CLog.h @@ -0,0 +1,207 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CLOG_H +#define CLOG_H + +#include "common.h" +#include "IArchMultithread.h" +#include "stdlist.h" +#include +#include "CArch.h" + +#define CLOG (CLog::getInstance()) + +class ILogOutputter; +class CThread; + +//! Logging facility +/*! +The logging class; all console output should go through this class. +It supports multithread safe operation, several message priority levels, +filtering by priority, and output redirection. The macros LOG() and +LOGC() provide convenient access. +*/ +class CLog { +public: + ~CLog(); + + //! @name manipulators + //@{ + + //! Add an outputter to the head of the list + /*! + Inserts an outputter to the head of the outputter list. When the + logger writes a message, it goes to the outputter at the head of + the outputter list. If that outputter's \c write() method returns + true then it also goes to the next outputter, as so on until an + outputter returns false or there are no more outputters. Outputters + still in the outputter list when the log is destroyed will be + deleted. If \c alwaysAtHead is true then the outputter is always + called before all outputters with \c alwaysAtHead false and the + return value of the outputter is ignored. + + By default, the logger has one outputter installed which writes to + the console. + */ + void insert(ILogOutputter* adopted, + bool alwaysAtHead = false); + + //! Remove an outputter from the list + /*! + Removes the first occurrence of the given outputter from the + outputter list. It does nothing if the outputter is not in the + list. The outputter is not deleted. + */ + void remove(ILogOutputter* orphaned); + + //! Remove the outputter from the head of the list + /*! + Removes and deletes the outputter at the head of the outputter list. + This does nothing if the outputter list is empty. Only removes + outputters that were inserted with the matching \c alwaysAtHead. + */ + void pop_front(bool alwaysAtHead = false); + + //! Set the minimum priority filter. + /*! + Set the filter. Messages below this priority are discarded. + The default priority is 4 (INFO) (unless built without NDEBUG + in which case it's 5 (DEBUG)). setFilter(const char*) returns + true if the priority \c name was recognized; if \c name is NULL + then it simply returns true. + */ + bool setFilter(const char* name); + void setFilter(int); + + //@} + //! @name accessors + //@{ + + //! Print a log message + /*! + Print a log message using the printf-like \c format and arguments + preceded by the filename and line number. If \c file is NULL then + neither the file nor the line are printed. + */ + void print(const char* file, int line, + const char* format, ...); + + //! Get the minimum priority level. + int getFilter() const; + + //! Get the filter name of the current filter level. + const char* getFilterName() const; + + //! Get the filter name of a specified filter level. + const char* getFilterName(int level) const; + + //! Get the singleton instance of the log + static CLog* getInstance(); + + //! Get the console filter level (messages above this are not sent to console). + int getConsoleMaxLevel() const { return kDEBUG1; } + //@} + +private: + CLog(); + void output(ELevel priority, char* msg); + +private: + typedef std::list COutputterList; + + static CLog* s_log; + + CArchMutex m_mutex; + COutputterList m_outputters; + COutputterList m_alwaysOutputters; + int m_maxNewlineLength; + int m_maxPriority; +}; + +/*! +\def LOG(arg) +Write to the log. Because macros cannot accept variable arguments, this +should be invoked like so: +\code +LOG((CLOG_XXX "%d and %d are %s", x, y, x == y ? "equal" : "not equal")); +\endcode +In particular, notice the double open and close parentheses. Also note +that there is no comma after the \c CLOG_XXX. The \c XXX should be +replaced by one of enumerants in \c CLog::ELevel without the leading +\c k. For example, \c CLOG_INFO. The special \c CLOG_PRINT level will +not be filtered and is never prefixed by the filename and line number. + +If \c NOLOGGING is defined during the build then this macro expands to +nothing. If \c NDEBUG is defined during the build then it expands to a +call to CLog::print. Otherwise it expands to a call to CLog::printt, +which includes the filename and line number. +*/ + +/*! +\def LOGC(expr, arg) +Write to the log if and only if expr is true. Because macros cannot accept +variable arguments, this should be invoked like so: +\code +LOGC(x == y, (CLOG_XXX "%d and %d are equal", x, y)); +\endcode +In particular, notice the parentheses around everything after the boolean +expression. Also note that there is no comma after the \c CLOG_XXX. +The \c XXX should be replaced by one of enumerants in \c CLog::ELevel +without the leading \c k. For example, \c CLOG_INFO. The special +\c CLOG_PRINT level will not be filtered and is never prefixed by the +filename and line number. + +If \c NOLOGGING is defined during the build then this macro expands to +nothing. If \c NDEBUG is not defined during the build then it expands +to a call to CLog::print that prints the filename and line number, +otherwise it expands to a call that doesn't. +*/ + +#if defined(NOLOGGING) +#define LOG(_a1) +#define LOGC(_a1, _a2) +#define CLOG_TRACE +#elif defined(NDEBUG) +#define LOG(_a1) CLOG->print _a1 +#define LOGC(_a1, _a2) if (_a1) CLOG->print _a2 +#define CLOG_TRACE NULL, 0, +#else +#define LOG(_a1) CLOG->print _a1 +#define LOGC(_a1, _a2) if (_a1) CLOG->print _a2 +#define CLOG_TRACE __FILE__, __LINE__, +#endif + +// the CLOG_* defines are line and file plus %z and an octal number (060=0, +// 071=9), but the limitation is that once we run out of numbers at either +// end, then we resort to using non-numerical chars. this still works (since +// to deduce the number we subtract octal \060, so '/' is -1, and ':' is 10 + +#define CLOG_PRINT CLOG_TRACE "%z\057" // char is '/' +#define CLOG_CRIT CLOG_TRACE "%z\060" // char is '0' +#define CLOG_ERR CLOG_TRACE "%z\061" +#define CLOG_WARN CLOG_TRACE "%z\062" +#define CLOG_NOTE CLOG_TRACE "%z\063" +#define CLOG_INFO CLOG_TRACE "%z\064" +#define CLOG_DEBUG CLOG_TRACE "%z\065" +#define CLOG_DEBUG1 CLOG_TRACE "%z\066" +#define CLOG_DEBUG2 CLOG_TRACE "%z\067" +#define CLOG_DEBUG3 CLOG_TRACE "%z\070" +#define CLOG_DEBUG4 CLOG_TRACE "%z\071" // char is '9' +#define CLOG_DEBUG5 CLOG_TRACE "%z\072" // char is ':' + +#endif diff --git a/src/lib/base/CMakeLists.txt b/src/lib/base/CMakeLists.txt new file mode 100644 index 00000000..2e623587 --- /dev/null +++ b/src/lib/base/CMakeLists.txt @@ -0,0 +1,78 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(inc + CEvent.h + CEventQueue.h + CFunctionEventJob.h + CFunctionJob.h + CLog.h + CPriorityQueue.h + CSimpleEventQueueBuffer.h + CStopwatch.h + CString.h + CStringUtil.h + CUnicode.h + IEventJob.h + IEventQueue.h + IEventQueueBuffer.h + IJob.h + ILogOutputter.h + LogOutputters.h + TMethodEventJob.h + TMethodJob.h + XBase.h + ELevel.h +) + +set(src + CEvent.cpp + CEventQueue.cpp + CFunctionEventJob.cpp + CFunctionJob.cpp + CLog.cpp + CSimpleEventQueueBuffer.cpp + CStopwatch.cpp + CStringUtil.cpp + CUnicode.cpp + IEventQueue.cpp + LogOutputters.cpp + XBase.cpp +) + +if (WIN32) + list(APPEND src ${inc}) +endif() + +set(inc + ../arch + ../common + ../mt + ../synergy +) + +if (UNIX) + list(APPEND inc + ../../.. + ../base + ) +endif() + +include_directories(${inc}) +add_library(base STATIC ${src}) + +if (UNIX) + target_link_libraries(base common) +endif() diff --git a/src/lib/base/CPriorityQueue.h b/src/lib/base/CPriorityQueue.h new file mode 100644 index 00000000..02d72c33 --- /dev/null +++ b/src/lib/base/CPriorityQueue.h @@ -0,0 +1,139 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CPRIORITYQUEUE_H +#define CPRIORITYQUEUE_H + +#include "stdvector.h" +#include +#include + +//! A priority queue with an iterator +/*! +This priority queue is the same as a standard priority queue except: +it sorts by std::greater, it has a forward iterator through the elements +(which can appear in any order), and its contents can be swapped. +*/ +template , +#if defined(_MSC_VER) + class Compare = std::greater > +#else + class Compare = std::greater > +#endif +class CPriorityQueue { +public: + typedef typename Container::value_type value_type; + typedef typename Container::size_type size_type; + typedef typename Container::iterator iterator; + typedef typename Container::const_iterator const_iterator; + typedef Container container_type; + + CPriorityQueue() { } + CPriorityQueue(Container& swappedIn) { swap(swappedIn); } + ~CPriorityQueue() { } + + //! @name manipulators + //@{ + + //! Add element + void push(const value_type& v) + { + c.push_back(v); + std::push_heap(c.begin(), c.end(), comp); + } + + //! Remove head element + void pop() + { + std::pop_heap(c.begin(), c.end(), comp); + c.pop_back(); + } + + //! Erase element + void erase(iterator i) + { + c.erase(i); + std::make_heap(c.begin(), c.end(), comp); + } + + //! Get start iterator + iterator begin() + { + return c.begin(); + } + + //! Get end iterator + iterator end() + { + return c.end(); + } + + //! Swap contents with another priority queue + void swap(CPriorityQueue& q) + { + c.swap(q.c); + } + + //! Swap contents with another container + void swap(Container& c2) + { + c.swap(c2); + std::make_heap(c.begin(), c.end(), comp); + } + + //@} + //! @name accessors + //@{ + + //! Returns true if there are no elements + bool empty() const + { + return c.empty(); + } + + //! Returns the number of elements + size_type size() const + { + return c.size(); + } + + //! Returns the head element + const value_type& top() const + { + return c.front(); + } + + //! Get start iterator + const_iterator begin() const + { + return c.begin(); + } + + //! Get end iterator + const_iterator end() const + { + return c.end(); + } + + //@} + +private: + Container c; + Compare comp; +}; + +#endif diff --git a/src/lib/base/CSimpleEventQueueBuffer.cpp b/src/lib/base/CSimpleEventQueueBuffer.cpp new file mode 100644 index 00000000..a8ca65d2 --- /dev/null +++ b/src/lib/base/CSimpleEventQueueBuffer.cpp @@ -0,0 +1,100 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CSimpleEventQueueBuffer.h" +#include "CStopwatch.h" +#include "CArch.h" + +class CEventQueueTimer { }; + +// +// CSimpleEventQueueBuffer +// + +CSimpleEventQueueBuffer::CSimpleEventQueueBuffer() +{ + m_queueMutex = ARCH->newMutex(); + m_queueReadyCond = ARCH->newCondVar(); + m_queueReady = false; +} + +CSimpleEventQueueBuffer::~CSimpleEventQueueBuffer() +{ + ARCH->closeCondVar(m_queueReadyCond); + ARCH->closeMutex(m_queueMutex); +} + +void +CSimpleEventQueueBuffer::waitForEvent(double timeout) +{ + CArchMutexLock lock(m_queueMutex); + CStopwatch timer(true); + while (!m_queueReady) { + double timeLeft = timeout; + if (timeLeft >= 0.0) { + timeLeft -= timer.getTime(); + if (timeLeft < 0.0) { + return; + } + } + ARCH->waitCondVar(m_queueReadyCond, m_queueMutex, timeLeft); + } +} + +IEventQueueBuffer::Type +CSimpleEventQueueBuffer::getEvent(CEvent&, UInt32& dataID) +{ + CArchMutexLock lock(m_queueMutex); + if (!m_queueReady) { + return kNone; + } + dataID = m_queue.back(); + m_queue.pop_back(); + m_queueReady = !m_queue.empty(); + return kUser; +} + +bool +CSimpleEventQueueBuffer::addEvent(UInt32 dataID) +{ + CArchMutexLock lock(m_queueMutex); + m_queue.push_front(dataID); + if (!m_queueReady) { + m_queueReady = true; + ARCH->broadcastCondVar(m_queueReadyCond); + } + return true; +} + +bool +CSimpleEventQueueBuffer::isEmpty() const +{ + CArchMutexLock lock(m_queueMutex); + return !m_queueReady; +} + +CEventQueueTimer* +CSimpleEventQueueBuffer::newTimer(double, bool) const +{ + return new CEventQueueTimer; +} + +void +CSimpleEventQueueBuffer::deleteTimer(CEventQueueTimer* timer) const +{ + delete timer; +} diff --git a/src/lib/base/CSimpleEventQueueBuffer.h b/src/lib/base/CSimpleEventQueueBuffer.h new file mode 100644 index 00000000..673f136b --- /dev/null +++ b/src/lib/base/CSimpleEventQueueBuffer.h @@ -0,0 +1,52 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CSIMPLEEVENTQUEUEBUFFER_H +#define CSIMPLEEVENTQUEUEBUFFER_H + +#include "IEventQueueBuffer.h" +#include "IArchMultithread.h" +#include "stddeque.h" + +//! In-memory event queue buffer +/*! +An event queue buffer provides a queue of events for an IEventQueue. +*/ +class CSimpleEventQueueBuffer : public IEventQueueBuffer { +public: + CSimpleEventQueueBuffer(); + ~CSimpleEventQueueBuffer(); + + // IEventQueueBuffer overrides + virtual void waitForEvent(double timeout); + virtual Type getEvent(CEvent& event, UInt32& dataID); + virtual bool addEvent(UInt32 dataID); + virtual bool isEmpty() const; + virtual CEventQueueTimer* + newTimer(double duration, bool oneShot) const; + virtual void deleteTimer(CEventQueueTimer*) const; + +private: + typedef std::deque CEventDeque; + + CArchMutex m_queueMutex; + CArchCond m_queueReadyCond; + bool m_queueReady; + CEventDeque m_queue; +}; + +#endif diff --git a/src/lib/base/CStopwatch.cpp b/src/lib/base/CStopwatch.cpp new file mode 100644 index 00000000..af43d7e8 --- /dev/null +++ b/src/lib/base/CStopwatch.cpp @@ -0,0 +1,129 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CStopwatch.h" +#include "CArch.h" + +// +// CStopwatch +// + +CStopwatch::CStopwatch(bool triggered) : + m_mark(0.0), + m_triggered(triggered), + m_stopped(triggered) +{ + if (!triggered) { + m_mark = ARCH->time(); + } +} + +CStopwatch::~CStopwatch() +{ + // do nothing +} + +double +CStopwatch::reset() +{ + if (m_stopped) { + const double dt = m_mark; + m_mark = 0.0; + return dt; + } + else { + const double t = ARCH->time(); + const double dt = t - m_mark; + m_mark = t; + return dt; + } +} + +void +CStopwatch::stop() +{ + if (m_stopped) { + return; + } + + // save the elapsed time + m_mark = ARCH->time() - m_mark; + m_stopped = true; +} + +void +CStopwatch::start() +{ + m_triggered = false; + if (!m_stopped) { + return; + } + + // set the mark such that it reports the time elapsed at stop() + m_mark = ARCH->time() - m_mark; + m_stopped = false; +} + +void +CStopwatch::setTrigger() +{ + stop(); + m_triggered = true; +} + +double +CStopwatch::getTime() +{ + if (m_triggered) { + const double dt = m_mark; + start(); + return dt; + } + else if (m_stopped) { + return m_mark; + } + else { + return ARCH->time() - m_mark; + } +} + +CStopwatch::operator double() +{ + return getTime(); +} + +bool +CStopwatch::isStopped() const +{ + return m_stopped; +} + +double +CStopwatch::getTime() const +{ + if (m_stopped) { + return m_mark; + } + else { + return ARCH->time() - m_mark; + } +} + +CStopwatch::operator double() const +{ + return getTime(); +} diff --git a/src/lib/base/CStopwatch.h b/src/lib/base/CStopwatch.h new file mode 100644 index 00000000..4ad49c5e --- /dev/null +++ b/src/lib/base/CStopwatch.h @@ -0,0 +1,111 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CSTOPWATCH_H +#define CSTOPWATCH_H + +#include "common.h" + +//! A timer class +/*! +This class measures time intervals. All time interval measurement +should use this class. +*/ +class CStopwatch { +public: + /*! + The default constructor does an implicit reset() or setTrigger(). + If triggered == false then the clock starts ticking. + */ + CStopwatch(bool triggered = false); + ~CStopwatch(); + + //! @name manipulators + //@{ + + //! Reset the timer to zero + /*! + Set the start time to the current time, returning the time since + the last reset. This does not remove the trigger if it's set nor + does it start a stopped clock. If the clock is stopped then + subsequent reset()'s will return 0. + */ + double reset(); + + //! Stop the timer + /*! + Stop the stopwatch. The time interval while stopped is not + counted by the stopwatch. stop() does not remove the trigger. + Has no effect if already stopped. + */ + void stop(); + + //! Start the timer + /*! + Start the stopwatch. start() removes the trigger, even if the + stopwatch was already started. + */ + void start(); + + //! Stop the timer and set the trigger + /*! + setTrigger() stops the clock like stop() except there's an + implicit start() the next time (non-const) getTime() is called. + This is useful when you want the clock to start the first time + you check it. + */ + void setTrigger(); + + //! Get elapsed time + /*! + Returns the time since the last reset() (or calls reset() and + returns zero if the trigger is set). + */ + double getTime(); + //! Same as getTime() + operator double(); + //@} + //! @name accessors + //@{ + + //! Check if timer is stopped + /*! + Returns true if the stopwatch is stopped. + */ + bool isStopped() const; + + // return the time since the last reset(). + //! Get elapsed time + /*! + Returns the time since the last reset(). This cannot trigger the + stopwatch to start and will not clear the trigger. + */ + double getTime() const; + //! Same as getTime() const + operator double() const; + //@} + +private: + double getClock() const; + +private: + double m_mark; + bool m_triggered; + bool m_stopped; +}; + +#endif diff --git a/src/lib/base/CString.h b/src/lib/base/CString.h new file mode 100644 index 00000000..630508c7 --- /dev/null +++ b/src/lib/base/CString.h @@ -0,0 +1,28 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CSTRING_H +#define CSTRING_H + +#include "common.h" +#include "stdstring.h" + +// use standard C++ string class for our string class +typedef std::string CString; + +#endif + diff --git a/src/lib/base/CStringUtil.cpp b/src/lib/base/CStringUtil.cpp new file mode 100644 index 00000000..8033fe78 --- /dev/null +++ b/src/lib/base/CStringUtil.cpp @@ -0,0 +1,198 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CStringUtil.h" +#include "CArch.h" +#include "common.h" +#include "stdvector.h" +#include +#include +#include +#include +#include + +// +// CStringUtil +// + +CString +CStringUtil::format(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + CString result = vformat(fmt, args); + va_end(args); + return result; +} + +CString +CStringUtil::vformat(const char* fmt, va_list args) +{ + // find highest indexed substitution and the locations of substitutions + std::vector pos; + std::vector width; + std::vector index; + int maxIndex = 0; + for (const char* scan = fmt; *scan != '\0'; ++scan) { + if (*scan == '%') { + ++scan; + if (*scan == '\0') { + break; + } + else if (*scan == '%') { + // literal + index.push_back(0); + pos.push_back(static_cast(scan - 1 - fmt)); + width.push_back(2); + } + else if (*scan == '{') { + // get argument index + char* end; + int i = static_cast(strtol(scan + 1, &end, 10)); + if (*end != '}') { + // invalid index -- ignore + scan = end - 1; + } + else { + index.push_back(i); + pos.push_back(static_cast(scan - 1 - fmt)); + width.push_back(static_cast(end - scan + 2)); + if (i > maxIndex) { + maxIndex = i; + } + scan = end; + } + } + else { + // improper escape -- ignore + } + } + } + + // get args + std::vector value; + std::vector length; + value.push_back("%"); + length.push_back(1); + for (int i = 0; i < maxIndex; ++i) { + const char* arg = va_arg(args, const char*); + size_t len = strlen(arg); + value.push_back(arg); + length.push_back(len); + } + + // compute final length + size_t resultLength = strlen(fmt); + const int n = static_cast(pos.size()); + for (int i = 0; i < n; ++i) { + resultLength -= width[i]; + resultLength += length[index[i]]; + } + + // substitute + CString result; + result.reserve(resultLength); + size_t src = 0; + for (int i = 0; i < n; ++i) { + result.append(fmt + src, pos[i] - src); + result.append(value[index[i]]); + src = pos[i] + width[i]; + } + result.append(fmt + src); + + return result; +} + +CString +CStringUtil::print(const char* fmt, ...) +{ + char tmp[1024]; + char* buffer = tmp; + int len = (int)(sizeof(tmp) / sizeof(tmp[0])); + CString result; + while (buffer != NULL) { + // try printing into the buffer + va_list args; + va_start(args, fmt); + int n = ARCH->vsnprintf(buffer, len, fmt, args); + va_end(args); + + // if the buffer wasn't big enough then make it bigger and try again + if (n < 0 || n > len) { + if (buffer != tmp) { + delete[] buffer; + } + len *= 2; + buffer = new char[len]; + } + + // if it was big enough then save the string and don't try again + else { + result = buffer; + if (buffer != tmp) { + delete[] buffer; + } + buffer = NULL; + } + } + + return result; +} + + +// +// CStringUtil::CaselessCmp +// + +bool +CStringUtil::CaselessCmp::cmpEqual( + const CString::value_type& a, + const CString::value_type& b) +{ + // should use std::tolower but not in all versions of libstdc++ have it + return tolower(a) == tolower(b); +} + +bool +CStringUtil::CaselessCmp::cmpLess( + const CString::value_type& a, + const CString::value_type& b) +{ + // should use std::tolower but not in all versions of libstdc++ have it + return tolower(a) < tolower(b); +} + +bool +CStringUtil::CaselessCmp::less(const CString& a, const CString& b) +{ + return std::lexicographical_compare( + a.begin(), a.end(), + b.begin(), b.end(), + &CStringUtil::CaselessCmp::cmpLess); +} + +bool +CStringUtil::CaselessCmp::equal(const CString& a, const CString& b) +{ + return !(less(a, b) || less(b, a)); +} + +bool +CStringUtil::CaselessCmp::operator()(const CString& a, const CString& b) const +{ + return less(a, b); +} diff --git a/src/lib/base/CStringUtil.h b/src/lib/base/CStringUtil.h new file mode 100644 index 00000000..408f5a91 --- /dev/null +++ b/src/lib/base/CStringUtil.h @@ -0,0 +1,80 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CSTRINGUTIL_H +#define CSTRINGUTIL_H + +#include "CString.h" +#include + +//! String utilities +/*! +This class provides various functions for string manipulation. +*/ +class CStringUtil { +public: + //! Format positional arguments + /*! + Format a string using positional arguments. fmt has literal + characters and conversion specifications introduced by `\%': + - \%\% -- literal `\%' + - \%{n} -- positional element n, n a positive integer, {} are literal + + All arguments in the variable list are const char*. Positional + elements are indexed from 1. + */ + static CString format(const char* fmt, ...); + + //! Format positional arguments + /*! + Same as format() except takes va_list. + */ + static CString vformat(const char* fmt, va_list); + + //! Print a string using printf-style formatting + /*! + Equivalent to printf() except the result is returned as a CString. + */ + static CString print(const char* fmt, ...); + + //! Case-insensitive comparisons + /*! + This class provides case-insensitve comparison functions. + */ + class CaselessCmp { + public: + //! Same as less() + bool operator()(const CString& a, const CString& b) const; + + //! Returns true iff \c a is lexicographically less than \c b + static bool less(const CString& a, const CString& b); + + //! Returns true iff \c a is lexicographically equal to \c b + static bool equal(const CString& a, const CString& b); + + //! Returns true iff \c a is lexicographically less than \c b + static bool cmpLess(const CString::value_type& a, + const CString::value_type& b); + + //! Returns true iff \c a is lexicographically equal to \c b + static bool cmpEqual(const CString::value_type& a, + const CString::value_type& b); + }; +}; + +#endif + diff --git a/src/lib/base/CUnicode.cpp b/src/lib/base/CUnicode.cpp new file mode 100644 index 00000000..894b3f9f --- /dev/null +++ b/src/lib/base/CUnicode.cpp @@ -0,0 +1,782 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CUnicode.h" +#include "CArch.h" +#include + +// +// local utility functions +// + +inline +static +UInt16 +decode16(const UInt8* n, bool byteSwapped) +{ + union x16 { + UInt8 n8[2]; + UInt16 n16; + } c; + if (byteSwapped) { + c.n8[0] = n[1]; + c.n8[1] = n[0]; + } + else { + c.n8[0] = n[0]; + c.n8[1] = n[1]; + } + return c.n16; +} + +inline +static +UInt32 +decode32(const UInt8* n, bool byteSwapped) +{ + union x32 { + UInt8 n8[4]; + UInt32 n32; + } c; + if (byteSwapped) { + c.n8[0] = n[3]; + c.n8[1] = n[2]; + c.n8[2] = n[1]; + c.n8[3] = n[0]; + } + else { + c.n8[0] = n[0]; + c.n8[1] = n[1]; + c.n8[2] = n[2]; + c.n8[3] = n[3]; + } + return c.n32; +} + +inline +static +void +resetError(bool* errors) +{ + if (errors != NULL) { + *errors = false; + } +} + +inline +static +void +setError(bool* errors) +{ + if (errors != NULL) { + *errors = true; + } +} + + +// +// CUnicode +// + +UInt32 CUnicode::s_invalid = 0x0000ffff; +UInt32 CUnicode::s_replacement = 0x0000fffd; + +bool +CUnicode::isUTF8(const CString& src) +{ + // convert and test each character + const UInt8* data = reinterpret_cast(src.c_str()); + for (UInt32 n = (UInt32)src.size(); n > 0; ) { + if (fromUTF8(data, n) == s_invalid) { + return false; + } + } + return true; +} + +CString +CUnicode::UTF8ToUCS2(const CString& src, bool* errors) +{ + // default to success + resetError(errors); + + // get size of input string and reserve some space in output + UInt32 n = (UInt32)src.size(); + CString dst; + dst.reserve(2 * n); + + // convert each character + const UInt8* data = reinterpret_cast(src.c_str()); + while (n > 0) { + UInt32 c = fromUTF8(data, n); + if (c == s_invalid) { + c = s_replacement; + } + else if (c >= 0x00010000) { + setError(errors); + c = s_replacement; + } + UInt16 ucs2 = static_cast(c); + dst.append(reinterpret_cast(&ucs2), 2); + } + + return dst; +} + +CString +CUnicode::UTF8ToUCS4(const CString& src, bool* errors) +{ + // default to success + resetError(errors); + + // get size of input string and reserve some space in output + UInt32 n = (UInt32)src.size(); + CString dst; + dst.reserve(4 * n); + + // convert each character + const UInt8* data = reinterpret_cast(src.c_str()); + while (n > 0) { + UInt32 c = fromUTF8(data, n); + if (c == s_invalid) { + c = s_replacement; + } + dst.append(reinterpret_cast(&c), 4); + } + + return dst; +} + +CString +CUnicode::UTF8ToUTF16(const CString& src, bool* errors) +{ + // default to success + resetError(errors); + + // get size of input string and reserve some space in output + UInt32 n = (UInt32)src.size(); + CString dst; + dst.reserve(2 * n); + + // convert each character + const UInt8* data = reinterpret_cast(src.c_str()); + while (n > 0) { + UInt32 c = fromUTF8(data, n); + if (c == s_invalid) { + c = s_replacement; + } + else if (c >= 0x00110000) { + setError(errors); + c = s_replacement; + } + if (c < 0x00010000) { + UInt16 ucs2 = static_cast(c); + dst.append(reinterpret_cast(&ucs2), 2); + } + else { + c -= 0x00010000; + UInt16 utf16h = static_cast((c >> 10) + 0xd800); + UInt16 utf16l = static_cast((c & 0x03ff) + 0xdc00); + dst.append(reinterpret_cast(&utf16h), 2); + dst.append(reinterpret_cast(&utf16l), 2); + } + } + + return dst; +} + +CString +CUnicode::UTF8ToUTF32(const CString& src, bool* errors) +{ + // default to success + resetError(errors); + + // get size of input string and reserve some space in output + UInt32 n = (UInt32)src.size(); + CString dst; + dst.reserve(4 * n); + + // convert each character + const UInt8* data = reinterpret_cast(src.c_str()); + while (n > 0) { + UInt32 c = fromUTF8(data, n); + if (c == s_invalid) { + c = s_replacement; + } + else if (c >= 0x00110000) { + setError(errors); + c = s_replacement; + } + dst.append(reinterpret_cast(&c), 4); + } + + return dst; +} + +CString +CUnicode::UTF8ToText(const CString& src, bool* errors) +{ + // default to success + resetError(errors); + + // convert to wide char + UInt32 size; + wchar_t* tmp = UTF8ToWideChar(src, size, errors); + + // convert string to multibyte + int len = ARCH->convStringWCToMB(NULL, tmp, size, errors); + char* mbs = new char[len + 1]; + ARCH->convStringWCToMB(mbs, tmp, size, errors); + CString text(mbs, len); + + // clean up + delete[] mbs; + delete[] tmp; + + return text; +} + +CString +CUnicode::UCS2ToUTF8(const CString& src, bool* errors) +{ + // default to success + resetError(errors); + + // convert + UInt32 n = (UInt32)src.size() >> 1; + return doUCS2ToUTF8(reinterpret_cast(src.data()), n, errors); +} + +CString +CUnicode::UCS4ToUTF8(const CString& src, bool* errors) +{ + // default to success + resetError(errors); + + // convert + UInt32 n = (UInt32)src.size() >> 2; + return doUCS4ToUTF8(reinterpret_cast(src.data()), n, errors); +} + +CString +CUnicode::UTF16ToUTF8(const CString& src, bool* errors) +{ + // default to success + resetError(errors); + + // convert + UInt32 n = (UInt32)src.size() >> 1; + return doUTF16ToUTF8(reinterpret_cast(src.data()), n, errors); +} + +CString +CUnicode::UTF32ToUTF8(const CString& src, bool* errors) +{ + // default to success + resetError(errors); + + // convert + UInt32 n = (UInt32)src.size() >> 2; + return doUTF32ToUTF8(reinterpret_cast(src.data()), n, errors); +} + +CString +CUnicode::textToUTF8(const CString& src, bool* errors) +{ + // default to success + resetError(errors); + + // convert string to wide characters + UInt32 n = (UInt32)src.size(); + int len = ARCH->convStringMBToWC(NULL, src.c_str(), n, errors); + wchar_t* wcs = new wchar_t[len + 1]; + ARCH->convStringMBToWC(wcs, src.c_str(), n, errors); + + // convert to UTF8 + CString utf8 = wideCharToUTF8(wcs, len, errors); + + // clean up + delete[] wcs; + + return utf8; +} + +wchar_t* +CUnicode::UTF8ToWideChar(const CString& src, UInt32& size, bool* errors) +{ + // convert to platform's wide character encoding + CString tmp; + switch (ARCH->getWideCharEncoding()) { + case IArchString::kUCS2: + tmp = UTF8ToUCS2(src, errors); + size = (UInt32)tmp.size() >> 1; + break; + + case IArchString::kUCS4: + tmp = UTF8ToUCS4(src, errors); + size = (UInt32)tmp.size() >> 2; + break; + + case IArchString::kUTF16: + tmp = UTF8ToUTF16(src, errors); + size = (UInt32)tmp.size() >> 1; + break; + + case IArchString::kUTF32: + tmp = UTF8ToUTF32(src, errors); + size = (UInt32)tmp.size() >> 2; + break; + + default: + assert(0 && "unknown wide character encoding"); + } + + // copy to a wchar_t array + wchar_t* dst = new wchar_t[size]; + ::memcpy(dst, tmp.data(), sizeof(wchar_t) * size); + return dst; +} + +CString +CUnicode::wideCharToUTF8(const wchar_t* src, UInt32 size, bool* errors) +{ + // convert from platform's wide character encoding. + // note -- this must include a wide nul character (independent of + // the CString's nul character). + switch (ARCH->getWideCharEncoding()) { + case IArchString::kUCS2: + return doUCS2ToUTF8(reinterpret_cast(src), size, errors); + + case IArchString::kUCS4: + return doUCS4ToUTF8(reinterpret_cast(src), size, errors); + + case IArchString::kUTF16: + return doUTF16ToUTF8(reinterpret_cast(src), size, errors); + + case IArchString::kUTF32: + return doUTF32ToUTF8(reinterpret_cast(src), size, errors); + + default: + assert(0 && "unknown wide character encoding"); + return CString(); + } +} + +CString +CUnicode::doUCS2ToUTF8(const UInt8* data, UInt32 n, bool* errors) +{ + // make some space + CString dst; + dst.reserve(n); + + // check if first character is 0xfffe or 0xfeff + bool byteSwapped = false; + if (n >= 1) { + switch (decode16(data, false)) { + case 0x0000feff: + data += 2; + --n; + break; + + case 0x0000fffe: + byteSwapped = true; + data += 2; + --n; + break; + + default: + break; + } + } + + // convert each character + for (; n > 0; data += 2, --n) { + UInt32 c = decode16(data, byteSwapped); + toUTF8(dst, c, errors); + } + + return dst; +} + +CString +CUnicode::doUCS4ToUTF8(const UInt8* data, UInt32 n, bool* errors) +{ + // make some space + CString dst; + dst.reserve(n); + + // check if first character is 0xfffe or 0xfeff + bool byteSwapped = false; + if (n >= 1) { + switch (decode32(data, false)) { + case 0x0000feff: + data += 4; + --n; + break; + + case 0x0000fffe: + byteSwapped = true; + data += 4; + --n; + break; + + default: + break; + } + } + + // convert each character + for (; n > 0; data += 4, --n) { + UInt32 c = decode32(data, byteSwapped); + toUTF8(dst, c, errors); + } + + return dst; +} + +CString +CUnicode::doUTF16ToUTF8(const UInt8* data, UInt32 n, bool* errors) +{ + // make some space + CString dst; + dst.reserve(n); + + // check if first character is 0xfffe or 0xfeff + bool byteSwapped = false; + if (n >= 1) { + switch (decode16(data, false)) { + case 0x0000feff: + data += 2; + --n; + break; + + case 0x0000fffe: + byteSwapped = true; + data += 2; + --n; + break; + + default: + break; + } + } + + // convert each character + for (; n > 0; data += 2, --n) { + UInt32 c = decode16(data, byteSwapped); + if (c < 0x0000d800 || c > 0x0000dfff) { + toUTF8(dst, c, errors); + } + else if (n == 1) { + // error -- missing second word + setError(errors); + toUTF8(dst, s_replacement, NULL); + } + else if (c >= 0x0000d800 && c <= 0x0000dbff) { + UInt32 c2 = decode16(data, byteSwapped); + data += 2; + --n; + if (c2 < 0x0000dc00 || c2 > 0x0000dfff) { + // error -- [d800,dbff] not followed by [dc00,dfff] + setError(errors); + toUTF8(dst, s_replacement, NULL); + } + else { + c = (((c - 0x0000d800) << 10) | (c2 - 0x0000dc00)) + 0x00010000; + toUTF8(dst, c, errors); + } + } + else { + // error -- [dc00,dfff] without leading [d800,dbff] + setError(errors); + toUTF8(dst, s_replacement, NULL); + } + } + + return dst; +} + +CString +CUnicode::doUTF32ToUTF8(const UInt8* data, UInt32 n, bool* errors) +{ + // make some space + CString dst; + dst.reserve(n); + + // check if first character is 0xfffe or 0xfeff + bool byteSwapped = false; + if (n >= 1) { + switch (decode32(data, false)) { + case 0x0000feff: + data += 4; + --n; + break; + + case 0x0000fffe: + byteSwapped = true; + data += 4; + --n; + break; + + default: + break; + } + } + + // convert each character + for (; n > 0; data += 4, --n) { + UInt32 c = decode32(data, byteSwapped); + if (c >= 0x00110000) { + setError(errors); + c = s_replacement; + } + toUTF8(dst, c, errors); + } + + return dst; +} + +UInt32 +CUnicode::fromUTF8(const UInt8*& data, UInt32& n) +{ + assert(data != NULL); + assert(n != 0); + + // compute character encoding length, checking for overlong + // sequences (i.e. characters that don't use the shortest + // possible encoding). + UInt32 size; + if (data[0] < 0x80) { + // 0xxxxxxx + size = 1; + } + else if (data[0] < 0xc0) { + // 10xxxxxx -- in the middle of a multibyte character. counts + // as one invalid character. + --n; + ++data; + return s_invalid; + } + else if (data[0] < 0xe0) { + // 110xxxxx + size = 2; + } + else if (data[0] < 0xf0) { + // 1110xxxx + size = 3; + } + else if (data[0] < 0xf8) { + // 11110xxx + size = 4; + } + else if (data[0] < 0xfc) { + // 111110xx + size = 5; + } + else if (data[0] < 0xfe) { + // 1111110x + size = 6; + } + else { + // invalid sequence. dunno how many bytes to skip so skip one. + --n; + ++data; + return s_invalid; + } + + // make sure we have enough data + if (size > n) { + data += n; + n = 0; + return s_invalid; + } + + // extract character + UInt32 c; + switch (size) { + case 1: + c = static_cast(data[0]); + break; + + case 2: + c = ((static_cast(data[0]) & 0x1f) << 6) | + ((static_cast(data[1]) & 0x3f) ); + break; + + case 3: + c = ((static_cast(data[0]) & 0x0f) << 12) | + ((static_cast(data[1]) & 0x3f) << 6) | + ((static_cast(data[2]) & 0x3f) ); + break; + + case 4: + c = ((static_cast(data[0]) & 0x07) << 18) | + ((static_cast(data[1]) & 0x3f) << 12) | + ((static_cast(data[1]) & 0x3f) << 6) | + ((static_cast(data[1]) & 0x3f) ); + break; + + case 5: + c = ((static_cast(data[0]) & 0x03) << 24) | + ((static_cast(data[1]) & 0x3f) << 18) | + ((static_cast(data[1]) & 0x3f) << 12) | + ((static_cast(data[1]) & 0x3f) << 6) | + ((static_cast(data[1]) & 0x3f) ); + break; + + case 6: + c = ((static_cast(data[0]) & 0x01) << 30) | + ((static_cast(data[1]) & 0x3f) << 24) | + ((static_cast(data[1]) & 0x3f) << 18) | + ((static_cast(data[1]) & 0x3f) << 12) | + ((static_cast(data[1]) & 0x3f) << 6) | + ((static_cast(data[1]) & 0x3f) ); + break; + + default: + assert(0 && "invalid size"); + return s_invalid; + } + + // check that all bytes after the first have the pattern 10xxxxxx. + // truncated sequences are treated as a single malformed character. + bool truncated = false; + switch (size) { + case 6: + if ((data[5] & 0xc0) != 0x80) { + truncated = true; + size = 5; + } + // fall through + + case 5: + if ((data[4] & 0xc0) != 0x80) { + truncated = true; + size = 4; + } + // fall through + + case 4: + if ((data[3] & 0xc0) != 0x80) { + truncated = true; + size = 3; + } + // fall through + + case 3: + if ((data[2] & 0xc0) != 0x80) { + truncated = true; + size = 2; + } + // fall through + + case 2: + if ((data[1] & 0xc0) != 0x80) { + truncated = true; + size = 1; + } + } + + // update parameters + data += size; + n -= size; + + // invalid if sequence was truncated + if (truncated) { + return s_invalid; + } + + // check for characters that didn't use the smallest possible encoding + static UInt32 s_minChar[] = { + 0, + 0x00000000, + 0x00000080, + 0x00000800, + 0x00010000, + 0x00200000, + 0x04000000 + }; + if (c < s_minChar[size]) { + return s_invalid; + } + + // check for characters not in ISO-10646 + if (c >= 0x0000d800 && c <= 0x0000dfff) { + return s_invalid; + } + if (c >= 0x0000fffe && c <= 0x0000ffff) { + return s_invalid; + } + + return c; +} + +void +CUnicode::toUTF8(CString& dst, UInt32 c, bool* errors) +{ + UInt8 data[6]; + + // handle characters outside the valid range + if ((c >= 0x0000d800 && c <= 0x0000dfff) || c >= 0x80000000) { + setError(errors); + c = s_replacement; + } + + // convert to UTF-8 + if (c < 0x00000080) { + data[0] = static_cast(c); + dst.append(reinterpret_cast(data), 1); + } + else if (c < 0x00000800) { + data[0] = static_cast(((c >> 6) & 0x0000001f) + 0xc0); + data[1] = static_cast((c & 0x0000003f) + 0x80); + dst.append(reinterpret_cast(data), 2); + } + else if (c < 0x00010000) { + data[0] = static_cast(((c >> 12) & 0x0000000f) + 0xe0); + data[1] = static_cast(((c >> 6) & 0x0000003f) + 0x80); + data[2] = static_cast((c & 0x0000003f) + 0x80); + dst.append(reinterpret_cast(data), 3); + } + else if (c < 0x00200000) { + data[0] = static_cast(((c >> 18) & 0x00000007) + 0xf0); + data[1] = static_cast(((c >> 12) & 0x0000003f) + 0x80); + data[2] = static_cast(((c >> 6) & 0x0000003f) + 0x80); + data[3] = static_cast((c & 0x0000003f) + 0x80); + dst.append(reinterpret_cast(data), 4); + } + else if (c < 0x04000000) { + data[0] = static_cast(((c >> 24) & 0x00000003) + 0xf8); + data[1] = static_cast(((c >> 18) & 0x0000003f) + 0x80); + data[2] = static_cast(((c >> 12) & 0x0000003f) + 0x80); + data[3] = static_cast(((c >> 6) & 0x0000003f) + 0x80); + data[4] = static_cast((c & 0x0000003f) + 0x80); + dst.append(reinterpret_cast(data), 5); + } + else if (c < 0x80000000) { + data[0] = static_cast(((c >> 30) & 0x00000001) + 0xfc); + data[1] = static_cast(((c >> 24) & 0x0000003f) + 0x80); + data[2] = static_cast(((c >> 18) & 0x0000003f) + 0x80); + data[3] = static_cast(((c >> 12) & 0x0000003f) + 0x80); + data[4] = static_cast(((c >> 6) & 0x0000003f) + 0x80); + data[5] = static_cast((c & 0x0000003f) + 0x80); + dst.append(reinterpret_cast(data), 6); + } + else { + assert(0 && "character out of range"); + } +} diff --git a/src/lib/base/CUnicode.h b/src/lib/base/CUnicode.h new file mode 100644 index 00000000..99621003 --- /dev/null +++ b/src/lib/base/CUnicode.h @@ -0,0 +1,146 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CUNICODE_H +#define CUNICODE_H + +#include "CString.h" +#include "BasicTypes.h" + +//! Unicode utility functions +/*! +This class provides functions for converting between various Unicode +encodings and the current locale encoding. +*/ +class CUnicode { +public: + //! @name accessors + //@{ + + //! Test UTF-8 string for validity + /*! + Returns true iff the string contains a valid sequence of UTF-8 + encoded characters. + */ + static bool isUTF8(const CString&); + + //! Convert from UTF-8 to UCS-2 encoding + /*! + Convert from UTF-8 to UCS-2. If errors is not NULL then *errors + is set to true iff any character could not be encoded in UCS-2. + Decoding errors do not set *errors. + */ + static CString UTF8ToUCS2(const CString&, bool* errors = NULL); + + //! Convert from UTF-8 to UCS-4 encoding + /*! + Convert from UTF-8 to UCS-4. If errors is not NULL then *errors + is set to true iff any character could not be encoded in UCS-4. + Decoding errors do not set *errors. + */ + static CString UTF8ToUCS4(const CString&, bool* errors = NULL); + + //! Convert from UTF-8 to UTF-16 encoding + /*! + Convert from UTF-8 to UTF-16. If errors is not NULL then *errors + is set to true iff any character could not be encoded in UTF-16. + Decoding errors do not set *errors. + */ + static CString UTF8ToUTF16(const CString&, bool* errors = NULL); + + //! Convert from UTF-8 to UTF-32 encoding + /*! + Convert from UTF-8 to UTF-32. If errors is not NULL then *errors + is set to true iff any character could not be encoded in UTF-32. + Decoding errors do not set *errors. + */ + static CString UTF8ToUTF32(const CString&, bool* errors = NULL); + + //! Convert from UTF-8 to the current locale encoding + /*! + Convert from UTF-8 to the current locale encoding. If errors is not + NULL then *errors is set to true iff any character could not be encoded. + Decoding errors do not set *errors. + */ + static CString UTF8ToText(const CString&, bool* errors = NULL); + + //! Convert from UCS-2 to UTF-8 + /*! + Convert from UCS-2 to UTF-8. If errors is not NULL then *errors is + set to true iff any character could not be decoded. + */ + static CString UCS2ToUTF8(const CString&, bool* errors = NULL); + + //! Convert from UCS-4 to UTF-8 + /*! + Convert from UCS-4 to UTF-8. If errors is not NULL then *errors is + set to true iff any character could not be decoded. + */ + static CString UCS4ToUTF8(const CString&, bool* errors = NULL); + + //! Convert from UTF-16 to UTF-8 + /*! + Convert from UTF-16 to UTF-8. If errors is not NULL then *errors is + set to true iff any character could not be decoded. + */ + static CString UTF16ToUTF8(const CString&, bool* errors = NULL); + + //! Convert from UTF-32 to UTF-8 + /*! + Convert from UTF-32 to UTF-8. If errors is not NULL then *errors is + set to true iff any character could not be decoded. + */ + static CString UTF32ToUTF8(const CString&, bool* errors = NULL); + + //! Convert from the current locale encoding to UTF-8 + /*! + Convert from the current locale encoding to UTF-8. If errors is not + NULL then *errors is set to true iff any character could not be decoded. + */ + static CString textToUTF8(const CString&, bool* errors = NULL); + + //@} + +private: + // convert UTF8 to wchar_t string (using whatever encoding is native + // to the platform). caller must delete[] the returned string. the + // string is *not* nul terminated; the length (in characters) is + // returned in size. + static wchar_t* UTF8ToWideChar(const CString&, + UInt32& size, bool* errors); + + // convert nul terminated wchar_t string (in platform's native + // encoding) to UTF8. + static CString wideCharToUTF8(const wchar_t*, + UInt32 size, bool* errors); + + // internal conversion to UTF8 + static CString doUCS2ToUTF8(const UInt8* src, UInt32 n, bool* errors); + static CString doUCS4ToUTF8(const UInt8* src, UInt32 n, bool* errors); + static CString doUTF16ToUTF8(const UInt8* src, UInt32 n, bool* errors); + static CString doUTF32ToUTF8(const UInt8* src, UInt32 n, bool* errors); + + // convert characters to/from UTF8 + static UInt32 fromUTF8(const UInt8*& src, UInt32& size); + static void toUTF8(CString& dst, UInt32 c, bool* errors); + +private: + static UInt32 s_invalid; + static UInt32 s_replacement; +}; + +#endif diff --git a/src/lib/base/ELevel.h b/src/lib/base/ELevel.h new file mode 100644 index 00000000..1552d40a --- /dev/null +++ b/src/lib/base/ELevel.h @@ -0,0 +1,40 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ELEVEL_H +#define ELEVEL_H + +//! Log levels +/*! +The logging priority levels in order of highest to lowest priority. +*/ +enum ELevel { + kPRINT = -1, //!< For print only (no file or time) + kFATAL, //!< For fatal errors + kERROR, //!< For serious errors + kWARNING, //!< For minor errors and warnings + kNOTE, //!< For messages about notable events + kINFO, //!< For informational messages + kDEBUG, //!< For important debugging messages + kDEBUG1, //!< For verbosity +1 debugging messages + kDEBUG2, //!< For verbosity +2 debugging messages + kDEBUG3, //!< For verbosity +3 debugging messages + kDEBUG4, //!< For verbosity +4 debugging messages + kDEBUG5 //!< For verbosity +5 debugging messages +}; + +#endif diff --git a/src/lib/base/IEventJob.h b/src/lib/base/IEventJob.h new file mode 100644 index 00000000..d9a2deb7 --- /dev/null +++ b/src/lib/base/IEventJob.h @@ -0,0 +1,35 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IEVENTJOB_H +#define IEVENTJOB_H + +#include "IInterface.h" + +class CEvent; + +//! Event handler interface +/*! +An event job is an interface for executing a event handler. +*/ +class IEventJob : public IInterface { +public: + //! Run the job + virtual void run(const CEvent&) = 0; +}; + +#endif diff --git a/src/lib/base/IEventQueue.cpp b/src/lib/base/IEventQueue.cpp new file mode 100644 index 00000000..36c65b29 --- /dev/null +++ b/src/lib/base/IEventQueue.cpp @@ -0,0 +1,46 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IEventQueue.h" + +// +// IEventQueue +// + +static int g_systemTarget = 0; +IEventQueue* IEventQueue::s_instance = NULL; + +void* +IEventQueue::getSystemTarget() +{ + // any unique arbitrary pointer will do + return &g_systemTarget; +} + +IEventQueue* +IEventQueue::getInstance() +{ + assert(s_instance != NULL); + return s_instance; +} + +void +IEventQueue::setInstance(IEventQueue* instance) +{ + assert(s_instance == NULL || instance == NULL); + s_instance = instance; +} diff --git a/src/lib/base/IEventQueue.h b/src/lib/base/IEventQueue.h new file mode 100644 index 00000000..edb7a691 --- /dev/null +++ b/src/lib/base/IEventQueue.h @@ -0,0 +1,223 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IEVENTQUEUE_H +#define IEVENTQUEUE_H + +#include "IInterface.h" +#include "CEvent.h" +#include "CString.h" + +#define EVENTQUEUE IEventQueue::getInstance() + +class IEventJob; +class IEventQueueBuffer; + +// Opaque type for timer info. This is defined by subclasses of +// IEventQueueBuffer. +class CEventQueueTimer; + +//! Event queue interface +/*! +An event queue provides a queue of CEvents. Clients can block waiting +on any event becoming available at the head of the queue and can place +new events at the end of the queue. Clients can also add and remove +timers which generate events periodically. +*/ +class IEventQueue : public IInterface { +public: + class CTimerEvent { + public: + CEventQueueTimer* m_timer; //!< The timer + UInt32 m_count; //!< Number of repeats + }; + + //! @name manipulators + //@{ + + //! Set the buffer + /*! + Replace the current event queue buffer. Any queued events are + discarded. The queue takes ownership of the buffer. + */ + virtual void adoptBuffer(IEventQueueBuffer*) = 0; + + //! Remove event from queue + /*! + Returns the next event on the queue into \p event. If no event is + available then blocks for up to \p timeout seconds, or forever if + \p timeout is negative. Returns true iff an event was available. + */ + virtual bool getEvent(CEvent& event, double timeout = -1.0) = 0; + + //! Dispatch an event + /*! + Looks up the dispatcher for the event's target and invokes it. + Returns true iff a dispatcher exists for the target. + */ + virtual bool dispatchEvent(const CEvent& event) = 0; + + //! Add event to queue + /*! + Adds \p event to the end of the queue. + */ + virtual void addEvent(const CEvent& event) = 0; + + //! Create a recurring timer + /*! + Creates and returns a timer. An event is returned after \p duration + seconds and the timer is reset to countdown again. When a timer event + is returned the data points to a \c CTimerEvent. The client must pass + the returned timer to \c deleteTimer() (whether or not the timer has + expired) to release the timer. The returned timer event uses the + given \p target. If \p target is NULL it uses the returned timer as + the target. + + Events for a single timer don't accumulate in the queue, even if the + client reading events can't keep up. Instead, the \c m_count member + of the \c CTimerEvent indicates how many events for the timer would + have been put on the queue since the last event for the timer was + removed (or since the timer was added). + */ + virtual CEventQueueTimer* + newTimer(double duration, void* target) = 0; + + //! Create a one-shot timer + /*! + Creates and returns a one-shot timer. An event is returned when + the timer expires and the timer is removed from further handling. + When a timer event is returned the data points to a \c CTimerEvent. + The c_count member of the \c CTimerEvent is always 1. The client + must pass the returned timer to \c deleteTimer() (whether or not the + timer has expired) to release the timer. The returned timer event + uses the given \p target. If \p target is NULL it uses the returned + timer as the target. + */ + virtual CEventQueueTimer* + newOneShotTimer(double duration, + void* target) = 0; + + //! Destroy a timer + /*! + Destroys a previously created timer. The timer is removed from the + queue and will not generate event, even if the timer has expired. + */ + virtual void deleteTimer(CEventQueueTimer*) = 0; + + //! Register an event handler for an event type + /*! + Registers an event handler for \p type and \p target. The \p handler + is adopted. Any existing handler for the type,target pair is deleted. + \c dispatchEvent() will invoke \p handler for any event for \p target + of type \p type. If no such handler exists it will use the handler + for \p target and type \p kUnknown if it exists. + */ + virtual void adoptHandler(CEvent::Type type, + void* target, IEventJob* handler) = 0; + + //! Unregister an event handler for an event type + /*! + Unregisters an event handler for the \p type, \p target pair and + deletes it. + */ + virtual void removeHandler(CEvent::Type type, void* target) = 0; + + //! Unregister all event handlers for an event target + /*! + Unregisters all event handlers for the \p target and deletes them. + */ + virtual void removeHandlers(void* target) = 0; + + //! Creates a new event type + /*! + Returns a unique event type id. + */ + virtual CEvent::Type + registerType(const char* name) = 0; + + //! Creates a new event type + /*! + If \p type contains \c kUnknown then it is set to a unique event + type id otherwise it is left alone. The final value of \p type + is returned. + */ + virtual CEvent::Type + registerTypeOnce(CEvent::Type& type, + const char* name) = 0; + + //@} + //! @name accessors + //@{ + + //! Test if queue is empty + /*! + Returns true iff the queue has no events in it, including timer + events. + */ + virtual bool isEmpty() const = 0; + + //! Get an event handler + /*! + Finds and returns the event handler for the \p type, \p target pair + if it exists, otherwise it returns NULL. + */ + virtual IEventJob* getHandler(CEvent::Type type, void* target) const = 0; + + //! Get name for event + /*! + Returns the name for the event \p type. This is primarily for + debugging. + */ + virtual const char* getTypeName(CEvent::Type type) = 0; + + //! Get an event type by name + /*! + Returns the registered type for an event for a given name. + */ + virtual CEvent::Type getRegisteredType(const CString& name) const = 0; + + //! Get the system event type target + /*! + Returns the target to use for dispatching \c CEvent::kSystem events. + */ + static void* getSystemTarget(); + + //! Get the singleton instance + /*! + Returns the singleton instance of the event queue + */ + static IEventQueue* getInstance(); + + //@} + +protected: + //! @name manipulators + //@{ + + //! Set the singleton instance + /*! + Sets the singleton instance of the event queue + */ + static void setInstance(IEventQueue*); + + //@} + +private: + static IEventQueue* s_instance; +}; + +#endif diff --git a/src/lib/base/IEventQueueBuffer.h b/src/lib/base/IEventQueueBuffer.h new file mode 100644 index 00000000..d9e6ae96 --- /dev/null +++ b/src/lib/base/IEventQueueBuffer.h @@ -0,0 +1,97 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IEVENTQUEUEBUFFER_H +#define IEVENTQUEUEBUFFER_H + +#include "IInterface.h" +#include "BasicTypes.h" + +class CEvent; +class CEventQueueTimer; + +//! Event queue buffer interface +/*! +An event queue buffer provides a queue of events for an IEventQueue. +*/ +class IEventQueueBuffer : public IInterface { +public: + enum Type { + kNone, //!< No event is available + kSystem, //!< Event is a system event + kUser //!< Event is a user event + }; + + //! @name manipulators + //@{ + + //! Block waiting for an event + /*! + Wait for an event in the event queue buffer for up to \p timeout + seconds. + */ + virtual void waitForEvent(double timeout) = 0; + + //! Get the next event + /*! + Get the next event from the buffer. Return kNone if no event is + available. If a system event is next, return kSystem and fill in + event. The event data in a system event can point to a static + buffer (because CEvent::deleteData() will not attempt to delete + data in a kSystem event). Otherwise, return kUser and fill in + \p dataID with the value passed to \c addEvent(). + */ + virtual Type getEvent(CEvent& event, UInt32& dataID) = 0; + + //! Post an event + /*! + Add the given event to the end of the queue buffer. This is a user + event and \c getEvent() must be able to identify it as such and + return \p dataID. This method must cause \c waitForEvent() to + return at some future time if it's blocked waiting on an event. + */ + virtual bool addEvent(UInt32 dataID) = 0; + + //@} + //! @name accessors + //@{ + + //! Check if event queue buffer is empty + /*! + Return true iff the event queue buffer is empty. + */ + virtual bool isEmpty() const = 0; + + //! Create a timer object + /*! + Create and return a timer object. The object is opaque and is + used only by the buffer but it must be a valid object (i.e. + not NULL). + */ + virtual CEventQueueTimer* + newTimer(double duration, bool oneShot) const = 0; + + //! Destroy a timer object + /*! + Destroy a timer object previously returned by \c newTimer(). + */ + virtual void deleteTimer(CEventQueueTimer*) const = 0; + + //@} +}; + +#endif diff --git a/src/lib/base/IJob.h b/src/lib/base/IJob.h new file mode 100644 index 00000000..7de55064 --- /dev/null +++ b/src/lib/base/IJob.h @@ -0,0 +1,33 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IJOB_H +#define IJOB_H + +#include "IInterface.h" + +//! Job interface +/*! +A job is an interface for executing some function. +*/ +class IJob : public IInterface { +public: + //! Run the job + virtual void run() = 0; +}; + +#endif diff --git a/src/lib/base/ILogOutputter.h b/src/lib/base/ILogOutputter.h new file mode 100644 index 00000000..6aa8b0b3 --- /dev/null +++ b/src/lib/base/ILogOutputter.h @@ -0,0 +1,71 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ILOGOUTPUTTER_H +#define ILOGOUTPUTTER_H + +#include "IInterface.h" +#include "CLog.h" +#include "ELevel.h" + +//! Outputter interface +/*! +Type of outputter interface. The logger performs all output through +outputters. ILogOutputter overrides must not call any log functions +directly or indirectly. +*/ +class ILogOutputter : public IInterface { +public: + //! @name manipulators + //@{ + + //! Open the outputter + /*! + Opens the outputter for writing. Calling this method on an + already open outputter must have no effect. + */ + virtual void open(const char* title) = 0; + + //! Close the outputter + /*! + Close the outputter. Calling this method on an already closed + outputter must have no effect. + */ + virtual void close() = 0; + + //! Show the outputter + /*! + Causes the output to become visible. This generally only makes sense + for a logger in a graphical user interface. Other implementations + will do nothing. Iff \p showIfEmpty is \c false then the implementation + may optionally only show the log if it's not empty. + */ + virtual void show(bool showIfEmpty) = 0; + + //! Write a message with level + /*! + Writes \c message, which has the given \c level, to a log. + If this method returns true then CLog will stop passing the + message to all outputters in the outputter chain, otherwise + it continues. Most implementations should return true. + */ + virtual bool write(ELevel level, const char* message) = 0; + + //@} +}; + +#endif diff --git a/src/lib/base/LogOutputters.cpp b/src/lib/base/LogOutputters.cpp new file mode 100644 index 00000000..aeb5fd63 --- /dev/null +++ b/src/lib/base/LogOutputters.cpp @@ -0,0 +1,302 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "LogOutputters.h" +#include "CArch.h" +#include "TMethodJob.h" + +#include +// +// CStopLogOutputter +// + +CStopLogOutputter::CStopLogOutputter() +{ + // do nothing +} + +CStopLogOutputter::~CStopLogOutputter() +{ + // do nothing +} + +void +CStopLogOutputter::open(const char*) +{ + // do nothing +} + +void +CStopLogOutputter::close() +{ + // do nothing +} + +void +CStopLogOutputter::show(bool) +{ + // do nothing +} + +bool +CStopLogOutputter::write(ELevel, const char*) +{ + return false; +} + + +// +// CConsoleLogOutputter +// + +CConsoleLogOutputter::CConsoleLogOutputter() +{ +} + +CConsoleLogOutputter::~CConsoleLogOutputter() +{ +} + +void +CConsoleLogOutputter::open(const char* title) +{ + ARCH->openConsole(title); +} + +void +CConsoleLogOutputter::close() +{ + ARCH->closeConsole(); +} + +void +CConsoleLogOutputter::show(bool showIfEmpty) +{ + ARCH->showConsole(showIfEmpty); +} + +bool +CConsoleLogOutputter::write(ELevel level, const char* msg) +{ + ARCH->writeConsole(level, msg); + return true; +} + +void +CConsoleLogOutputter::flush() +{ + +} + + +// +// CSystemLogOutputter +// + +CSystemLogOutputter::CSystemLogOutputter() +{ + // do nothing +} + +CSystemLogOutputter::~CSystemLogOutputter() +{ + // do nothing +} + +void +CSystemLogOutputter::open(const char* title) +{ + ARCH->openLog(title); +} + +void +CSystemLogOutputter::close() +{ + ARCH->closeLog(); +} + +void +CSystemLogOutputter::show(bool showIfEmpty) +{ + ARCH->showLog(showIfEmpty); +} + +bool +CSystemLogOutputter::write(ELevel level, const char* msg) +{ + ARCH->writeLog(level, msg); + return true; +} + +// +// CSystemLogger +// + +CSystemLogger::CSystemLogger(const char* title, bool blockConsole) : + m_stop(NULL) +{ + // redirect log messages + if (blockConsole) { + m_stop = new CStopLogOutputter; + CLOG->insert(m_stop); + } + m_syslog = new CSystemLogOutputter; + m_syslog->open(title); + CLOG->insert(m_syslog); +} + +CSystemLogger::~CSystemLogger() +{ + CLOG->remove(m_syslog); + delete m_syslog; + if (m_stop != NULL) { + CLOG->remove(m_stop); + delete m_stop; + } +} + + +// +// CBufferedLogOutputter +// + +CBufferedLogOutputter::CBufferedLogOutputter(UInt32 maxBufferSize) : + m_maxBufferSize(maxBufferSize) +{ + // do nothing +} + +CBufferedLogOutputter::~CBufferedLogOutputter() +{ + // do nothing +} + +CBufferedLogOutputter::const_iterator +CBufferedLogOutputter::begin() const +{ + return m_buffer.begin(); +} + +CBufferedLogOutputter::const_iterator +CBufferedLogOutputter::end() const +{ + return m_buffer.end(); +} + +void +CBufferedLogOutputter::open(const char*) +{ + // do nothing +} + +void +CBufferedLogOutputter::close() +{ + // remove all elements from the buffer + m_buffer.clear(); +} + +void +CBufferedLogOutputter::show(bool) +{ + // do nothing +} + +bool +CBufferedLogOutputter::write(ELevel, const char* message) +{ + while (m_buffer.size() >= m_maxBufferSize) { + m_buffer.pop_front(); + } + m_buffer.push_back(CString(message)); + return true; +} + + +// +// CFileLogOutputter +// + +CFileLogOutputter::CFileLogOutputter(const char* logFile) +{ + assert(logFile != NULL); + m_fileName = logFile; +} + +CFileLogOutputter::~CFileLogOutputter() +{ +} + +bool +CFileLogOutputter::write(ELevel level, const char *message) +{ + std::ofstream m_handle; + m_handle.open(m_fileName.c_str(), std::fstream::app); + if (m_handle.is_open() && m_handle.fail() != true) { + m_handle << message << std::endl; + } + m_handle.close(); + + return true; +} + +void +CFileLogOutputter::open(const char *title) {} + +void +CFileLogOutputter::close() {} + +void +CFileLogOutputter::show(bool showIfEmpty) {} + + +// +// CConsoleLogOutputter +// + +CIpcLogOutputter::CIpcLogOutputter() +{ +} + +CIpcLogOutputter::~CIpcLogOutputter() +{ +} + +void +CIpcLogOutputter::open(const char* title) +{ + ARCH->ipcLog().openLog(title); +} + +void +CIpcLogOutputter::close() +{ + ARCH->ipcLog().closeLog(); +} + +void +CIpcLogOutputter::show(bool showIfEmpty) +{ + ARCH->ipcLog().showLog(showIfEmpty); +} + +bool +CIpcLogOutputter::write(ELevel level, const char* msg) +{ + ARCH->ipcLog().writeLog(level, msg); + return true; +} diff --git a/src/lib/base/LogOutputters.h b/src/lib/base/LogOutputters.h new file mode 100644 index 00000000..8fa26bac --- /dev/null +++ b/src/lib/base/LogOutputters.h @@ -0,0 +1,171 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef LOGOUTPUTTERS_H +#define LOGOUTPUTTERS_H + +#include "BasicTypes.h" +#include "ILogOutputter.h" +#include "CString.h" +#include "stddeque.h" +#include "CThread.h" + +#include +#include + +//! Stop traversing log chain outputter +/*! +This outputter performs no output and returns false from \c write(), +causing the logger to stop traversing the outputter chain. Insert +this to prevent already inserted outputters from writing. +*/ +class CStopLogOutputter : public ILogOutputter { +public: + CStopLogOutputter(); + virtual ~CStopLogOutputter(); + + // ILogOutputter overrides + virtual void open(const char* title); + virtual void close(); + virtual void show(bool showIfEmpty); + virtual bool write(ELevel level, const char* message); +}; + +//! Write log to console +/*! +This outputter writes output to the console. The level for each +message is ignored. +*/ +class CConsoleLogOutputter : public ILogOutputter { +public: + CConsoleLogOutputter(); + virtual ~CConsoleLogOutputter(); + + // ILogOutputter overrides + virtual void open(const char* title); + virtual void close(); + virtual void show(bool showIfEmpty); + virtual bool write(ELevel level, const char* message); + virtual void flush(); +}; + +//! Write log to file +/*! +This outputter writes output to the file. The level for each +message is ignored. +*/ + +class CFileLogOutputter : public ILogOutputter { +public: + CFileLogOutputter(const char* logFile); + virtual ~CFileLogOutputter(); + + // ILogOutputter overrides + virtual void open(const char* title); + virtual void close(); + virtual void show(bool showIfEmpty); + virtual bool write(ELevel level, const char* message); +private: + std::string m_fileName; +}; + +//! Write log to system log +/*! +This outputter writes output to the system log. +*/ +class CSystemLogOutputter : public ILogOutputter { +public: + CSystemLogOutputter(); + virtual ~CSystemLogOutputter(); + + // ILogOutputter overrides + virtual void open(const char* title); + virtual void close(); + virtual void show(bool showIfEmpty); + virtual bool write(ELevel level, const char* message); +}; + +//! Write log to GUI over IPC +/*! +This outputter writes output to the GUI via IPC. +*/ +class CIpcLogOutputter : public ILogOutputter { +public: + CIpcLogOutputter(); + virtual ~CIpcLogOutputter(); + + // ILogOutputter overrides + virtual void open(const char* title); + virtual void close(); + virtual void show(bool showIfEmpty); + virtual bool write(ELevel level, const char* message); +}; + +//! Write log to system log only +/*! +Creating an object of this type inserts a CStopLogOutputter followed +by a CSystemLogOutputter into CLog. The destructor removes those +outputters. Add one of these to any scope that needs to write to +the system log (only) and restore the old outputters when exiting +the scope. +*/ +class CSystemLogger { +public: + CSystemLogger(const char* title, bool blockConsole); + ~CSystemLogger(); + +private: + ILogOutputter* m_syslog; + ILogOutputter* m_stop; +}; + +//! Save log history +/*! +This outputter records the last N log messages. +*/ +class CBufferedLogOutputter : public ILogOutputter { +private: + typedef std::deque CBuffer; + +public: + typedef CBuffer::const_iterator const_iterator; + + CBufferedLogOutputter(UInt32 maxBufferSize); + virtual ~CBufferedLogOutputter(); + + //! @name accessors + //@{ + + //! Get start of buffer + const_iterator begin() const; + + //! Get end of buffer + const_iterator end() const; + + //@} + + // ILogOutputter overrides + virtual void open(const char* title); + virtual void close(); + virtual void show(bool showIfEmpty); + virtual bool write(ELevel level, const char* message); +private: + UInt32 m_maxBufferSize; + CBuffer m_buffer; +}; + +#endif diff --git a/src/lib/base/TMethodEventJob.h b/src/lib/base/TMethodEventJob.h new file mode 100644 index 00000000..d8206735 --- /dev/null +++ b/src/lib/base/TMethodEventJob.h @@ -0,0 +1,73 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMETHODEVENTJOB_H +#define CMETHODEVENTJOB_H + +#include "IEventJob.h" + +//! Use a member function as an event job +/*! +An event job class that invokes a member function. +*/ +template +class TMethodEventJob : public IEventJob { +public: + //! run(event) invokes \c object->method(event, arg) + TMethodEventJob(T* object, + void (T::*method)(const CEvent&, void*), + void* arg = NULL); + virtual ~TMethodEventJob(); + + // IJob overrides + virtual void run(const CEvent&); + +private: + T* m_object; + void (T::*m_method)(const CEvent&, void*); + void* m_arg; +}; + +template +inline +TMethodEventJob::TMethodEventJob(T* object, + void (T::*method)(const CEvent&, void*), void* arg) : + m_object(object), + m_method(method), + m_arg(arg) +{ + // do nothing +} + +template +inline +TMethodEventJob::~TMethodEventJob() +{ + // do nothing +} + +template +inline +void +TMethodEventJob::run(const CEvent& event) +{ + if (m_object != NULL) { + (m_object->*m_method)(event, m_arg); + } +} + +#endif diff --git a/src/lib/base/TMethodJob.h b/src/lib/base/TMethodJob.h new file mode 100644 index 00000000..27c17103 --- /dev/null +++ b/src/lib/base/TMethodJob.h @@ -0,0 +1,70 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMETHODJOB_H +#define CMETHODJOB_H + +#include "IJob.h" + +//! Use a function as a job +/*! +A job class that invokes a member function. +*/ +template +class TMethodJob : public IJob { +public: + //! run() invokes \c object->method(arg) + TMethodJob(T* object, void (T::*method)(void*), void* arg = NULL); + virtual ~TMethodJob(); + + // IJob overrides + virtual void run(); + +private: + T* m_object; + void (T::*m_method)(void*); + void* m_arg; +}; + +template +inline +TMethodJob::TMethodJob(T* object, void (T::*method)(void*), void* arg) : + m_object(object), + m_method(method), + m_arg(arg) +{ + // do nothing +} + +template +inline +TMethodJob::~TMethodJob() +{ + // do nothing +} + +template +inline +void +TMethodJob::run() +{ + if (m_object != NULL) { + (m_object->*m_method)(m_arg); + } +} + +#endif diff --git a/src/lib/base/XBase.cpp b/src/lib/base/XBase.cpp new file mode 100644 index 00000000..4221fa61 --- /dev/null +++ b/src/lib/base/XBase.cpp @@ -0,0 +1,72 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "XBase.h" +#include "CStringUtil.h" +#include +#include + +// +// XBase +// + +XBase::XBase() : + m_what() +{ + // do nothing +} + +XBase::XBase(const CString& msg) : + m_what(msg) +{ + // do nothing +} + +XBase::~XBase() +{ + // do nothing +} + +const char* +XBase::what() const +{ + if (m_what.empty()) { + m_what = getWhat(); + } + return m_what.c_str(); +} + +CString +XBase::format(const char* /*id*/, const char* fmt, ...) const throw() +{ + // FIXME -- lookup message string using id as an index. set + // fmt to that string if it exists. + + // format + CString result; + va_list args; + va_start(args, fmt); + try { + result = CStringUtil::vformat(fmt, args); + } + catch (...) { + // ignore + } + va_end(args); + + return result; +} diff --git a/src/lib/base/XBase.h b/src/lib/base/XBase.h new file mode 100644 index 00000000..79fde568 --- /dev/null +++ b/src/lib/base/XBase.h @@ -0,0 +1,124 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef XBASE_H +#define XBASE_H + +#include "CString.h" + +//! Exception base class +/*! +This is the base class of most exception types. +*/ +class XBase { +public: + //! Use getWhat() as the result of what() + XBase(); + //! Use \c msg as the result of what() + XBase(const CString& msg); + virtual ~XBase(); + + //! Reason for exception + virtual const char* what() const; + +protected: + //! Get a human readable string describing the exception + virtual CString getWhat() const throw() = 0; + + //! Format a string + /*! + Looks up a message format using \c id, using \c defaultFormat if + no format can be found, then replaces positional parameters in + the format string and returns the result. + */ + virtual CString format(const char* id, + const char* defaultFormat, ...) const throw(); + +private: + mutable CString m_what; +}; + +/*! +\def XBASE_SUBCLASS +Convenience macro to subclass from XBase (or a subclass of it), +providing the c'tor taking a const CString&. getWhat() is not +declared. +*/ +#define XBASE_SUBCLASS(name_, super_) \ +class name_ : public super_ { \ +public: \ + name_() : super_() { } \ + name_(const CString& msg) : super_(msg) { } \ +} + +/*! +\def XBASE_SUBCLASS +Convenience macro to subclass from XBase (or a subclass of it), +providing the c'tor taking a const CString&. getWhat() must be +implemented. +*/ +#define XBASE_SUBCLASS_WHAT(name_, super_) \ +class name_ : public super_ { \ +public: \ + name_() : super_() { } \ + name_(const CString& msg) : super_(msg) { } \ + \ +protected: \ + virtual CString getWhat() const throw(); \ +} + +/*! +\def XBASE_SUBCLASS_FORMAT +Convenience macro to subclass from XBase (or a subclass of it), +providing the c'tor taking a const CString&. what() is overridden +to call getWhat() when first called; getWhat() can format the +error message and can call what() to get the message passed to the +c'tor. +*/ +#define XBASE_SUBCLASS_FORMAT(name_, super_) \ +class name_ : public super_ { \ +private: \ + enum EState { kFirst, kFormat, kDone }; \ + \ +public: \ + name_() : super_(), m_state(kDone) { } \ + name_(const CString& msg) : super_(msg), m_state(kFirst) { } \ + \ + virtual const char* what() const \ + { \ + if (m_state == kFirst) { \ + m_state = kFormat; \ + m_formatted = getWhat(); \ + m_state = kDone; \ + } \ + if (m_state == kDone) { \ + return m_formatted.c_str(); \ + } \ + else { \ + return super_::what(); \ + } \ + } \ + \ +protected: \ + virtual CString getWhat() const throw(); \ + \ +private: \ + mutable EState m_state; \ + mutable std::string m_formatted; \ +} + +#endif diff --git a/src/lib/client/CClient.cpp b/src/lib/client/CClient.cpp new file mode 100644 index 00000000..5c8d982e --- /dev/null +++ b/src/lib/client/CClient.cpp @@ -0,0 +1,741 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClient.h" +#include "CServerProxy.h" +#include "CScreen.h" +#include "CClipboard.h" +#include "CPacketStreamFilter.h" +#include "CProtocolUtil.h" +#include "ProtocolTypes.h" +#include "XSynergy.h" +#include "IDataSocket.h" +#include "ISocketFactory.h" +#include "IStreamFilterFactory.h" +#include "CLog.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" +#include +#include +#include "CArch.h" +#include "IPlatformScreen.h" + +// +// CClient +// + +CEvent::Type CClient::s_connectedEvent = CEvent::kUnknown; +CEvent::Type CClient::s_connectionFailedEvent = CEvent::kUnknown; +CEvent::Type CClient::s_disconnectedEvent = CEvent::kUnknown; + +CClient::CClient(IEventQueue& eventQueue) : + m_eventQueue(eventQueue) +{ +} + +CClient::CClient(IEventQueue& eventQueue, + const CString& name, const CNetworkAddress& address, + ISocketFactory* socketFactory, + IStreamFilterFactory* streamFilterFactory, + CScreen* screen) : + m_name(name), + m_serverAddress(address), + m_socketFactory(socketFactory), + m_streamFilterFactory(streamFilterFactory), + m_screen(screen), + m_stream(NULL), + m_timer(NULL), + m_server(NULL), + m_ready(false), + m_active(false), + m_suspended(false), + m_connectOnResume(false), + m_eventQueue(eventQueue), + m_mock(false) +{ + assert(m_socketFactory != NULL); + assert(m_screen != NULL); + + // register suspend/resume event handlers + m_eventQueue.adoptHandler(IScreen::getSuspendEvent(), + getEventTarget(), + new TMethodEventJob(this, + &CClient::handleSuspend)); + m_eventQueue.adoptHandler(IScreen::getResumeEvent(), + getEventTarget(), + new TMethodEventJob(this, + &CClient::handleResume)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getGameDeviceTimingRespEvent(), + getEventTarget(), + new TMethodEventJob(this, + &CClient::handleGameDeviceTimingResp)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getGameDeviceFeedbackEvent(), + getEventTarget(), + new TMethodEventJob(this, + &CClient::handleGameDeviceFeedback)); +} + +CClient::~CClient() +{ + // HACK: can't disable dtor with mocks + if (m_mock) + return; + + m_eventQueue.removeHandler(IScreen::getSuspendEvent(), + getEventTarget()); + m_eventQueue.removeHandler(IScreen::getResumeEvent(), + getEventTarget()); + + cleanupTimer(); + cleanupScreen(); + cleanupConnecting(); + cleanupConnection(); + delete m_socketFactory; + delete m_streamFilterFactory; +} + +void +CClient::connect() +{ + if (m_stream != NULL) { + return; + } + if (m_suspended) { + m_connectOnResume = true; + return; + } + + try { + // resolve the server hostname. do this every time we connect + // in case we couldn't resolve the address earlier or the address + // has changed (which can happen frequently if this is a laptop + // being shuttled between various networks). patch by Brent + // Priddy. + m_serverAddress.resolve(); + + // m_serverAddress will be null if the hostname address is not reolved + if (m_serverAddress.getAddress() != NULL) { + // to help users troubleshoot, show server host name (issue: 60) + LOG((CLOG_NOTE "connecting to '%s': %s:%i", + m_serverAddress.getHostname().c_str(), + ARCH->addrToString(m_serverAddress.getAddress()).c_str(), + m_serverAddress.getPort())); + } + + // create the socket + IDataSocket* socket = m_socketFactory->create(); + + // filter socket messages, including a packetizing filter + m_stream = socket; + if (m_streamFilterFactory != NULL) { + m_stream = m_streamFilterFactory->create(m_stream, true); + } + m_stream = new CPacketStreamFilter(m_stream, true); + + // connect + LOG((CLOG_DEBUG1 "connecting to server")); + setupConnecting(); + setupTimer(); + socket->connect(m_serverAddress); + } + catch (XBase& e) { + cleanupTimer(); + cleanupConnecting(); + delete m_stream; + m_stream = NULL; + LOG((CLOG_DEBUG1 "connection failed")); + sendConnectionFailedEvent(e.what()); + return; + } +} + +void +CClient::disconnect(const char* msg) +{ + m_connectOnResume = false; + cleanupTimer(); + cleanupScreen(); + cleanupConnecting(); + cleanupConnection(); + if (msg != NULL) { + sendConnectionFailedEvent(msg); + } + else { + sendEvent(getDisconnectedEvent(), NULL); + } +} + +void +CClient::handshakeComplete() +{ + m_ready = true; + m_screen->enable(); + sendEvent(getConnectedEvent(), NULL); +} + +bool +CClient::isConnected() const +{ + return (m_server != NULL); +} + +bool +CClient::isConnecting() const +{ + return (m_timer != NULL); +} + +CNetworkAddress +CClient::getServerAddress() const +{ + return m_serverAddress; +} + +CEvent::Type +CClient::getConnectedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_connectedEvent, + "CClient::connected"); +} + +CEvent::Type +CClient::getConnectionFailedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_connectionFailedEvent, + "CClient::failed"); +} + +CEvent::Type +CClient::getDisconnectedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_disconnectedEvent, + "CClient::disconnected"); +} + +void* +CClient::getEventTarget() const +{ + return m_screen->getEventTarget(); +} + +bool +CClient::getClipboard(ClipboardID id, IClipboard* clipboard) const +{ + return m_screen->getClipboard(id, clipboard); +} + +void +CClient::getShape(SInt32& x, SInt32& y, SInt32& w, SInt32& h) const +{ + m_screen->getShape(x, y, w, h); +} + +void +CClient::getCursorPos(SInt32& x, SInt32& y) const +{ + m_screen->getCursorPos(x, y); +} + +void +CClient::enter(SInt32 xAbs, SInt32 yAbs, UInt32, KeyModifierMask mask, bool) +{ + m_active = true; + m_screen->mouseMove(xAbs, yAbs); + m_screen->enter(mask); +} + +bool +CClient::leave() +{ + m_screen->leave(); + + m_active = false; + + // send clipboards that we own and that have changed + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + if (m_ownClipboard[id]) { + sendClipboard(id); + } + } + + return true; +} + +void +CClient::setClipboard(ClipboardID id, const IClipboard* clipboard) +{ + m_screen->setClipboard(id, clipboard); + m_ownClipboard[id] = false; + m_sentClipboard[id] = false; +} + +void +CClient::grabClipboard(ClipboardID id) +{ + m_screen->grabClipboard(id); + m_ownClipboard[id] = false; + m_sentClipboard[id] = false; +} + +void +CClient::setClipboardDirty(ClipboardID, bool) +{ + assert(0 && "shouldn't be called"); +} + +void +CClient::keyDown(KeyID id, KeyModifierMask mask, KeyButton button) +{ + m_screen->keyDown(id, mask, button); +} + +void +CClient::keyRepeat(KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton button) +{ + m_screen->keyRepeat(id, mask, count, button); +} + +void +CClient::keyUp(KeyID id, KeyModifierMask mask, KeyButton button) +{ + m_screen->keyUp(id, mask, button); +} + +void +CClient::mouseDown(ButtonID id) +{ + m_screen->mouseDown(id); +} + +void +CClient::mouseUp(ButtonID id) +{ + m_screen->mouseUp(id); +} + +void +CClient::mouseMove(SInt32 x, SInt32 y) +{ + m_screen->mouseMove(x, y); +} + +void +CClient::mouseRelativeMove(SInt32 dx, SInt32 dy) +{ + m_screen->mouseRelativeMove(dx, dy); +} + +void +CClient::mouseWheel(SInt32 xDelta, SInt32 yDelta) +{ + m_screen->mouseWheel(xDelta, yDelta); +} + +void +CClient::screensaver(bool activate) +{ + m_screen->screensaver(activate); +} + +void +CClient::resetOptions() +{ + m_screen->resetOptions(); +} + +void +CClient::setOptions(const COptionsList& options) +{ + m_screen->setOptions(options); +} + +void +CClient::gameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) +{ + m_screen->gameDeviceButtons(id, buttons); +} + +void +CClient::gameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) +{ + m_screen->gameDeviceSticks(id, x1, y1, x2, y2); +} + +void +CClient::gameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) +{ + m_screen->gameDeviceTriggers(id, t1, t2); +} + +void +CClient::gameDeviceTimingReq() +{ + m_screen->gameDeviceTimingReq(); +} + +CString +CClient::getName() const +{ + return m_name; +} + +void +CClient::sendClipboard(ClipboardID id) +{ + // note -- m_mutex must be locked on entry + assert(m_screen != NULL); + assert(m_server != NULL); + + // get clipboard data. set the clipboard time to the last + // clipboard time before getting the data from the screen + // as the screen may detect an unchanged clipboard and + // avoid copying the data. + CClipboard clipboard; + if (clipboard.open(m_timeClipboard[id])) { + clipboard.close(); + } + m_screen->getClipboard(id, &clipboard); + + // check time + if (m_timeClipboard[id] == 0 || + clipboard.getTime() != m_timeClipboard[id]) { + // save new time + m_timeClipboard[id] = clipboard.getTime(); + + // marshall the data + CString data = clipboard.marshall(); + + // save and send data if different or not yet sent + if (!m_sentClipboard[id] || data != m_dataClipboard[id]) { + m_sentClipboard[id] = true; + m_dataClipboard[id] = data; + m_server->onClipboardChanged(id, &clipboard); + } + } +} + +void +CClient::sendEvent(CEvent::Type type, void* data) +{ + m_eventQueue.addEvent(CEvent(type, getEventTarget(), data)); +} + +void +CClient::sendConnectionFailedEvent(const char* msg) +{ + CFailInfo* info = new CFailInfo(msg); + info->m_retry = true; + CEvent event(getConnectionFailedEvent(), getEventTarget(), info, CEvent::kDontFreeData); + EVENTQUEUE->addEvent(event); +} + +void +CClient::setupConnecting() +{ + assert(m_stream != NULL); + + m_eventQueue.adoptHandler(IDataSocket::getConnectedEvent(), + m_stream->getEventTarget(), + new TMethodEventJob(this, + &CClient::handleConnected)); + m_eventQueue.adoptHandler(IDataSocket::getConnectionFailedEvent(), + m_stream->getEventTarget(), + new TMethodEventJob(this, + &CClient::handleConnectionFailed)); +} + +void +CClient::setupConnection() +{ + assert(m_stream != NULL); + + m_eventQueue.adoptHandler(ISocket::getDisconnectedEvent(), + m_stream->getEventTarget(), + new TMethodEventJob(this, + &CClient::handleDisconnected)); + m_eventQueue.adoptHandler(m_stream->getInputReadyEvent(), + m_stream->getEventTarget(), + new TMethodEventJob(this, + &CClient::handleHello)); + m_eventQueue.adoptHandler(m_stream->getOutputErrorEvent(), + m_stream->getEventTarget(), + new TMethodEventJob(this, + &CClient::handleOutputError)); + m_eventQueue.adoptHandler(m_stream->getInputShutdownEvent(), + m_stream->getEventTarget(), + new TMethodEventJob(this, + &CClient::handleDisconnected)); + m_eventQueue.adoptHandler(m_stream->getOutputShutdownEvent(), + m_stream->getEventTarget(), + new TMethodEventJob(this, + &CClient::handleDisconnected)); +} + +void +CClient::setupScreen() +{ + assert(m_server == NULL); + + m_ready = false; + m_server = new CServerProxy(this, m_stream, *EVENTQUEUE); + m_eventQueue.adoptHandler(IScreen::getShapeChangedEvent(), + getEventTarget(), + new TMethodEventJob(this, + &CClient::handleShapeChanged)); + m_eventQueue.adoptHandler(IScreen::getClipboardGrabbedEvent(), + getEventTarget(), + new TMethodEventJob(this, + &CClient::handleClipboardGrabbed)); +} + +void +CClient::setupTimer() +{ + assert(m_timer == NULL); + + m_timer = m_eventQueue.newOneShotTimer(15.0, NULL); + m_eventQueue.adoptHandler(CEvent::kTimer, m_timer, + new TMethodEventJob(this, + &CClient::handleConnectTimeout)); +} + +void +CClient::cleanupConnecting() +{ + if (m_stream != NULL) { + m_eventQueue.removeHandler(IDataSocket::getConnectedEvent(), + m_stream->getEventTarget()); + m_eventQueue.removeHandler(IDataSocket::getConnectionFailedEvent(), + m_stream->getEventTarget()); + } +} + +void +CClient::cleanupConnection() +{ + if (m_stream != NULL) { + m_eventQueue.removeHandler(m_stream->getInputReadyEvent(), + m_stream->getEventTarget()); + m_eventQueue.removeHandler(m_stream->getOutputErrorEvent(), + m_stream->getEventTarget()); + m_eventQueue.removeHandler(m_stream->getInputShutdownEvent(), + m_stream->getEventTarget()); + m_eventQueue.removeHandler(m_stream->getOutputShutdownEvent(), + m_stream->getEventTarget()); + m_eventQueue.removeHandler(ISocket::getDisconnectedEvent(), + m_stream->getEventTarget()); + delete m_stream; + m_stream = NULL; + } +} + +void +CClient::cleanupScreen() +{ + if (m_server != NULL) { + if (m_ready) { + m_screen->disable(); + m_ready = false; + } + m_eventQueue.removeHandler(IScreen::getShapeChangedEvent(), + getEventTarget()); + m_eventQueue.removeHandler(IScreen::getClipboardGrabbedEvent(), + getEventTarget()); + delete m_server; + m_server = NULL; + } +} + +void +CClient::cleanupTimer() +{ + if (m_timer != NULL) { + m_eventQueue.removeHandler(CEvent::kTimer, m_timer); + m_eventQueue.deleteTimer(m_timer); + m_timer = NULL; + } +} + +void +CClient::handleConnected(const CEvent&, void*) +{ + LOG((CLOG_DEBUG1 "connected; wait for hello")); + cleanupConnecting(); + setupConnection(); + + // reset clipboard state + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + m_ownClipboard[id] = false; + m_sentClipboard[id] = false; + m_timeClipboard[id] = 0; + } +} + +void +CClient::handleConnectionFailed(const CEvent& event, void*) +{ + IDataSocket::CConnectionFailedInfo* info = + reinterpret_cast(event.getData()); + + cleanupTimer(); + cleanupConnecting(); + delete m_stream; + m_stream = NULL; + LOG((CLOG_DEBUG1 "connection failed")); + sendConnectionFailedEvent(info->m_what.c_str()); + delete info; +} + +void +CClient::handleConnectTimeout(const CEvent&, void*) +{ + cleanupTimer(); + cleanupConnecting(); + cleanupConnection(); + delete m_stream; + m_stream = NULL; + LOG((CLOG_DEBUG1 "connection timed out")); + sendConnectionFailedEvent("Timed out"); +} + +void +CClient::handleOutputError(const CEvent&, void*) +{ + cleanupTimer(); + cleanupScreen(); + cleanupConnection(); + LOG((CLOG_WARN "error sending to server")); + sendEvent(getDisconnectedEvent(), NULL); +} + +void +CClient::handleDisconnected(const CEvent&, void*) +{ + cleanupTimer(); + cleanupScreen(); + cleanupConnection(); + LOG((CLOG_DEBUG1 "disconnected")); + sendEvent(getDisconnectedEvent(), NULL); +} + +void +CClient::handleShapeChanged(const CEvent&, void*) +{ + LOG((CLOG_DEBUG "resolution changed")); + m_server->onInfoChanged(); +} + +void +CClient::handleClipboardGrabbed(const CEvent& event, void*) +{ + const IScreen::CClipboardInfo* info = + reinterpret_cast(event.getData()); + + // grab ownership + m_server->onGrabClipboard(info->m_id); + + // we now own the clipboard and it has not been sent to the server + m_ownClipboard[info->m_id] = true; + m_sentClipboard[info->m_id] = false; + m_timeClipboard[info->m_id] = 0; + + // if we're not the active screen then send the clipboard now, + // otherwise we'll wait until we leave. + if (!m_active) { + sendClipboard(info->m_id); + } +} + +void +CClient::handleHello(const CEvent&, void*) +{ + SInt16 major, minor; + if (!CProtocolUtil::readf(m_stream, kMsgHello, &major, &minor)) { + sendConnectionFailedEvent("Protocol error from server"); + cleanupTimer(); + cleanupConnection(); + return; + } + + // check versions + LOG((CLOG_DEBUG1 "got hello version %d.%d", major, minor)); + if (major < kProtocolMajorVersion || + (major == kProtocolMajorVersion && minor < kProtocolMinorVersion)) { + sendConnectionFailedEvent(XIncompatibleClient(major, minor).what()); + cleanupTimer(); + cleanupConnection(); + return; + } + + // say hello back + LOG((CLOG_DEBUG1 "say hello version %d.%d", kProtocolMajorVersion, kProtocolMinorVersion)); + CProtocolUtil::writef(m_stream, kMsgHelloBack, + kProtocolMajorVersion, + kProtocolMinorVersion, &m_name); + + // now connected but waiting to complete handshake + setupScreen(); + cleanupTimer(); + + // make sure we process any remaining messages later. we won't + // receive another event for already pending messages so we fake + // one. + if (m_stream->isReady()) { + m_eventQueue.addEvent(CEvent(m_stream->getInputReadyEvent(), + m_stream->getEventTarget())); + } +} + +void +CClient::handleSuspend(const CEvent&, void*) +{ + LOG((CLOG_INFO "suspend")); + m_suspended = true; + bool wasConnected = isConnected(); + disconnect(NULL); + m_connectOnResume = wasConnected; +} + +void +CClient::handleResume(const CEvent&, void*) +{ + LOG((CLOG_INFO "resume")); + m_suspended = false; + if (m_connectOnResume) { + m_connectOnResume = false; + connect(); + } +} + +void +CClient::handleGameDeviceTimingResp(const CEvent& event, void*) +{ + IPlatformScreen::CGameDeviceTimingRespInfo* info = + reinterpret_cast(event.getData()); + + m_server->onGameDeviceTimingResp(info->m_freq); +} + +void +CClient::handleGameDeviceFeedback(const CEvent& event, void*) +{ + IPlatformScreen::CGameDeviceFeedbackInfo* info = + reinterpret_cast(event.getData()); + + m_server->onGameDeviceFeedback(info->m_id, info->m_m1, info->m_m2); +} diff --git a/src/lib/client/CClient.h b/src/lib/client/CClient.h new file mode 100644 index 00000000..0216d558 --- /dev/null +++ b/src/lib/client/CClient.h @@ -0,0 +1,219 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CCLIENT_H +#define CCLIENT_H + +#include "IClient.h" +#include "IClipboard.h" +#include "CNetworkAddress.h" +#include "INode.h" + +class CEventQueueTimer; +class CScreen; +class CServerProxy; +class IDataSocket; +class ISocketFactory; +class IStream; +class IStreamFilterFactory; +class IEventQueue; + +//! Synergy client +/*! +This class implements the top-level client algorithms for synergy. +*/ +class CClient : public IClient, public INode { +public: + class CFailInfo { + public: + CFailInfo(const char* what) : m_retry(false), m_what(what) { } + bool m_retry; + CString m_what; + }; + +protected: + CClient(IEventQueue& eventQueue); + +public: + /*! + This client will attempt to connect to the server using \p name + as its name and \p address as the server's address and \p factory + to create the socket. \p screen is the local screen. + */ + CClient(IEventQueue& eventQueue, + const CString& name, const CNetworkAddress& address, + ISocketFactory* socketFactory, + IStreamFilterFactory* streamFilterFactory, + CScreen* screen); + ~CClient(); + + //! @name manipulators + //@{ + + //! Connect to server + /*! + Starts an attempt to connect to the server. This is ignored if + the client is trying to connect or is already connected. + */ + void connect(); + + //! Disconnect + /*! + Disconnects from the server with an optional error message. + */ + void disconnect(const char* msg); + + //! Notify of handshake complete + /*! + Notifies the client that the connection handshake has completed. + */ + void handshakeComplete(); + + //@} + //! @name accessors + //@{ + + //! Test if connected + /*! + Returns true iff the client is successfully connected to the server. + */ + bool isConnected() const; + + //! Test if connecting + /*! + Returns true iff the client is currently attempting to connect to + the server. + */ + bool isConnecting() const; + + //! Get address of server + /*! + Returns the address of the server the client is connected (or wants + to connect) to. + */ + CNetworkAddress getServerAddress() const; + + //! Get connected event type + /*! + Returns the connected event type. This is sent when the client has + successfully connected to the server. + */ + static CEvent::Type getConnectedEvent(); + + //! Get connection failed event type + /*! + Returns the connection failed event type. This is sent when the + server fails for some reason. The event data is a CFailInfo*. + */ + static CEvent::Type getConnectionFailedEvent(); + + //! Get disconnected event type + /*! + Returns the disconnected event type. This is sent when the client + has disconnected from the server (and only after having successfully + connected). + */ + static CEvent::Type getDisconnectedEvent(); + + //@} + + // IScreen overrides + virtual void* getEventTarget() const; + virtual bool getClipboard(ClipboardID id, IClipboard*) const; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const; + virtual void getCursorPos(SInt32& x, SInt32& y) const; + + // IClient overrides + virtual void enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, + bool forScreensaver); + virtual bool leave(); + virtual void setClipboard(ClipboardID, const IClipboard*); + virtual void grabClipboard(ClipboardID); + virtual void setClipboardDirty(ClipboardID, bool); + virtual void keyDown(KeyID, KeyModifierMask, KeyButton); + virtual void keyRepeat(KeyID, KeyModifierMask, + SInt32 count, KeyButton); + virtual void keyUp(KeyID, KeyModifierMask, KeyButton); + virtual void mouseDown(ButtonID); + virtual void mouseUp(ButtonID); + virtual void mouseMove(SInt32 xAbs, SInt32 yAbs); + virtual void mouseRelativeMove(SInt32 xRel, SInt32 yRel); + virtual void mouseWheel(SInt32 xDelta, SInt32 yDelta); + virtual void screensaver(bool activate); + virtual void resetOptions(); + virtual void setOptions(const COptionsList& options); + virtual void gameDeviceButtons(GameDeviceID id, GameDeviceButton buttons); + virtual void gameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2); + virtual void gameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2); + virtual void gameDeviceTimingReq(); + virtual CString getName() const; + +private: + void sendClipboard(ClipboardID); + void sendEvent(CEvent::Type, void*); + void sendConnectionFailedEvent(const char* msg); + void setupConnecting(); + void setupConnection(); + void setupScreen(); + void setupTimer(); + void cleanupConnecting(); + void cleanupConnection(); + void cleanupScreen(); + void cleanupTimer(); + void handleConnected(const CEvent&, void*); + void handleConnectionFailed(const CEvent&, void*); + void handleConnectTimeout(const CEvent&, void*); + void handleOutputError(const CEvent&, void*); + void handleDisconnected(const CEvent&, void*); + void handleShapeChanged(const CEvent&, void*); + void handleClipboardGrabbed(const CEvent&, void*); + void handleHello(const CEvent&, void*); + void handleSuspend(const CEvent& event, void*); + void handleResume(const CEvent& event, void*); + void handleGameDeviceTimingResp(const CEvent& event, void*); + void handleGameDeviceFeedback(const CEvent& event, void*); + +private: + CString m_name; + CNetworkAddress m_serverAddress; + ISocketFactory* m_socketFactory; + IStreamFilterFactory* m_streamFilterFactory; + CScreen* m_screen; + IStream* m_stream; + CEventQueueTimer* m_timer; + CServerProxy* m_server; + bool m_ready; + bool m_active; + bool m_suspended; + bool m_connectOnResume; + bool m_ownClipboard[kClipboardEnd]; + bool m_sentClipboard[kClipboardEnd]; + IClipboard::Time m_timeClipboard[kClipboardEnd]; + CString m_dataClipboard[kClipboardEnd]; + IEventQueue& m_eventQueue; + + static CEvent::Type s_connectedEvent; + static CEvent::Type s_connectionFailedEvent; + static CEvent::Type s_disconnectedEvent; + +protected: + bool m_mock; +}; + +#endif diff --git a/src/lib/client/CMakeLists.txt b/src/lib/client/CMakeLists.txt new file mode 100644 index 00000000..44e54ab1 --- /dev/null +++ b/src/lib/client/CMakeLists.txt @@ -0,0 +1,52 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(inc + CClient.h + CServerProxy.h +) + +set(src + CClient.cpp + CServerProxy.cpp +) + +if (WIN32) + list(APPEND src ${inc}) +endif() + +set(inc + ../arch + ../base + ../common + ../io + ../mt + ../net + ../synergy +) + +if (UNIX) + list(APPEND inc + ../../.. + ) + +endif() + +include_directories(${inc}) +add_library(client STATIC ${src}) + +if (UNIX) + target_link_libraries(client synergy io) +endif() diff --git a/src/lib/client/CServerProxy.cpp b/src/lib/client/CServerProxy.cpp new file mode 100644 index 00000000..22cb0785 --- /dev/null +++ b/src/lib/client/CServerProxy.cpp @@ -0,0 +1,914 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CServerProxy.h" +#include "CClient.h" +#include "CClipboard.h" +#include "CProtocolUtil.h" +#include "OptionTypes.h" +#include "ProtocolTypes.h" +#include "IStream.h" +#include "CLog.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" +#include "XBase.h" +#include +#include + +// +// CServerProxy +// + +CServerProxy::CServerProxy(CClient* client, IStream* stream, IEventQueue& eventQueue) : + m_client(client), + m_stream(stream), + m_seqNum(0), + m_compressMouse(false), + m_compressMouseRelative(false), + m_xMouse(0), + m_yMouse(0), + m_dxMouse(0), + m_dyMouse(0), + m_ignoreMouse(false), + m_keepAliveAlarm(0.0), + m_keepAliveAlarmTimer(NULL), + m_parser(&CServerProxy::parseHandshakeMessage), + m_eventQueue(eventQueue) +{ + assert(m_client != NULL); + assert(m_stream != NULL); + + // initialize modifier translation table + for (KeyModifierID id = 0; id < kKeyModifierIDLast; ++id) + m_modifierTranslationTable[id] = id; + + // handle data on stream + m_eventQueue.adoptHandler(m_stream->getInputReadyEvent(), + m_stream->getEventTarget(), + new TMethodEventJob(this, + &CServerProxy::handleData)); + + // send heartbeat + setKeepAliveRate(kKeepAliveRate); +} + +CServerProxy::~CServerProxy() +{ + setKeepAliveRate(-1.0); + m_eventQueue.removeHandler(m_stream->getInputReadyEvent(), + m_stream->getEventTarget()); +} + +void +CServerProxy::resetKeepAliveAlarm() +{ + if (m_keepAliveAlarmTimer != NULL) { + m_eventQueue.removeHandler(CEvent::kTimer, m_keepAliveAlarmTimer); + m_eventQueue.deleteTimer(m_keepAliveAlarmTimer); + m_keepAliveAlarmTimer = NULL; + } + if (m_keepAliveAlarm > 0.0) { + m_keepAliveAlarmTimer = + m_eventQueue.newOneShotTimer(m_keepAliveAlarm, NULL); + m_eventQueue.adoptHandler(CEvent::kTimer, m_keepAliveAlarmTimer, + new TMethodEventJob(this, + &CServerProxy::handleKeepAliveAlarm)); + } +} + +void +CServerProxy::setKeepAliveRate(double rate) +{ + m_keepAliveAlarm = rate * kKeepAlivesUntilDeath; + resetKeepAliveAlarm(); +} + +void +CServerProxy::handleData(const CEvent&, void*) +{ + // handle messages until there are no more. first read message code. + UInt8 code[4]; + UInt32 n = m_stream->read(code, 4); + while (n != 0) { + // verify we got an entire code + if (n != 4) { + LOG((CLOG_ERR "incomplete message from server: %d bytes", n)); + m_client->disconnect("incomplete message from server"); + return; + } + + // parse message + LOG((CLOG_DEBUG2 "msg from server: %c%c%c%c", code[0], code[1], code[2], code[3])); + switch ((this->*m_parser)(code)) { + case kOkay: + break; + + case kUnknown: + LOG((CLOG_ERR "invalid message from server: %c%c%c%c", code[0], code[1], code[2], code[3])); + m_client->disconnect("invalid message from server"); + return; + + case kDisconnect: + return; + } + + // next message + n = m_stream->read(code, 4); + } + + flushCompressedMouse(); +} + +CServerProxy::EResult +CServerProxy::parseHandshakeMessage(const UInt8* code) +{ + if (memcmp(code, kMsgQInfo, 4) == 0) { + queryInfo(); + } + + else if (memcmp(code, kMsgCInfoAck, 4) == 0) { + infoAcknowledgment(); + } + + else if (memcmp(code, kMsgDSetOptions, 4) == 0) { + setOptions(); + + // handshake is complete + m_parser = &CServerProxy::parseMessage; + m_client->handshakeComplete(); + } + + else if (memcmp(code, kMsgCResetOptions, 4) == 0) { + resetOptions(); + } + + else if (memcmp(code, kMsgCKeepAlive, 4) == 0) { + // echo keep alives and reset alarm + CProtocolUtil::writef(m_stream, kMsgCKeepAlive); + resetKeepAliveAlarm(); + } + + else if (memcmp(code, kMsgCNoop, 4) == 0) { + // accept and discard no-op + } + + else if (memcmp(code, kMsgCClose, 4) == 0) { + // server wants us to hangup + LOG((CLOG_DEBUG1 "recv close")); + m_client->disconnect(NULL); + return kDisconnect; + } + + else if (memcmp(code, kMsgEIncompatible, 4) == 0) { + SInt32 major, minor; + CProtocolUtil::readf(m_stream, + kMsgEIncompatible + 4, &major, &minor); + LOG((CLOG_ERR "server has incompatible version %d.%d", major, minor)); + m_client->disconnect("server has incompatible version"); + return kDisconnect; + } + + else if (memcmp(code, kMsgEBusy, 4) == 0) { + LOG((CLOG_ERR "server already has a connected client with name \"%s\"", m_client->getName().c_str())); + m_client->disconnect("server already has a connected client with our name"); + return kDisconnect; + } + + else if (memcmp(code, kMsgEUnknown, 4) == 0) { + LOG((CLOG_ERR "server refused client with name \"%s\"", m_client->getName().c_str())); + m_client->disconnect("server refused client with our name"); + return kDisconnect; + } + + else if (memcmp(code, kMsgEBad, 4) == 0) { + LOG((CLOG_ERR "server disconnected due to a protocol error")); + m_client->disconnect("server reported a protocol error"); + return kDisconnect; + } + else { + return kUnknown; + } + + return kOkay; +} + +CServerProxy::EResult +CServerProxy::parseMessage(const UInt8* code) +{ + if (memcmp(code, kMsgDMouseMove, 4) == 0) { + mouseMove(); + } + + else if (memcmp(code, kMsgDMouseRelMove, 4) == 0) { + mouseRelativeMove(); + } + + else if (memcmp(code, kMsgDMouseWheel, 4) == 0) { + mouseWheel(); + } + + else if (memcmp(code, kMsgDKeyDown, 4) == 0) { + keyDown(); + } + + else if (memcmp(code, kMsgDKeyUp, 4) == 0) { + keyUp(); + } + + else if (memcmp(code, kMsgDMouseDown, 4) == 0) { + mouseDown(); + } + + else if (memcmp(code, kMsgDMouseUp, 4) == 0) { + mouseUp(); + } + + else if (memcmp(code, kMsgDKeyRepeat, 4) == 0) { + keyRepeat(); + } + + else if (memcmp(code, kMsgCKeepAlive, 4) == 0) { + // echo keep alives and reset alarm + CProtocolUtil::writef(m_stream, kMsgCKeepAlive); + resetKeepAliveAlarm(); + } + + else if (memcmp(code, kMsgCNoop, 4) == 0) { + // accept and discard no-op + } + + else if (memcmp(code, kMsgCEnter, 4) == 0) { + enter(); + } + + else if (memcmp(code, kMsgCLeave, 4) == 0) { + leave(); + } + + else if (memcmp(code, kMsgCClipboard, 4) == 0) { + grabClipboard(); + } + + else if (memcmp(code, kMsgCScreenSaver, 4) == 0) { + screensaver(); + } + + else if (memcmp(code, kMsgQInfo, 4) == 0) { + queryInfo(); + } + + else if (memcmp(code, kMsgCInfoAck, 4) == 0) { + infoAcknowledgment(); + } + + else if (memcmp(code, kMsgDClipboard, 4) == 0) { + setClipboard(); + } + + else if (memcmp(code, kMsgCResetOptions, 4) == 0) { + resetOptions(); + } + + else if (memcmp(code, kMsgDSetOptions, 4) == 0) { + setOptions(); + } + + else if (memcmp(code, kMsgDGameButtons, 4) == 0) { + gameDeviceButtons(); + } + + else if (memcmp(code, kMsgDGameSticks, 4) == 0) { + gameDeviceSticks(); + } + + else if (memcmp(code, kMsgDGameTriggers, 4) == 0) { + gameDeviceTriggers(); + } + + else if (memcmp(code, kMsgCGameTimingReq, 4) == 0) { + gameDeviceTimingReq(); + } + + else if (memcmp(code, kMsgCClose, 4) == 0) { + // server wants us to hangup + LOG((CLOG_DEBUG1 "recv close")); + m_client->disconnect(NULL); + return kDisconnect; + } + else if (memcmp(code, kMsgEBad, 4) == 0) { + LOG((CLOG_ERR "server disconnected due to a protocol error")); + m_client->disconnect("server reported a protocol error"); + return kDisconnect; + } + else { + return kUnknown; + } + + // send a reply. this is intended to work around a delay when + // running a linux server and an OS X (any BSD?) client. the + // client waits to send an ACK (if the system control flag + // net.inet.tcp.delayed_ack is 1) in hopes of piggybacking it + // on a data packet. we provide that packet here. i don't + // know why a delayed ACK should cause the server to wait since + // TCP_NODELAY is enabled. + CProtocolUtil::writef(m_stream, kMsgCNoop); + + return kOkay; +} + +void +CServerProxy::handleKeepAliveAlarm(const CEvent&, void*) +{ + LOG((CLOG_NOTE "server is dead")); + m_client->disconnect("server is not responding"); +} + +void +CServerProxy::onInfoChanged() +{ + // ignore mouse motion until we receive acknowledgment of our info + // change message. + m_ignoreMouse = true; + + // send info update + queryInfo(); +} + +bool +CServerProxy::onGrabClipboard(ClipboardID id) +{ + LOG((CLOG_DEBUG1 "sending clipboard %d changed", id)); + CProtocolUtil::writef(m_stream, kMsgCClipboard, id, m_seqNum); + return true; +} + +void +CServerProxy::onClipboardChanged(ClipboardID id, const IClipboard* clipboard) +{ + CString data = IClipboard::marshall(clipboard); + LOG((CLOG_DEBUG1 "sending clipboard %d seqnum=%d, size=%d", id, m_seqNum, data.size())); + CProtocolUtil::writef(m_stream, kMsgDClipboard, id, m_seqNum, &data); +} + +void +CServerProxy::onGameDeviceTimingResp(UInt16 freq) +{ + LOG((CLOG_DEBUG1 "sending game device timing response freq=%d", freq)); + CProtocolUtil::writef(m_stream, kMsgCGameTimingResp, freq); +} + +void +CServerProxy::onGameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2) +{ + LOG((CLOG_DEBUG1 "sending game device feedback id=%d, m1=%d, m2=%d", id, m1, m2)); + CProtocolUtil::writef(m_stream, kMsgDGameFeedback, id, m1, m2); +} + +void +CServerProxy::flushCompressedMouse() +{ + if (m_compressMouse) { + m_compressMouse = false; + m_client->mouseMove(m_xMouse, m_yMouse); + } + if (m_compressMouseRelative) { + m_compressMouseRelative = false; + m_client->mouseRelativeMove(m_dxMouse, m_dyMouse); + m_dxMouse = 0; + m_dyMouse = 0; + } +} + +void +CServerProxy::sendInfo(const CClientInfo& info) +{ + LOG((CLOG_DEBUG1 "sending info shape=%d,%d %dx%d", info.m_x, info.m_y, info.m_w, info.m_h)); + CProtocolUtil::writef(m_stream, kMsgDInfo, + info.m_x, info.m_y, + info.m_w, info.m_h, 0, + info.m_mx, info.m_my); +} + +KeyID +CServerProxy::translateKey(KeyID id) const +{ + static const KeyID s_translationTable[kKeyModifierIDLast][2] = { + { kKeyNone, kKeyNone }, + { kKeyShift_L, kKeyShift_R }, + { kKeyControl_L, kKeyControl_R }, + { kKeyAlt_L, kKeyAlt_R }, + { kKeyMeta_L, kKeyMeta_R }, + { kKeySuper_L, kKeySuper_R }, + { kKeyAltGr, kKeyAltGr} + }; + + KeyModifierID id2 = kKeyModifierIDNull; + UInt32 side = 0; + switch (id) { + case kKeyShift_L: + id2 = kKeyModifierIDShift; + side = 0; + break; + + case kKeyShift_R: + id2 = kKeyModifierIDShift; + side = 1; + break; + + case kKeyControl_L: + id2 = kKeyModifierIDControl; + side = 0; + break; + + case kKeyControl_R: + id2 = kKeyModifierIDControl; + side = 1; + break; + + case kKeyAlt_L: + id2 = kKeyModifierIDAlt; + side = 0; + break; + + case kKeyAlt_R: + id2 = kKeyModifierIDAlt; + side = 1; + break; + + case kKeyAltGr: + id2 = kKeyModifierIDAltGr; + side = 1; // there is only one alt gr key on the right side + break; + + case kKeyMeta_L: + id2 = kKeyModifierIDMeta; + side = 0; + break; + + case kKeyMeta_R: + id2 = kKeyModifierIDMeta; + side = 1; + break; + + case kKeySuper_L: + id2 = kKeyModifierIDSuper; + side = 0; + break; + + case kKeySuper_R: + id2 = kKeyModifierIDSuper; + side = 1; + break; + } + + if (id2 != kKeyModifierIDNull) { + return s_translationTable[m_modifierTranslationTable[id2]][side]; + } + else { + return id; + } +} + +KeyModifierMask +CServerProxy::translateModifierMask(KeyModifierMask mask) const +{ + static const KeyModifierMask s_masks[kKeyModifierIDLast] = { + 0x0000, + KeyModifierShift, + KeyModifierControl, + KeyModifierAlt, + KeyModifierMeta, + KeyModifierSuper, + KeyModifierAltGr + }; + + KeyModifierMask newMask = mask & ~(KeyModifierShift | + KeyModifierControl | + KeyModifierAlt | + KeyModifierMeta | + KeyModifierSuper | + KeyModifierAltGr ); + if ((mask & KeyModifierShift) != 0) { + newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDShift]]; + } + if ((mask & KeyModifierControl) != 0) { + newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDControl]]; + } + if ((mask & KeyModifierAlt) != 0) { + newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDAlt]]; + } + if ((mask & KeyModifierAltGr) != 0) { + newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDAltGr]]; + } + if ((mask & KeyModifierMeta) != 0) { + newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDMeta]]; + } + if ((mask & KeyModifierSuper) != 0) { + newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDSuper]]; + } + return newMask; +} + +void +CServerProxy::enter() +{ + // parse + SInt16 x, y; + UInt16 mask; + UInt32 seqNum; + CProtocolUtil::readf(m_stream, kMsgCEnter + 4, &x, &y, &seqNum, &mask); + LOG((CLOG_DEBUG1 "recv enter, %d,%d %d %04x", x, y, seqNum, mask)); + + // discard old compressed mouse motion, if any + m_compressMouse = false; + m_compressMouseRelative = false; + m_dxMouse = 0; + m_dyMouse = 0; + m_seqNum = seqNum; + + // forward + m_client->enter(x, y, seqNum, static_cast(mask), false); +} + +void +CServerProxy::leave() +{ + // parse + LOG((CLOG_DEBUG1 "recv leave")); + + // send last mouse motion + flushCompressedMouse(); + + // forward + m_client->leave(); +} + +void +CServerProxy::setClipboard() +{ + // parse + ClipboardID id; + UInt32 seqNum; + CString data; + CProtocolUtil::readf(m_stream, kMsgDClipboard + 4, &id, &seqNum, &data); + LOG((CLOG_DEBUG "recv clipboard %d size=%d", id, data.size())); + + // validate + if (id >= kClipboardEnd) { + return; + } + + // forward + CClipboard clipboard; + clipboard.unmarshall(data, 0); + m_client->setClipboard(id, &clipboard); +} + +void +CServerProxy::grabClipboard() +{ + // parse + ClipboardID id; + UInt32 seqNum; + CProtocolUtil::readf(m_stream, kMsgCClipboard + 4, &id, &seqNum); + LOG((CLOG_DEBUG "recv grab clipboard %d", id)); + + // validate + if (id >= kClipboardEnd) { + return; + } + + // forward + m_client->grabClipboard(id); +} + +void +CServerProxy::keyDown() +{ + // get mouse up to date + flushCompressedMouse(); + + // parse + UInt16 id, mask, button; + CProtocolUtil::readf(m_stream, kMsgDKeyDown + 4, &id, &mask, &button); + LOG((CLOG_DEBUG1 "recv key down id=0x%08x, mask=0x%04x, button=0x%04x", id, mask, button)); + + // translate + KeyID id2 = translateKey(static_cast(id)); + KeyModifierMask mask2 = translateModifierMask( + static_cast(mask)); + if (id2 != static_cast(id) || + mask2 != static_cast(mask)) + LOG((CLOG_DEBUG1 "key down translated to id=0x%08x, mask=0x%04x", id2, mask2)); + + // forward + m_client->keyDown(id2, mask2, button); +} + +void +CServerProxy::keyRepeat() +{ + // get mouse up to date + flushCompressedMouse(); + + // parse + UInt16 id, mask, count, button; + CProtocolUtil::readf(m_stream, kMsgDKeyRepeat + 4, + &id, &mask, &count, &button); + LOG((CLOG_DEBUG1 "recv key repeat id=0x%08x, mask=0x%04x, count=%d, button=0x%04x", id, mask, count, button)); + + // translate + KeyID id2 = translateKey(static_cast(id)); + KeyModifierMask mask2 = translateModifierMask( + static_cast(mask)); + if (id2 != static_cast(id) || + mask2 != static_cast(mask)) + LOG((CLOG_DEBUG1 "key repeat translated to id=0x%08x, mask=0x%04x", id2, mask2)); + + // forward + m_client->keyRepeat(id2, mask2, count, button); +} + +void +CServerProxy::keyUp() +{ + // get mouse up to date + flushCompressedMouse(); + + // parse + UInt16 id, mask, button; + CProtocolUtil::readf(m_stream, kMsgDKeyUp + 4, &id, &mask, &button); + LOG((CLOG_DEBUG1 "recv key up id=0x%08x, mask=0x%04x, button=0x%04x", id, mask, button)); + + // translate + KeyID id2 = translateKey(static_cast(id)); + KeyModifierMask mask2 = translateModifierMask( + static_cast(mask)); + if (id2 != static_cast(id) || + mask2 != static_cast(mask)) + LOG((CLOG_DEBUG1 "key up translated to id=0x%08x, mask=0x%04x", id2, mask2)); + + // forward + m_client->keyUp(id2, mask2, button); +} + +void +CServerProxy::mouseDown() +{ + // get mouse up to date + flushCompressedMouse(); + + // parse + SInt8 id; + CProtocolUtil::readf(m_stream, kMsgDMouseDown + 4, &id); + LOG((CLOG_DEBUG1 "recv mouse down id=%d", id)); + + // forward + m_client->mouseDown(static_cast(id)); +} + +void +CServerProxy::mouseUp() +{ + // get mouse up to date + flushCompressedMouse(); + + // parse + SInt8 id; + CProtocolUtil::readf(m_stream, kMsgDMouseUp + 4, &id); + LOG((CLOG_DEBUG1 "recv mouse up id=%d", id)); + + // forward + m_client->mouseUp(static_cast(id)); +} + +void +CServerProxy::mouseMove() +{ + // parse + bool ignore; + SInt16 x, y; + CProtocolUtil::readf(m_stream, kMsgDMouseMove + 4, &x, &y); + + // note if we should ignore the move + ignore = m_ignoreMouse; + + // compress mouse motion events if more input follows + if (!ignore && !m_compressMouse && m_stream->isReady()) { + m_compressMouse = true; + } + + // if compressing then ignore the motion but record it + if (m_compressMouse) { + m_compressMouseRelative = false; + ignore = true; + m_xMouse = x; + m_yMouse = y; + m_dxMouse = 0; + m_dyMouse = 0; + } + LOG((CLOG_DEBUG2 "recv mouse move %d,%d", x, y)); + + // forward + if (!ignore) { + m_client->mouseMove(x, y); + } +} + +void +CServerProxy::mouseRelativeMove() +{ + // parse + bool ignore; + SInt16 dx, dy; + CProtocolUtil::readf(m_stream, kMsgDMouseRelMove + 4, &dx, &dy); + + // note if we should ignore the move + ignore = m_ignoreMouse; + + // compress mouse motion events if more input follows + if (!ignore && !m_compressMouseRelative && m_stream->isReady()) { + m_compressMouseRelative = true; + } + + // if compressing then ignore the motion but record it + if (m_compressMouseRelative) { + ignore = true; + m_dxMouse += dx; + m_dyMouse += dy; + } + LOG((CLOG_DEBUG2 "recv mouse relative move %d,%d", dx, dy)); + + // forward + if (!ignore) { + m_client->mouseRelativeMove(dx, dy); + } +} + +void +CServerProxy::mouseWheel() +{ + // get mouse up to date + flushCompressedMouse(); + + // parse + SInt16 xDelta, yDelta; + CProtocolUtil::readf(m_stream, kMsgDMouseWheel + 4, &xDelta, &yDelta); + LOG((CLOG_DEBUG2 "recv mouse wheel %+d,%+d", xDelta, yDelta)); + + // forward + m_client->mouseWheel(xDelta, yDelta); +} + +void +CServerProxy::gameDeviceButtons() +{ + // parse + GameDeviceID id; + GameDeviceButton buttons; + CProtocolUtil::readf(m_stream, kMsgDGameButtons + 4, &id, &buttons); + LOG((CLOG_DEBUG2 "recv game device id=%d buttons=%d", id, buttons)); + + // forward + m_client->gameDeviceButtons(id, buttons); +} + +void +CServerProxy::gameDeviceSticks() +{ + // parse + GameDeviceID id; + SInt16 x1, y1, x2, y2; + CProtocolUtil::readf(m_stream, kMsgDGameSticks + 4, &id, &x1, &y1, &x2, &y2); + LOG((CLOG_DEBUG2 "recv game device sticks id=%d s1=%+d,%+d s2=%+d,%+d", id, x1, y1, x2, y2)); + + // forward + m_client->gameDeviceSticks(id, x1, y1, x2, y2); +} + +void +CServerProxy::gameDeviceTriggers() +{ + // parse + GameDeviceID id; + UInt8 t1, t2; + CProtocolUtil::readf(m_stream, kMsgDGameTriggers + 4, &id, &t1, &t2); + LOG((CLOG_DEBUG2 "recv game device triggers id=%d t1=%d t2=%d", id, t1, t2)); + + // forward + m_client->gameDeviceTriggers(id, t1, t2); +} + +void +CServerProxy::gameDeviceTimingReq() +{ + // parse + LOG((CLOG_DEBUG2 "recv game device timing request")); + + // forward + m_client->gameDeviceTimingReq(); +} + +void +CServerProxy::screensaver() +{ + // parse + SInt8 on; + CProtocolUtil::readf(m_stream, kMsgCScreenSaver + 4, &on); + LOG((CLOG_DEBUG1 "recv screen saver on=%d", on)); + + // forward + m_client->screensaver(on != 0); +} + +void +CServerProxy::resetOptions() +{ + // parse + LOG((CLOG_DEBUG1 "recv reset options")); + + // forward + m_client->resetOptions(); + + // reset keep alive + setKeepAliveRate(kKeepAliveRate); + + // reset modifier translation table + for (KeyModifierID id = 0; id < kKeyModifierIDLast; ++id) { + m_modifierTranslationTable[id] = id; + } +} + +void +CServerProxy::setOptions() +{ + // parse + COptionsList options; + CProtocolUtil::readf(m_stream, kMsgDSetOptions + 4, &options); + LOG((CLOG_DEBUG1 "recv set options size=%d", options.size())); + + // forward + m_client->setOptions(options); + + // update modifier table + for (UInt32 i = 0, n = (UInt32)options.size(); i < n; i += 2) { + KeyModifierID id = kKeyModifierIDNull; + if (options[i] == kOptionModifierMapForShift) { + id = kKeyModifierIDShift; + } + else if (options[i] == kOptionModifierMapForControl) { + id = kKeyModifierIDControl; + } + else if (options[i] == kOptionModifierMapForAlt) { + id = kKeyModifierIDAlt; + } + else if (options[i] == kOptionModifierMapForAltGr) { + id = kKeyModifierIDAltGr; + } + else if (options[i] == kOptionModifierMapForMeta) { + id = kKeyModifierIDMeta; + } + else if (options[i] == kOptionModifierMapForSuper) { + id = kKeyModifierIDSuper; + } + else if (options[i] == kOptionHeartbeat) { + // update keep alive + setKeepAliveRate(1.0e-3 * static_cast(options[i + 1])); + } + if (id != kKeyModifierIDNull) { + m_modifierTranslationTable[id] = + static_cast(options[i + 1]); + LOG((CLOG_DEBUG1 "modifier %d mapped to %d", id, m_modifierTranslationTable[id])); + } + } +} + +void +CServerProxy::queryInfo() +{ + CClientInfo info; + m_client->getShape(info.m_x, info.m_y, info.m_w, info.m_h); + m_client->getCursorPos(info.m_mx, info.m_my); + sendInfo(info); +} + +void +CServerProxy::infoAcknowledgment() +{ + LOG((CLOG_DEBUG1 "recv info acknowledgment")); + m_ignoreMouse = false; +} diff --git a/src/lib/client/CServerProxy.h b/src/lib/client/CServerProxy.h new file mode 100644 index 00000000..6a4b6590 --- /dev/null +++ b/src/lib/client/CServerProxy.h @@ -0,0 +1,127 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CSERVERPROXY_H +#define CSERVERPROXY_H + +#include "ClipboardTypes.h" +#include "KeyTypes.h" +#include "CEvent.h" +#include "GameDeviceTypes.h" + +class CClient; +class CClientInfo; +class CEventQueueTimer; +class IClipboard; +class IStream; +class IEventQueue; + +//! Proxy for server +/*! +This class acts a proxy for the server, converting calls into messages +to the server and messages from the server to calls on the client. +*/ +class CServerProxy { +public: + /*! + Process messages from the server on \p stream and forward to + \p client. + */ + CServerProxy(CClient* client, IStream* stream, IEventQueue& eventQueue); + ~CServerProxy(); + + //! @name manipulators + //@{ + + void onInfoChanged(); + bool onGrabClipboard(ClipboardID); + void onClipboardChanged(ClipboardID, const IClipboard*); + void onGameDeviceTimingResp(UInt16 freq); + void onGameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2); + + //@} + +protected: + enum EResult { kOkay, kUnknown, kDisconnect }; + EResult parseHandshakeMessage(const UInt8* code); + EResult parseMessage(const UInt8* code); + +private: + // if compressing mouse motion then send the last motion now + void flushCompressedMouse(); + + void sendInfo(const CClientInfo&); + + void resetKeepAliveAlarm(); + void setKeepAliveRate(double); + + // modifier key translation + KeyID translateKey(KeyID) const; + KeyModifierMask translateModifierMask(KeyModifierMask) const; + + // event handlers + void handleData(const CEvent&, void*); + void handleKeepAliveAlarm(const CEvent&, void*); + + // message handlers + void enter(); + void leave(); + void setClipboard(); + void grabClipboard(); + void keyDown(); + void keyRepeat(); + void keyUp(); + void mouseDown(); + void mouseUp(); + void mouseMove(); + void mouseRelativeMove(); + void mouseWheel(); + void gameDeviceButtons(); + void gameDeviceSticks(); + void gameDeviceTriggers(); + void gameDeviceTimingReq(); + void screensaver(); + void resetOptions(); + void setOptions(); + void queryInfo(); + void infoAcknowledgment(); + +private: + typedef EResult (CServerProxy::*MessageParser)(const UInt8*); + + CClient* m_client; + IStream* m_stream; + + UInt32 m_seqNum; + + bool m_compressMouse; + bool m_compressMouseRelative; + SInt32 m_xMouse, m_yMouse; + SInt32 m_dxMouse, m_dyMouse; + + bool m_ignoreMouse; + + KeyModifierID m_modifierTranslationTable[kKeyModifierIDLast]; + + double m_keepAliveAlarm; + CEventQueueTimer* m_keepAliveAlarmTimer; + + MessageParser m_parser; + IEventQueue& m_eventQueue; +}; + +#endif diff --git a/src/lib/common/BasicTypes.h b/src/lib/common/BasicTypes.h new file mode 100644 index 00000000..2e89ff42 --- /dev/null +++ b/src/lib/common/BasicTypes.h @@ -0,0 +1,90 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef BASICTYPES_H +#define BASICTYPES_H + +#include "common.h" + +// +// pick types of particular sizes +// + +#if !defined(TYPE_OF_SIZE_1) +# if SIZEOF_CHAR == 1 +# define TYPE_OF_SIZE_1 char +# endif +#endif + +#if !defined(TYPE_OF_SIZE_2) +# if SIZEOF_INT == 2 +# define TYPE_OF_SIZE_2 int +# else +# define TYPE_OF_SIZE_2 short +# endif +#endif + +#if !defined(TYPE_OF_SIZE_4) + // Carbon defines SInt32 and UInt32 in terms of long +# if SIZEOF_INT == 4 && !defined(__APPLE__) +# define TYPE_OF_SIZE_4 int +# else +# define TYPE_OF_SIZE_4 long +# endif +#endif + + // +// verify existence of required types +// + +#if !defined(TYPE_OF_SIZE_1) +# error No 1 byte integer type +#endif +#if !defined(TYPE_OF_SIZE_2) +# error No 2 byte integer type +#endif +#if !defined(TYPE_OF_SIZE_4) +# error No 4 byte integer type +#endif + + +// +// make typedefs +// +// except for SInt8 and UInt8 these types are only guaranteed to be +// at least as big as indicated (in bits). that is, they may be +// larger than indicated. +// + +// Added this because it doesn't compile on OS X 10.6 because they are already defined in Carbon +#if !defined(__MACTYPES__) +typedef signed TYPE_OF_SIZE_1 SInt8; +typedef signed TYPE_OF_SIZE_2 SInt16; +typedef signed TYPE_OF_SIZE_4 SInt32; +typedef unsigned TYPE_OF_SIZE_1 UInt8; +typedef unsigned TYPE_OF_SIZE_2 UInt16; +typedef unsigned TYPE_OF_SIZE_4 UInt32; +#endif +// +// clean up +// + +#undef TYPE_OF_SIZE_1 +#undef TYPE_OF_SIZE_2 +#undef TYPE_OF_SIZE_4 + +#endif diff --git a/src/lib/common/CMakeLists.txt b/src/lib/common/CMakeLists.txt new file mode 100644 index 00000000..a92eb1b7 --- /dev/null +++ b/src/lib/common/CMakeLists.txt @@ -0,0 +1,34 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(inc + Version.h +) + +set(src + Version.cpp +) + +if (WIN32) + list(APPEND src ${inc}) +endif() + +if (UNIX) + include_directories( + ../../.. + ) +endif() + +add_library(common STATIC ${src}) diff --git a/src/lib/common/IInterface.h b/src/lib/common/IInterface.h new file mode 100644 index 00000000..ad4176fd --- /dev/null +++ b/src/lib/common/IInterface.h @@ -0,0 +1,34 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IINTERFACE_H +#define IINTERFACE_H + +#include "common.h" + +//! Base class of interfaces +/*! +This is the base class of all interface classes. An interface class has +only pure virtual methods. +*/ +class IInterface { +public: + //! Interface destructor does nothing + virtual ~IInterface() { } +}; + +#endif diff --git a/src/lib/common/MacOSXPrecomp.h b/src/lib/common/MacOSXPrecomp.h new file mode 100644 index 00000000..fab1e83d --- /dev/null +++ b/src/lib/common/MacOSXPrecomp.h @@ -0,0 +1,25 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + + // +// Prefix header for all source files of the 'deleteme' target in the 'deleteme' project. +// + +#define MAC_OS_X_VERSION_MAX_ALLOWED MAC_OS_X_VERSION_10_2 + + +#include diff --git a/src/lib/common/Version.cpp b/src/lib/common/Version.cpp new file mode 100644 index 00000000..8f551269 --- /dev/null +++ b/src/lib/common/Version.cpp @@ -0,0 +1,25 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "Version.h" + +const char* kApplication = "Synergy"; +const char* kCopyright = "Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea"; +const char* kContact = "Synergy Mailing List, synergy-plus@googlegroups.com"; +const char* kWebsite = "http://synergy-foss.org/"; +const char* kVersion = VERSION; +const char* kAppVersion = "Synergy " VERSION; diff --git a/src/lib/common/Version.h b/src/lib/common/Version.h new file mode 100644 index 00000000..be189961 --- /dev/null +++ b/src/lib/common/Version.h @@ -0,0 +1,41 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef VERSION_H +#define VERSION_H + +#include "common.h" + +// set version macro if not set yet +#if !defined(VERSION) +#error Version was not set (should be passed to compiler). +#endif + +// important strings +extern const char* kApplication; +extern const char* kCopyright; +extern const char* kContact; +extern const char* kWebsite; + +// build version. follows linux kernel style: an even minor number implies +// a release version, odd implies development version. +extern const char* kVersion; + +// application version +extern const char* kAppVersion; + +#endif diff --git a/src/lib/common/common.h b/src/lib/common/common.h new file mode 100644 index 00000000..3f395786 --- /dev/null +++ b/src/lib/common/common.h @@ -0,0 +1,161 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef COMMON_H +#define COMMON_H + +// this file should be included, directly or indirectly by every other. + +#if HAVE_CONFIG_H +# include "config.h" + + // don't use poll() on mac +# if defined(__APPLE__) +# undef HAVE_POLL +# endif +#else + // we may not have run configure on win32 +# if defined(_WIN32) +# define SYSAPI_WIN32 1 +# define WINAPI_MSWINDOWS 1 +# endif + + // we may not have run configure on OS X +# if defined(__APPLE__) +# define SYSAPI_UNIX 1 +# define WINAPI_CARBON 1 + +# define HAVE_CXX_BOOL 1 +# define HAVE_CXX_CASTS 1 +# define HAVE_CXX_EXCEPTIONS 1 +# define HAVE_CXX_MUTABLE 1 +# define HAVE_CXX_STDLIB 1 +# define HAVE_GETPWUID_R 1 +# define HAVE_GMTIME_R 1 +# define HAVE_INET_ATON 1 +# define HAVE_INTTYPES_H 1 +# define HAVE_ISTREAM 1 +# define HAVE_MEMORY_H 1 +# define HAVE_NANOSLEEP 1 +# define HAVE_OSTREAM 1 +# define HAVE_POSIX_SIGWAIT 1 +# define HAVE_PTHREAD 1 +# define HAVE_PTHREAD_SIGNAL 1 +# include +# include +# if defined(_SOCKLEN_T) +# define HAVE_SOCKLEN_T 1 +# endif +# define HAVE_SSTREAM 1 +# define HAVE_STDINT_H 1 +# define HAVE_STDLIB_H 1 +# define HAVE_STRINGS_H 1 +# define HAVE_STRING_H 1 +# define HAVE_SYS_SELECT_H 1 +# define HAVE_SYS_SOCKET_H 1 +# define HAVE_SYS_STAT_H 1 +# define HAVE_SYS_TIME_H 1 +# define HAVE_SYS_TYPES_H 1 +# define HAVE_SYS_UTSNAME_H 1 +# define HAVE_UNISTD_H 1 +# define HAVE_VSNPRINTF 1 +/* disable this so we can build with the 10.2.8 SDK */ +/*# define HAVE_WCHAR_H 1*/ + +# define SELECT_TYPE_ARG1 int +# define SELECT_TYPE_ARG234 (fd_set *) +# define SELECT_TYPE_ARG5 (struct timeval *) +# define SIZEOF_CHAR 1 +# define SIZEOF_INT 4 +# define SIZEOF_LONG 4 +# define SIZEOF_SHORT 2 +# define STDC_HEADERS 1 +# define TIME_WITH_SYS_TIME 1 +# define X_DISPLAY_MISSING 1 +# endif +#endif + +// VC++ specific +#if (_MSC_VER >= 1200) + // work around for statement scoping bug +# define for if (false) { } else for + + // turn off bonehead warnings +# pragma warning(disable: 4786) // identifier truncated in debug info +# pragma warning(disable: 4514) // unreferenced inline function removed + + // this one's a little too aggressive +# pragma warning(disable: 4127) // conditional expression is constant + + // Code Analysis +# pragma warning(disable: 6011) + + + // emitted incorrectly under release build in some circumstances +# if defined(NDEBUG) +# pragma warning(disable: 4702) // unreachable code +# pragma warning(disable: 4701) // variable maybe used uninitialized +# endif +#endif // (_MSC_VER >= 1200) + +// VC++ has built-in sized types +#if defined(_MSC_VER) +# include +# define TYPE_OF_SIZE_1 __int8 +# define TYPE_OF_SIZE_2 __int16 +# define TYPE_OF_SIZE_4 __int32 +#else +# define SIZE_OF_CHAR 1 +# define SIZE_OF_SHORT 2 +# define SIZE_OF_INT 4 +# define SIZE_OF_LONG 4 +#endif + +// FIXME -- including fp.h from Carbon.h causes a undefined symbol error +// on my build system. the symbol is scalb. since we don't need any +// math functions we define __FP__, the include guard macro for fp.h, to +// prevent fp.h from being included. +#if defined(__APPLE__) +#define __FP__ +#endif + +// define NULL +#include + +// we don't want to use NULL since it's old and nasty, so replace any +// usages with nullptr (warning: this could break many things). +// if not c++0x yet, future proof code by allowing use of nullptr +#ifdef nullptr +#define NULL nullptr +#else +#define nullptr NULL +#endif + +// make assert available since we use it a lot +#include +#include +#include + +enum { + kExitSuccess = 0, // successful completion + kExitFailed = 1, // general failure + kExitTerminated = 2, // killed by signal + kExitArgs = 3, // bad arguments + kExitConfig = 4 // cannot read configuration +}; + +#endif diff --git a/src/lib/common/stdbitset.h b/src/lib/common/stdbitset.h new file mode 100644 index 00000000..d3a93a5f --- /dev/null +++ b/src/lib/common/stdbitset.h @@ -0,0 +1,20 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "stdpre.h" +#include +#include "stdpost.h" diff --git a/src/lib/common/stddeque.h b/src/lib/common/stddeque.h new file mode 100644 index 00000000..7a52facb --- /dev/null +++ b/src/lib/common/stddeque.h @@ -0,0 +1,20 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "stdpre.h" +#include +#include "stdpost.h" diff --git a/src/lib/common/stdfstream.h b/src/lib/common/stdfstream.h new file mode 100644 index 00000000..95856248 --- /dev/null +++ b/src/lib/common/stdfstream.h @@ -0,0 +1,21 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "stdpre.h" +#include +#include "stdpost.h" +#include "stdistream.h" diff --git a/src/lib/common/stdistream.h b/src/lib/common/stdistream.h new file mode 100644 index 00000000..496b8bdb --- /dev/null +++ b/src/lib/common/stdistream.h @@ -0,0 +1,46 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "stdpre.h" +#if HAVE_ISTREAM +#include +#else +#include +#endif +#include "stdpost.h" + +#if defined(_MSC_VER) && _MSC_VER <= 1200 +// VC++6 istream has no overloads for __int* types, .NET does +inline +std::istream& operator>>(std::istream& s, SInt8& i) +{ return s >> (signed char&)i; } +inline +std::istream& operator>>(std::istream& s, SInt16& i) +{ return s >> (short&)i; } +inline +std::istream& operator>>(std::istream& s, SInt32& i) +{ return s >> (int&)i; } +inline +std::istream& operator>>(std::istream& s, UInt8& i) +{ return s >> (unsigned char&)i; } +inline +std::istream& operator>>(std::istream& s, UInt16& i) +{ return s >> (unsigned short&)i; } +inline +std::istream& operator>>(std::istream& s, UInt32& i) +{ return s >> (unsigned int&)i; } +#endif diff --git a/src/lib/common/stdlist.h b/src/lib/common/stdlist.h new file mode 100644 index 00000000..16ae5671 --- /dev/null +++ b/src/lib/common/stdlist.h @@ -0,0 +1,20 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "stdpre.h" +#include +#include "stdpost.h" diff --git a/src/lib/common/stdmap.h b/src/lib/common/stdmap.h new file mode 100644 index 00000000..09230c83 --- /dev/null +++ b/src/lib/common/stdmap.h @@ -0,0 +1,20 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "stdpre.h" +#include +#include "stdpost.h" diff --git a/src/lib/common/stdostream.h b/src/lib/common/stdostream.h new file mode 100644 index 00000000..7e58bae2 --- /dev/null +++ b/src/lib/common/stdostream.h @@ -0,0 +1,24 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "stdpre.h" +#if HAVE_OSTREAM +#include +#else +#include +#endif +#include "stdpost.h" diff --git a/src/lib/common/stdpost.h b/src/lib/common/stdpost.h new file mode 100644 index 00000000..c5f6e882 --- /dev/null +++ b/src/lib/common/stdpost.h @@ -0,0 +1,20 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#if defined(_MSC_VER) +#pragma warning(pop) +#endif diff --git a/src/lib/common/stdpre.h b/src/lib/common/stdpre.h new file mode 100644 index 00000000..ce64fd1e --- /dev/null +++ b/src/lib/common/stdpre.h @@ -0,0 +1,30 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#if defined(_MSC_VER) +#pragma warning(disable: 4786) // identifier truncated +#pragma warning(disable: 4514) // unreferenced inline +#pragma warning(disable: 4710) // not inlined +#pragma warning(disable: 4663) // C++ change, template specialization +#pragma warning(disable: 4503) // decorated name length too long +#pragma warning(push, 3) +#pragma warning(disable: 4018) // signed/unsigned mismatch +#pragma warning(disable: 4284) +#pragma warning(disable: 4146) // unary minus on unsigned value +#pragma warning(disable: 4127) // conditional expression is constant +#pragma warning(disable: 4701) // variable possibly used uninitialized +#endif diff --git a/src/lib/common/stdset.h b/src/lib/common/stdset.h new file mode 100644 index 00000000..0916a7af --- /dev/null +++ b/src/lib/common/stdset.h @@ -0,0 +1,20 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "stdpre.h" +#include +#include "stdpost.h" diff --git a/src/lib/common/stdsstream.h b/src/lib/common/stdsstream.h new file mode 100644 index 00000000..8f70bafa --- /dev/null +++ b/src/lib/common/stdsstream.h @@ -0,0 +1,374 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "stdpre.h" + +#if HAVE_SSTREAM || !defined(__GNUC__) || (__GNUC__ >= 3) + +#include + +#elif defined(__GNUC_MINOR__) && (__GNUC_MINOR__ >= 95) +// g++ 2.95 didn't ship with sstream. the following is a backport +// by Magnus Fromreide of the sstream in g++ 3.0. + +/* This is part of libio/iostream, providing -*- C++ -*- input/output. +Copyright (C) 2000 Free Software Foundation + +This file is part of the GNU IO Library. This library is free +software; you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the +Free Software Foundation; either version 2, or (at your option) +any later version. + +This library 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 library; see the file COPYING. If not, write to the Free +Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +As a special exception, if you link this library with files +compiled with a GNU compiler to produce an executable, this does not cause +the resulting executable to be covered by the GNU General Public License. +This exception does not however invalidate any other reasons why +the executable file might be covered by the GNU General Public License. */ + +/* Written by Magnus Fromreide (magfr@lysator.liu.se). */ +/* seekoff and ideas for overflow is largely borrowed from libstdc++-v3 */ + +#include +#include +#include + +namespace std +{ + class stringbuf : public streambuf + { + public: + typedef char char_type; + typedef int int_type; + typedef streampos pos_type; + typedef streamoff off_type; + + explicit + stringbuf(int which=ios::in|ios::out) + : streambuf(), mode(static_cast(which)), + stream(NULL), stream_len(0) + { + stringbuf_init(); + } + + explicit + stringbuf(const string &str, int which=ios::in|ios::out) + : streambuf(), mode(static_cast(which)), + stream(NULL), stream_len(0) + { + if (mode & (ios::in|ios::out)) + { + stream_len = str.size(); + stream = new char_type[stream_len]; + str.copy(stream, stream_len); + } + stringbuf_init(); + } + + virtual + ~stringbuf() + { + delete[] stream; + } + + string + str() const + { + if (pbase() != 0) + return string(stream, pptr()-pbase()); + else + return string(); + } + + void + str(const string& str) + { + delete[] stream; + stream_len = str.size(); + stream = new char_type[stream_len]; + str.copy(stream, stream_len); + stringbuf_init(); + } + + protected: + // The buffer is already in gptr, so if it ends then it is out of data. + virtual int + underflow() + { + return EOF; + } + + virtual int + overflow(int c = EOF) + { + int res; + if (mode & ios::out) + { + if (c != EOF) + { + streamsize old_stream_len = stream_len; + stream_len += 1; + char_type* new_stream = new char_type[stream_len]; + memcpy(new_stream, stream, old_stream_len); + delete[] stream; + stream = new_stream; + stringbuf_sync(gptr()-eback(), pptr()-pbase()); + sputc(c); + res = c; + } + else + res = EOF; + } + else + res = 0; + return res; + } + + virtual streambuf* + setbuf(char_type* s, streamsize n) + { + if (n != 0) + { + delete[] stream; + stream = new char_type[n]; + memcpy(stream, s, n); + stream_len = n; + stringbuf_sync(0, 0); + } + return this; + } + + virtual pos_type + seekoff(off_type off, ios::seek_dir way, int which = ios::in | ios::out) + { + pos_type ret = pos_type(off_type(-1)); + bool testin = which & ios::in && mode & ios::in; + bool testout = which & ios::out && mode & ios::out; + bool testboth = testin && testout && way != ios::cur; + + if (stream_len && ((testin != testout) || testboth)) + { + char_type* beg = stream; + char_type* curi = NULL; + char_type* curo = NULL; + char_type* endi = NULL; + char_type* endo = NULL; + + if (testin) + { + curi = gptr(); + endi = egptr(); + } + if (testout) + { + curo = pptr(); + endo = epptr(); + } + + off_type newoffi = 0; + off_type newoffo = 0; + if (way == ios::beg) + { + newoffi = beg - curi; + newoffo = beg - curo; + } + else if (way == ios::end) + { + newoffi = endi - curi; + newoffo = endo - curo; + } + + if (testin && newoffi + off + curi - beg >= 0 && + endi - beg >= newoffi + off + curi - beg) + { + gbump(newoffi + off); + ret = pos_type(newoffi + off + curi); + } + if (testout && newoffo + off + curo - beg >= 0 && + endo - beg >= newoffo + off + curo - beg) + { + pbump(newoffo + off); + ret = pos_type(newoffo + off + curo); + } + } + return ret; + } + + virtual pos_type + seekpos(pos_type sp, int which = ios::in | ios::out) + { + pos_type ret = seekoff(sp, ios::beg, which); + return ret; + } + + private: + void + stringbuf_sync(streamsize i, streamsize o) + { + if (mode & ios::in) + setg(stream, stream + i, stream + stream_len); + if (mode & ios::out) + { + setp(stream, stream + stream_len); + pbump(o); + } + } + void + stringbuf_init() + { + if (mode & ios::ate) + stringbuf_sync(0, stream_len); + else + stringbuf_sync(0, 0); + } + + private: + ios::open_mode mode; + char_type* stream; + streamsize stream_len; + }; + + class istringstream : public istream { + public: + typedef char char_type; + typedef int int_type; + typedef streampos pos_type; + typedef streamoff off_type; + + explicit + istringstream(int which=ios::in) + : istream(&sb), sb(which | ios::in) + { } + + explicit + istringstream(const string& str, int which=ios::in) + : istream(&sb), sb(str, which | ios::in) + { } + + stringbuf* + rdbuf() const + { + return const_cast(&sb); + } + + string + str() const + { + return rdbuf()->str(); + } + void + str(const string& s) + { + rdbuf()->str(s); + } + private: + stringbuf sb; + }; + + class ostringstream : public ostream { + public: + typedef char char_type; + typedef int int_type; + typedef streampos pos_type; + typedef streamoff off_type; + + explicit + ostringstream(int which=ios::out) + : ostream(&sb), sb(which | ios::out) + { } + + explicit + ostringstream(const string& str, int which=ios::out) + : ostream(&sb), sb(str, which | ios::out) + { } + + stringbuf* + rdbuf() const + { + return const_cast(&sb); + } + + string + str() const + { + return rdbuf()->str(); + } + + void str(const string& s) + { + rdbuf()->str(s); + } + private: + stringbuf sb; + }; + + class stringstream : public iostream { + public: + typedef char char_type; + typedef int int_type; + typedef streampos pos_type; + typedef streamoff off_type; + + explicit + stringstream(int which=ios::out|ios::in) + : iostream(&sb), sb(which) + { } + + explicit + stringstream(const string& str, int which=ios::out|ios::in) + : iostream(&sb), sb(str, which) + { } + + stringbuf* + rdbuf() const + { + return const_cast(&sb); + } + + string + str() const + { + return rdbuf()->str(); + } + + void + str(const string& s) + { + rdbuf()->str(s); + } + private: + stringbuf sb; + }; +}; + +#else /* not g++ 2.95 and no */ + +#error "Standard C++ library is missing required sstream header." + +#endif /* not g++ 2.95 and no */ + +#include "stdpost.h" +#include "stdistream.h" diff --git a/src/lib/common/stdstring.h b/src/lib/common/stdstring.h new file mode 100644 index 00000000..54c3be31 --- /dev/null +++ b/src/lib/common/stdstring.h @@ -0,0 +1,20 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "stdpre.h" +#include +#include "stdpost.h" diff --git a/src/lib/common/stdvector.h b/src/lib/common/stdvector.h new file mode 100644 index 00000000..bbde89f7 --- /dev/null +++ b/src/lib/common/stdvector.h @@ -0,0 +1,20 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "stdpre.h" +#include +#include "stdpost.h" diff --git a/src/lib/io/CMakeLists.txt b/src/lib/io/CMakeLists.txt new file mode 100644 index 00000000..4fdaf92b --- /dev/null +++ b/src/lib/io/CMakeLists.txt @@ -0,0 +1,48 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(inc + CStreamBuffer.h + CStreamFilter.h + IStream.h + IStreamFilterFactory.h + XIO.h +) + +set(src + CStreamBuffer.cpp + CStreamFilter.cpp + IStream.cpp + XIO.cpp +) + +if (WIN32) + list(APPEND src ${inc}) +endif() + +set(inc + ../arch + ../base + ../common +) + +if (UNIX) + list(APPEND inc + ../../.. + ) +endif() + +include_directories(${inc}) +add_library(io STATIC ${src}) diff --git a/src/lib/io/CStreamBuffer.cpp b/src/lib/io/CStreamBuffer.cpp new file mode 100644 index 00000000..23b5a870 --- /dev/null +++ b/src/lib/io/CStreamBuffer.cpp @@ -0,0 +1,145 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CStreamBuffer.h" + +// +// CStreamBuffer +// + +const UInt32 CStreamBuffer::kChunkSize = 4096; + +CStreamBuffer::CStreamBuffer() : + m_size(0), + m_headUsed(0) +{ + // do nothing +} + +CStreamBuffer::~CStreamBuffer() +{ + // do nothing +} + +const void* +CStreamBuffer::peek(UInt32 n) +{ + assert(n <= m_size); + + // if requesting no data then return NULL so we don't try to access + // an empty list. + if (n == 0) { + return NULL; + } + + // reserve space in first chunk + ChunkList::iterator head = m_chunks.begin(); + head->reserve(n + m_headUsed); + + // consolidate chunks into the first chunk until it has n bytes + ChunkList::iterator scan = head; + ++scan; + while (head->size() - m_headUsed < n && scan != m_chunks.end()) { + head->insert(head->end(), scan->begin(), scan->end()); + scan = m_chunks.erase(scan); + } + + return reinterpret_cast(&(head->begin()[m_headUsed])); +} + +void +CStreamBuffer::pop(UInt32 n) +{ + // discard all chunks if n is greater than or equal to m_size + if (n >= m_size) { + m_size = 0; + m_headUsed = 0; + m_chunks.clear(); + return; + } + + // update size + m_size -= n; + + // discard chunks until more than n bytes would've been discarded + ChunkList::iterator scan = m_chunks.begin(); + assert(scan != m_chunks.end()); + while (scan->size() - m_headUsed <= n) { + n -= (UInt32)scan->size() - m_headUsed; + m_headUsed = 0; + scan = m_chunks.erase(scan); + assert(scan != m_chunks.end()); + } + + // remove left over bytes from the head chunk + if (n > 0) { + m_headUsed += n; + } +} + +void +CStreamBuffer::write(const void* vdata, UInt32 n) +{ + assert(vdata != NULL); + + // ignore if no data, otherwise update size + if (n == 0) { + return; + } + m_size += n; + + // cast data to bytes + const UInt8* data = reinterpret_cast(vdata); + + // point to last chunk if it has space, otherwise append an empty chunk + ChunkList::iterator scan = m_chunks.end(); + if (scan != m_chunks.begin()) { + --scan; + if (scan->size() >= kChunkSize) { + ++scan; + } + } + if (scan == m_chunks.end()) { + scan = m_chunks.insert(scan, Chunk()); + } + + // append data in chunks + while (n > 0) { + // choose number of bytes for next chunk + assert(scan->size() <= kChunkSize); + UInt32 count = kChunkSize - (UInt32)scan->size(); + if (count > n) + count = n; + + // transfer data + scan->insert(scan->end(), data, data + count); + n -= count; + data += count; + + // append another empty chunk if we're not done yet + if (n > 0) { + ++scan; + scan = m_chunks.insert(scan, Chunk()); + } + } +} + +UInt32 +CStreamBuffer::getSize() const +{ + return m_size; +} diff --git a/src/lib/io/CStreamBuffer.h b/src/lib/io/CStreamBuffer.h new file mode 100644 index 00000000..53fafdb6 --- /dev/null +++ b/src/lib/io/CStreamBuffer.h @@ -0,0 +1,81 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CSTREAMBUFFER_H +#define CSTREAMBUFFER_H + +#include "BasicTypes.h" +#include "stdlist.h" +#include "stdvector.h" + +//! FIFO of bytes +/*! +This class maintains a FIFO (first-in, last-out) buffer of bytes. +*/ +class CStreamBuffer { +public: + CStreamBuffer(); + ~CStreamBuffer(); + + //! @name manipulators + //@{ + + //! Read data without removing from buffer + /*! + Return a pointer to memory with the next \c n bytes in the buffer + (which must be <= getSize()). The caller must not modify the returned + memory nor delete it. + */ + const void* peek(UInt32 n); + + //! Discard data + /*! + Discards the next \c n bytes. If \c n >= getSize() then the buffer + is cleared. + */ + void pop(UInt32 n); + + //! Write data to buffer + /*! + Appends \c n bytes from \c data to the buffer. + */ + void write(const void* data, UInt32 n); + + //@} + //! @name accessors + //@{ + + //! Get size of buffer + /*! + Returns the number of bytes in the buffer. + */ + UInt32 getSize() const; + + //@} + +private: + static const UInt32 kChunkSize; + + typedef std::vector Chunk; + typedef std::list ChunkList; + + ChunkList m_chunks; + UInt32 m_size; + UInt32 m_headUsed; +}; + +#endif diff --git a/src/lib/io/CStreamFilter.cpp b/src/lib/io/CStreamFilter.cpp new file mode 100644 index 00000000..830b427a --- /dev/null +++ b/src/lib/io/CStreamFilter.cpp @@ -0,0 +1,116 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CStreamFilter.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" + +// +// CStreamFilter +// + +CStreamFilter::CStreamFilter(IStream* stream, bool adoptStream) : + m_stream(stream), + m_adopted(adoptStream) +{ + // replace handlers for m_stream + EVENTQUEUE->removeHandlers(m_stream->getEventTarget()); + EVENTQUEUE->adoptHandler(CEvent::kUnknown, m_stream->getEventTarget(), + new TMethodEventJob(this, + &CStreamFilter::handleUpstreamEvent)); +} + +CStreamFilter::~CStreamFilter() +{ + EVENTQUEUE->removeHandler(CEvent::kUnknown, m_stream->getEventTarget()); + if (m_adopted) { + delete m_stream; + } +} + +void +CStreamFilter::close() +{ + getStream()->close(); +} + +UInt32 +CStreamFilter::read(void* buffer, UInt32 n) +{ + return getStream()->read(buffer, n); +} + +void +CStreamFilter::write(const void* buffer, UInt32 n) +{ + getStream()->write(buffer, n); +} + +void +CStreamFilter::flush() +{ + getStream()->flush(); +} + +void +CStreamFilter::shutdownInput() +{ + getStream()->shutdownInput(); +} + +void +CStreamFilter::shutdownOutput() +{ + getStream()->shutdownOutput(); +} + +void* +CStreamFilter::getEventTarget() const +{ + return const_cast(reinterpret_cast(this)); +} + +bool +CStreamFilter::isReady() const +{ + return getStream()->isReady(); +} + +UInt32 +CStreamFilter::getSize() const +{ + return getStream()->getSize(); +} + +IStream* +CStreamFilter::getStream() const +{ + return m_stream; +} + +void +CStreamFilter::filterEvent(const CEvent& event) +{ + EVENTQUEUE->dispatchEvent(CEvent(event.getType(), + getEventTarget(), event.getData())); +} + +void +CStreamFilter::handleUpstreamEvent(const CEvent& event, void*) +{ + filterEvent(event); +} diff --git a/src/lib/io/CStreamFilter.h b/src/lib/io/CStreamFilter.h new file mode 100644 index 00000000..bcab9f6a --- /dev/null +++ b/src/lib/io/CStreamFilter.h @@ -0,0 +1,73 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CSTREAMFILTER_H +#define CSTREAMFILTER_H + +#include "IStream.h" + +//! A stream filter +/*! +This class wraps a stream. Subclasses provide indirect access +to the wrapped stream, typically performing some filtering. +*/ +class CStreamFilter : public IStream { +public: + /*! + Create a wrapper around \c stream. Iff \c adoptStream is true then + this object takes ownership of the stream and will delete it in the + d'tor. + */ + CStreamFilter(IStream* stream, bool adoptStream = true); + ~CStreamFilter(); + + // IStream overrides + // These all just forward to the underlying stream except getEventTarget. + // Override as necessary. getEventTarget returns a pointer to this. + virtual void close(); + virtual UInt32 read(void* buffer, UInt32 n); + virtual void write(const void* buffer, UInt32 n); + virtual void flush(); + virtual void shutdownInput(); + virtual void shutdownOutput(); + virtual void* getEventTarget() const; + virtual bool isReady() const; + virtual UInt32 getSize() const; + +protected: + //! Get the stream + /*! + Returns the stream passed to the c'tor. + */ + IStream* getStream() const; + + //! Handle events from source stream + /*! + Does the event filtering. The default simply dispatches an event + identical except using this object as the event target. + */ + virtual void filterEvent(const CEvent&); + +private: + void handleUpstreamEvent(const CEvent&, void*); + +private: + IStream* m_stream; + bool m_adopted; +}; + +#endif diff --git a/src/lib/io/IStream.cpp b/src/lib/io/IStream.cpp new file mode 100644 index 00000000..f8e813ef --- /dev/null +++ b/src/lib/io/IStream.cpp @@ -0,0 +1,64 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IStream.h" +#include "CEventQueue.h" + +// +// IStream +// + +CEvent::Type IStream::s_inputReadyEvent = CEvent::kUnknown; +CEvent::Type IStream::s_outputFlushedEvent = CEvent::kUnknown; +CEvent::Type IStream::s_outputErrorEvent = CEvent::kUnknown; +CEvent::Type IStream::s_inputShutdownEvent = CEvent::kUnknown; +CEvent::Type IStream::s_outputShutdownEvent = CEvent::kUnknown; + +CEvent::Type +IStream::getInputReadyEvent() +{ + return m_eventQueue.registerTypeOnce(s_inputReadyEvent, + "IStream::inputReady"); +} + +CEvent::Type +IStream::getOutputFlushedEvent() +{ + return m_eventQueue.registerTypeOnce(s_outputFlushedEvent, + "IStream::outputFlushed"); +} + +CEvent::Type +IStream::getOutputErrorEvent() +{ + return m_eventQueue.registerTypeOnce(s_outputErrorEvent, + "IStream::outputError"); +} + +CEvent::Type +IStream::getInputShutdownEvent() +{ + return m_eventQueue.registerTypeOnce(s_inputShutdownEvent, + "IStream::inputShutdown"); +} + +CEvent::Type +IStream::getOutputShutdownEvent() +{ + return m_eventQueue.registerTypeOnce(s_outputShutdownEvent, + "IStream::outputShutdown"); +} diff --git a/src/lib/io/IStream.h b/src/lib/io/IStream.h new file mode 100644 index 00000000..6c33ca63 --- /dev/null +++ b/src/lib/io/IStream.h @@ -0,0 +1,168 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ISTREAM_H +#define ISTREAM_H + +#include "IInterface.h" +#include "CEvent.h" +#include "IEventQueue.h" + +class IEventQueue; + +//! Bidirectional stream interface +/*! +Defines the interface for all streams. +*/ +class IStream : public IInterface { +public: + IStream() : m_eventQueue(*EVENTQUEUE) { } + IStream(IEventQueue& eventQueue) : m_eventQueue(eventQueue) { } + + //! @name manipulators + //@{ + + //! Close the stream + /*! + Closes the stream. Pending input data and buffered output data + are discarded. Use \c flush() before \c close() to send buffered + output data. Attempts to \c read() after a close return 0, + attempts to \c write() generate output error events, and attempts + to \c flush() return immediately. + */ + virtual void close() = 0; + + //! Read from stream + /*! + Read up to \p n bytes into \p buffer, returning the number read + (zero if no data is available or input is shutdown). \p buffer + may be NULL in which case the data is discarded. + */ + virtual UInt32 read(void* buffer, UInt32 n) = 0; + + //! Write to stream + /*! + Write \c n bytes from \c buffer to the stream. If this can't + complete immediately it will block. Data may be buffered in + order to return more quickly. A output error event is generated + when writing fails. + */ + virtual void write(const void* buffer, UInt32 n) = 0; + + //! Flush the stream + /*! + Waits until all buffered data has been written to the stream. + */ + virtual void flush() = 0; + + //! Shutdown input + /*! + Shutdown the input side of the stream. Any pending input data is + discarded and further reads immediately return 0. + */ + virtual void shutdownInput() = 0; + + //! Shutdown output + /*! + Shutdown the output side of the stream. Any buffered output data + is discarded and further writes generate output error events. Use + \c flush() before \c shutdownOutput() to send buffered output data. + */ + virtual void shutdownOutput() = 0; + + //@} + //! @name accessors + //@{ + + //! Get event target + /*! + Returns the event target for events generated by this stream. It + should be the source stream in a chain of stream filters. + */ + virtual void* getEventTarget() const = 0; + + //! Test if \c read() will succeed + /*! + Returns true iff an immediate \c read() will return data. This + may or may not be the same as \c getSize() > 0, depending on the + stream type. + */ + virtual bool isReady() const = 0; + + //! Get bytes available to read + /*! + Returns a conservative estimate of the available bytes to read + (i.e. a number not greater than the actual number of bytes). + Some streams may not be able to determine this and will always + return zero. + */ + virtual UInt32 getSize() const = 0; + + //! Get input ready event type + /*! + Returns the input ready event type. A stream sends this event + when \c read() will return with data. + */ + virtual CEvent::Type getInputReadyEvent(); + + //! Get output flushed event type + /*! + Returns the output flushed event type. A stream sends this event + when the output buffer has been flushed. If there have been no + writes since the event was posted, calling \c shutdownOutput() or + \c close() will not discard any data and \c flush() will return + immediately. + */ + virtual CEvent::Type getOutputFlushedEvent(); + + //! Get output error event type + /*! + Returns the output error event type. A stream sends this event + when a write has failed. + */ + virtual CEvent::Type getOutputErrorEvent(); + + //! Get input shutdown event type + /*! + Returns the input shutdown event type. This is sent when the + input side of the stream has shutdown. When the input has + shutdown, no more data will ever be available to read. + */ + virtual CEvent::Type getInputShutdownEvent(); + + //! Get output shutdown event type + /*! + Returns the output shutdown event type. This is sent when the + output side of the stream has shutdown. When the output has + shutdown, no more data can ever be written to the stream. Any + attempt to do so will generate a output error event. + */ + virtual CEvent::Type getOutputShutdownEvent(); + + //@} + +private: + IEventQueue& m_eventQueue; + + static CEvent::Type s_inputReadyEvent; + static CEvent::Type s_outputFlushedEvent; + static CEvent::Type s_outputErrorEvent; + static CEvent::Type s_inputShutdownEvent; + static CEvent::Type s_outputShutdownEvent; +}; + +#endif diff --git a/src/lib/io/IStreamFilterFactory.h b/src/lib/io/IStreamFilterFactory.h new file mode 100644 index 00000000..449df64f --- /dev/null +++ b/src/lib/io/IStreamFilterFactory.h @@ -0,0 +1,39 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ISTREAMFILTERFACTORY_H +#define ISTREAMFILTERFACTORY_H + +#include "IInterface.h" + +class IStream; + +//! Stream filter factory interface +/*! +This interface provides factory methods to create stream filters. +*/ +class IStreamFilterFactory : public IInterface { +public: + //! Create filter + /*! + Create and return a stream filter on \p stream. The caller must + delete the returned object. + */ + virtual IStream* create(IStream* stream, bool adoptStream) = 0; +}; + +#endif diff --git a/src/lib/io/XIO.cpp b/src/lib/io/XIO.cpp new file mode 100644 index 00000000..ad0f6165 --- /dev/null +++ b/src/lib/io/XIO.cpp @@ -0,0 +1,50 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "XIO.h" + +// +// XIOClosed +// + +CString +XIOClosed::getWhat() const throw() +{ + return format("XIOClosed", "already closed"); +} + + +// +// XIOEndOfStream +// + +CString +XIOEndOfStream::getWhat() const throw() +{ + return format("XIOEndOfStream", "reached end of stream"); +} + + +// +// XIOWouldBlock +// + +CString +XIOWouldBlock::getWhat() const throw() +{ + return format("XIOWouldBlock", "stream operation would block"); +} diff --git a/src/lib/io/XIO.h b/src/lib/io/XIO.h new file mode 100644 index 00000000..9db729af --- /dev/null +++ b/src/lib/io/XIO.h @@ -0,0 +1,51 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef XIO_H +#define XIO_H + +#include "XBase.h" + +//! Generic I/O exception +XBASE_SUBCLASS(XIO, XBase); + +//! I/O closing exception +/*! +Thrown if a stream cannot be closed. +*/ +XBASE_SUBCLASS(XIOClose, XIO); + +//! I/O already closed exception +/*! +Thrown when attempting to close or perform I/O on an already closed. +stream. +*/ +XBASE_SUBCLASS_WHAT(XIOClosed, XIO); + +//! I/O end of stream exception +/*! +Thrown when attempting to read beyond the end of a stream. +*/ +XBASE_SUBCLASS_WHAT(XIOEndOfStream, XIO); + +//! I/O would block exception +/*! +Thrown if an operation on a stream would block. +*/ +XBASE_SUBCLASS_WHAT(XIOWouldBlock, XIO); + +#endif diff --git a/src/lib/mt/CCondVar.cpp b/src/lib/mt/CCondVar.cpp new file mode 100644 index 00000000..28fb9ae3 --- /dev/null +++ b/src/lib/mt/CCondVar.cpp @@ -0,0 +1,84 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CCondVar.h" +#include "CStopwatch.h" +#include "CArch.h" + +// +// CCondVarBase +// + +CCondVarBase::CCondVarBase(CMutex* mutex) : + m_mutex(mutex) +{ + assert(m_mutex != NULL); + m_cond = ARCH->newCondVar(); +} + +CCondVarBase::~CCondVarBase() +{ + ARCH->closeCondVar(m_cond); +} + +void +CCondVarBase::lock() const +{ + m_mutex->lock(); +} + +void +CCondVarBase::unlock() const +{ + m_mutex->unlock(); +} + +void +CCondVarBase::signal() +{ + ARCH->signalCondVar(m_cond); +} + +void +CCondVarBase::broadcast() +{ + ARCH->broadcastCondVar(m_cond); +} + +bool +CCondVarBase::wait(CStopwatch& timer, double timeout) const +{ + // check timeout against timer + if (timeout >= 0.0) { + timeout -= timer.getTime(); + if (timeout < 0.0) + return false; + } + return wait(timeout); +} + +bool +CCondVarBase::wait(double timeout) const +{ + return ARCH->waitCondVar(m_cond, m_mutex->m_mutex, timeout); +} + +CMutex* +CCondVarBase::getMutex() const +{ + return m_mutex; +} diff --git a/src/lib/mt/CCondVar.h b/src/lib/mt/CCondVar.h new file mode 100644 index 00000000..0ed1f576 --- /dev/null +++ b/src/lib/mt/CCondVar.h @@ -0,0 +1,227 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CCONDVAR_H +#define CCONDVAR_H + +#include "CMutex.h" +#include "BasicTypes.h" + +class CStopwatch; + +//! Generic condition variable +/*! +This class provides functionality common to all condition variables +but doesn't provide the actual variable storage. A condition variable +is a multiprocessing primitive that can be waited on. Every condition +variable has an associated mutex. +*/ +class CCondVarBase { +public: + /*! + \c mutex must not be NULL. All condition variables have an + associated mutex. The mutex needn't be unique to one condition + variable. + */ + CCondVarBase(CMutex* mutex); + ~CCondVarBase(); + + //! @name manipulators + //@{ + + //! Lock the condition variable's mutex + /*! + Lock the condition variable's mutex. The condition variable should + be locked before reading or writing it. It must be locked for a + call to wait(). Locks are not recursive; locking a locked mutex + will deadlock the thread. + */ + void lock() const; + + //! Unlock the condition variable's mutex + void unlock() const; + + //! Signal the condition variable + /*! + Wake up one waiting thread, if there are any. Which thread gets + woken is undefined. + */ + void signal(); + + //! Signal the condition variable + /*! + Wake up all waiting threads, if any. + */ + void broadcast(); + + //@} + //! @name accessors + //@{ + + //! Wait on the condition variable + /*! + Wait on the condition variable. If \c timeout < 0 then wait until + signalled, otherwise up to \c timeout seconds or until signalled, + whichever comes first. Returns true if the object was signalled + during the wait, false otherwise. + + The proper way to wait for a condition is: + \code + cv.lock(); + while (cv-expr) { + cv.wait(); + } + cv.unlock(); + \endcode + where \c cv-expr involves the value of \c cv and is false when the + condition is satisfied. + + (cancellation point) + */ + bool wait(double timeout = -1.0) const; + + //! Wait on the condition variable + /*! + Same as \c wait(double) but use \c timer to compare against \c timeout. + Since clients normally wait on condition variables in a loop, clients + can use this to avoid recalculating \c timeout on each iteration. + Passing a stopwatch with a negative \c timeout is pointless (it will + never time out) but permitted. + + (cancellation point) + */ + bool wait(CStopwatch& timer, double timeout) const; + + //! Get the mutex + /*! + Get the mutex passed to the c'tor. + */ + CMutex* getMutex() const; + + //@} + +private: + // not implemented + CCondVarBase(const CCondVarBase&); + CCondVarBase& operator=(const CCondVarBase&); + +private: + CMutex* m_mutex; + CArchCond m_cond; +}; + +//! Condition variable +/*! +A condition variable with storage for type \c T. +*/ +template +class CCondVar : public CCondVarBase { +public: + //! Initialize using \c value + CCondVar(CMutex* mutex, const T& value); + //! Initialize using another condition variable's value + CCondVar(const CCondVar&); + ~CCondVar(); + + //! @name manipulators + //@{ + + //! Assigns the value of \c cv to this + /*! + Set the variable's value. The condition variable should be locked + before calling this method. + */ + CCondVar& operator=(const CCondVar& cv); + + //! Assigns \c value to this + /*! + Set the variable's value. The condition variable should be locked + before calling this method. + */ + CCondVar& operator=(const T& v); + + //@} + //! @name accessors + //@{ + + //! Get the variable's value + /*! + Get the variable's value. The condition variable should be locked + before calling this method. + */ + operator const volatile T&() const; + + //@} + +private: + volatile T m_data; +}; + +template +inline +CCondVar::CCondVar( + CMutex* mutex, + const T& data) : + CCondVarBase(mutex), + m_data(data) +{ + // do nothing +} + +template +inline +CCondVar::CCondVar( + const CCondVar& cv) : + CCondVarBase(cv.getMutex()), + m_data(cv.m_data) +{ + // do nothing +} + +template +inline +CCondVar::~CCondVar() +{ + // do nothing +} + +template +inline +CCondVar& +CCondVar::operator=(const CCondVar& cv) +{ + m_data = cv.m_data; + return *this; +} + +template +inline +CCondVar& +CCondVar::operator=(const T& data) +{ + m_data = data; + return *this; +} + +template +inline +CCondVar::operator const volatile T&() const +{ + return m_data; +} + +#endif diff --git a/src/lib/mt/CLock.cpp b/src/lib/mt/CLock.cpp new file mode 100644 index 00000000..d4f1498c --- /dev/null +++ b/src/lib/mt/CLock.cpp @@ -0,0 +1,41 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CLock.h" +#include "CCondVar.h" +#include "CMutex.h" + +// +// CLock +// + +CLock::CLock(const CMutex* mutex) : + m_mutex(mutex) +{ + m_mutex->lock(); +} + +CLock::CLock(const CCondVarBase* cv) : + m_mutex(cv->getMutex()) +{ + m_mutex->lock(); +} + +CLock::~CLock() +{ + m_mutex->unlock(); +} diff --git a/src/lib/mt/CLock.h b/src/lib/mt/CLock.h new file mode 100644 index 00000000..445159e0 --- /dev/null +++ b/src/lib/mt/CLock.h @@ -0,0 +1,51 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CLOCK_H +#define CLOCK_H + +#include "common.h" + +class CMutex; +class CCondVarBase; + +//! Mutual exclusion lock utility +/*! +This class locks a mutex or condition variable in the c'tor and unlocks +it in the d'tor. It's easier and safer than manually locking and +unlocking since unlocking must usually be done no matter how a function +exits (including by unwinding due to an exception). +*/ +class CLock { +public: + //! Lock the mutex \c mutex + CLock(const CMutex* mutex); + //! Lock the condition variable \c cv + CLock(const CCondVarBase* cv); + //! Unlock the mutex or condition variable + ~CLock(); + +private: + // not implemented + CLock(const CLock&); + CLock& operator=(const CLock&); + +private: + const CMutex* m_mutex; +}; + +#endif diff --git a/src/lib/mt/CMakeLists.txt b/src/lib/mt/CMakeLists.txt new file mode 100644 index 00000000..e42ee7d9 --- /dev/null +++ b/src/lib/mt/CMakeLists.txt @@ -0,0 +1,51 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(inc + CCondVar.h + CLock.h + CMutex.h + CThread.h + XMT.h + XThread.h +) + +set(src + CCondVar.cpp + CLock.cpp + CMutex.cpp + CThread.cpp + XMT.cpp +) + +if (WIN32) + list(APPEND src ${inc}) +endif() + +set(inc + ../arch + ../base + ../common + ../synergy +) + +if (UNIX) + list(APPEND inc + ../../.. + ) +endif() + +include_directories(${inc}) +add_library(mt STATIC ${src}) diff --git a/src/lib/mt/CMutex.cpp b/src/lib/mt/CMutex.cpp new file mode 100644 index 00000000..fe07b381 --- /dev/null +++ b/src/lib/mt/CMutex.cpp @@ -0,0 +1,56 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMutex.h" +#include "CArch.h" + +// +// CMutex +// + +CMutex::CMutex() +{ + m_mutex = ARCH->newMutex(); +} + +CMutex::CMutex(const CMutex&) +{ + m_mutex = ARCH->newMutex(); +} + +CMutex::~CMutex() +{ + ARCH->closeMutex(m_mutex); +} + +CMutex& +CMutex::operator=(const CMutex&) +{ + return *this; +} + +void +CMutex::lock() const +{ + ARCH->lockMutex(m_mutex); +} + +void +CMutex::unlock() const +{ + ARCH->unlockMutex(m_mutex); +} diff --git a/src/lib/mt/CMutex.h b/src/lib/mt/CMutex.h new file mode 100644 index 00000000..6976a583 --- /dev/null +++ b/src/lib/mt/CMutex.h @@ -0,0 +1,81 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMUTEX_H +#define CMUTEX_H + +#include "IArchMultithread.h" + +//! Mutual exclusion +/*! +A non-recursive mutual exclusion object. Only one thread at a time can +hold a lock on a mutex. Any thread that attempts to lock a locked mutex +will block until the mutex is unlocked. At that time, if any threads are +blocked, exactly one waiting thread will acquire the lock and continue +running. A thread may not lock a mutex it already owns the lock on; if +it tries it will deadlock itself. +*/ +class CMutex { +public: + CMutex(); + //! Equivalent to default c'tor + /*! + Copy c'tor doesn't copy anything. It just makes it possible to + copy objects that contain a mutex. + */ + CMutex(const CMutex&); + ~CMutex(); + + //! @name manipulators + //@{ + + //! Does nothing + /*! + This does nothing. It just makes it possible to assign objects + that contain a mutex. + */ + CMutex& operator=(const CMutex&); + + //@} + //! @name accessors + //@{ + + //! Lock the mutex + /*! + Locks the mutex, which must not have been previously locked by the + calling thread. This blocks if the mutex is already locked by another + thread. + + (cancellation point) + */ + void lock() const; + + //! Unlock the mutex + /*! + Unlocks the mutex, which must have been previously locked by the + calling thread. + */ + void unlock() const; + + //@} + +private: + friend class CCondVarBase; + CArchMutex m_mutex; +}; + +#endif diff --git a/src/lib/mt/CThread.cpp b/src/lib/mt/CThread.cpp new file mode 100644 index 00000000..23e5f0ff --- /dev/null +++ b/src/lib/mt/CThread.cpp @@ -0,0 +1,186 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CThread.h" +#include "XMT.h" +#include "XThread.h" +#include "CLog.h" +#include "IJob.h" +#include "CArch.h" + +// +// CThread +// + +CThread::CThread(IJob* job) +{ + m_thread = ARCH->newThread(&CThread::threadFunc, job); + if (m_thread == NULL) { + // couldn't create thread + delete job; + throw XMTThreadUnavailable(); + } +} + +CThread::CThread(const CThread& thread) +{ + m_thread = ARCH->copyThread(thread.m_thread); +} + +CThread::CThread(CArchThread adoptedThread) +{ + m_thread = adoptedThread; +} + +CThread::~CThread() +{ + ARCH->closeThread(m_thread); +} + +CThread& +CThread::operator=(const CThread& thread) +{ + // copy given thread and release ours + CArchThread copy = ARCH->copyThread(thread.m_thread); + ARCH->closeThread(m_thread); + + // cut over + m_thread = copy; + + return *this; +} + +void +CThread::exit(void* result) +{ + throw XThreadExit(result); +} + +void +CThread::cancel() +{ + ARCH->cancelThread(m_thread); +} + +void +CThread::setPriority(int n) +{ + ARCH->setPriorityOfThread(m_thread, n); +} + +void +CThread::unblockPollSocket() +{ + ARCH->unblockPollSocket(m_thread); +} + +CThread +CThread::getCurrentThread() +{ + return CThread(ARCH->newCurrentThread()); +} + +void +CThread::testCancel() +{ + ARCH->testCancelThread(); +} + +bool +CThread::wait(double timeout) const +{ + return ARCH->wait(m_thread, timeout); +} + +void* +CThread::getResult() const +{ + if (wait()) + return ARCH->getResultOfThread(m_thread); + else + return NULL; +} + +IArchMultithread::ThreadID +CThread::getID() const +{ + return ARCH->getIDOfThread(m_thread); +} + +bool +CThread::operator==(const CThread& thread) const +{ + return ARCH->isSameThread(m_thread, thread.m_thread); +} + +bool +CThread::operator!=(const CThread& thread) const +{ + return !ARCH->isSameThread(m_thread, thread.m_thread); +} + +void* +CThread::threadFunc(void* vjob) +{ + // get this thread's id for logging + IArchMultithread::ThreadID id; + { + CArchThread thread = ARCH->newCurrentThread(); + id = ARCH->getIDOfThread(thread); + ARCH->closeThread(thread); + } + + // get job + IJob* job = reinterpret_cast(vjob); + + // run job + void* result = NULL; + try { + // go + LOG((CLOG_DEBUG1 "thread 0x%08x entry", id)); + job->run(); + LOG((CLOG_DEBUG1 "thread 0x%08x exit", id)); + } + + catch (XThreadCancel&) { + // client called cancel() + LOG((CLOG_DEBUG1 "caught cancel on thread 0x%08x", id)); + delete job; + throw; + } + catch (XThreadExit& e) { + // client called exit() + result = e.m_result; + LOG((CLOG_DEBUG1 "caught exit on thread 0x%08x, result %p", id, result)); + } + catch (XBase& e) { + LOG((CLOG_ERR "exception on thread 0x%08x: %s", id, e.what())); + delete job; + throw; + } + catch (...) { + LOG((CLOG_ERR "exception on thread 0x%08x: ", id)); + delete job; + throw; + } + + // done with job + delete job; + + // return exit result + return result; +} diff --git a/src/lib/mt/CThread.h b/src/lib/mt/CThread.h new file mode 100644 index 00000000..b31cf623 --- /dev/null +++ b/src/lib/mt/CThread.h @@ -0,0 +1,212 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CTHREAD_H +#define CTHREAD_H + +#include "IArchMultithread.h" + +class IJob; + +//! Thread handle +/*! +Creating a CThread creates a new context of execution (i.e. thread) that +runs simulatenously with the calling thread. A CThread is only a handle +to a thread; deleting a CThread does not cancel or destroy the thread it +refers to and multiple CThread objects can refer to the same thread. + +Threads can terminate themselves but cannot be forced to terminate by +other threads. However, other threads can signal a thread to terminate +itself by cancelling it. And a thread can wait (block) on another thread +to terminate. + +Most functions that can block for an arbitrary time are cancellation +points. A cancellation point is a function that can be interrupted by +a request to cancel the thread. Cancellation points are noted in the +documentation. +*/ +// note -- do not derive from this class +class CThread { +public: + //! Run \c adoptedJob in a new thread + /*! + Create and start a new thread executing the \c adoptedJob. The + new thread takes ownership of \c adoptedJob and will delete it. + */ + CThread(IJob* adoptedJob); + + //! Duplicate a thread handle + /*! + Make a new thread object that refers to an existing thread. + This does \b not start a new thread. + */ + CThread(const CThread&); + + //! Release a thread handle + /*! + Release a thread handle. This does not terminate the thread. A thread + will keep running until the job completes or calls exit() or allows + itself to be cancelled. + */ + ~CThread(); + + //! @name manipulators + //@{ + + //! Assign thread handle + /*! + Assign a thread handle. This has no effect on the threads, it simply + makes this thread object refer to another thread. It does \b not + start a new thread. + */ + CThread& operator=(const CThread&); + + //! Terminate the calling thread + /*! + Terminate the calling thread. This function does not return but + the stack is unwound and automatic objects are destroyed, as if + exit() threw an exception (which is, in fact, what it does). The + argument is saved as the result returned by getResult(). If you + have \c catch(...) blocks then you should add the following before + each to avoid catching the exit: + \code + catch(CThreadExit&) { throw; } + \endcode + or add the \c RETHROW_XTHREAD macro to the \c catch(...) block. + */ + static void exit(void*); + + //! Cancel thread + /*! + Cancel the thread. cancel() never waits for the thread to + terminate; it just posts the cancel and returns. A thread will + terminate when it enters a cancellation point with cancellation + enabled. If cancellation is disabled then the cancel is + remembered but not acted on until the first call to a + cancellation point after cancellation is enabled. + + A cancellation point is a function that can act on cancellation. + A cancellation point does not return if there's a cancel pending. + Instead, it unwinds the stack and destroys automatic objects, as + if cancel() threw an exception (which is, in fact, what it does). + Threads must take care to unlock and clean up any resources they + may have, especially mutexes. They can \c catch(XThreadCancel) to + do that then rethrow the exception or they can let it happen + automatically by doing clean up in the d'tors of automatic + objects (like CLock). Clients are strongly encouraged to do the latter. + During cancellation, further cancel() calls are ignored (i.e. + a thread cannot be interrupted by a cancel during cancellation). + + Clients that \c catch(XThreadCancel) must always rethrow the + exception. Clients that \c catch(...) must either rethrow the + exception or include a \c catch(XThreadCancel) handler that + rethrows. The \c RETHROW_XTHREAD macro may be useful for that. + */ + void cancel(); + + //! Change thread priority + /*! + Change the priority of the thread. Normal priority is 0, 1 is + the next lower, etc. -1 is the next higher, etc. but boosting + the priority may not be permitted and will be silenty ignored. + */ + void setPriority(int n); + + //! Force pollSocket() to return + /*! + Forces a currently blocked pollSocket() in the thread to return + immediately. + */ + void unblockPollSocket(); + + //@} + //! @name accessors + //@{ + + //! Get current thread's handle + /*! + Return a CThread object representing the calling thread. + */ + static CThread getCurrentThread(); + + //! Test for cancellation + /*! + testCancel() does nothing but is a cancellation point. Call + this to make a function itself a cancellation point. If the + thread was cancelled and cancellation is enabled this will + cause the thread to unwind the stack and terminate. + + (cancellation point) + */ + static void testCancel(); + + //! Wait for thread to terminate + /*! + Waits for the thread to terminate (by exit() or cancel() or + by returning from the thread job) for up to \c timeout seconds, + returning true if the thread terminated and false otherwise. + This returns immediately with false if called by a thread on + itself and immediately with true if the thread has already + terminated. This will wait forever if \c timeout < 0.0. + + (cancellation point) + */ + bool wait(double timeout = -1.0) const; + + //! Get the exit result + /*! + Returns the exit result. This does an implicit wait(). It returns + NULL immediately if called by a thread on itself or on a thread that + was cancelled. + + (cancellation point) + */ + void* getResult() const; + + //! Get the thread id + /*! + Returns an integer id for this thread. This id must not be used to + check if two CThread objects refer to the same thread. Use + operator==() for that. + */ + IArchMultithread::ThreadID + getID() const; + + //! Compare thread handles + /*! + Returns true if two CThread objects refer to the same thread. + */ + bool operator==(const CThread&) const; + + //! Compare thread handles + /*! + Returns true if two CThread objects do not refer to the same thread. + */ + bool operator!=(const CThread&) const; + + //@} + +private: + CThread(CArchThread); + + static void* threadFunc(void*); + +private: + CArchThread m_thread; +}; + +#endif diff --git a/src/lib/mt/XMT.cpp b/src/lib/mt/XMT.cpp new file mode 100644 index 00000000..d4259b74 --- /dev/null +++ b/src/lib/mt/XMT.cpp @@ -0,0 +1,28 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "XMT.h" + +// +// XMTThreadUnavailable +// + +CString +XMTThreadUnavailable::getWhat() const throw() +{ + return format("XMTThreadUnavailable", "cannot create thread"); +} diff --git a/src/lib/mt/XMT.h b/src/lib/mt/XMT.h new file mode 100644 index 00000000..9b805c20 --- /dev/null +++ b/src/lib/mt/XMT.h @@ -0,0 +1,32 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef XMT_H +#define XMT_H + +#include "XBase.h" + +//! Generic multithreading exception +XBASE_SUBCLASS(XMT, XBase); + +//! Thread creation exception +/*! +Thrown when a thread cannot be created. +*/ +XBASE_SUBCLASS_WHAT(XMTThreadUnavailable, XMT); + +#endif diff --git a/src/lib/mt/XThread.h b/src/lib/mt/XThread.h new file mode 100644 index 00000000..47ec8283 --- /dev/null +++ b/src/lib/mt/XThread.h @@ -0,0 +1,39 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef XTHREAD_H +#define XTHREAD_H + +#include "XArch.h" + +//! Thread exception to exit +/*! +Thrown by CThread::exit() to exit a thread. Clients of CThread +must not throw this type but must rethrow it if caught (by +XThreadExit, XThread, or ...). +*/ +class XThreadExit : public XThread { +public: + //! \c result is the result of the thread + XThreadExit(void* result) : m_result(result) { } + ~XThreadExit() { } + +public: + void* m_result; +}; + +#endif diff --git a/src/lib/net/CMakeLists.txt b/src/lib/net/CMakeLists.txt new file mode 100644 index 00000000..5736a892 --- /dev/null +++ b/src/lib/net/CMakeLists.txt @@ -0,0 +1,67 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(inc + CNetworkAddress.h + CSocketMultiplexer.h + CTCPListenSocket.h + CTCPSocket.h + CTCPSocketFactory.h + IDataSocket.h + IListenSocket.h + ISocket.h + ISocketFactory.h + ISocketMultiplexerJob.h + TSocketMultiplexerMethodJob.h + XSocket.h +) + +set(src + CNetworkAddress.cpp + CSocketMultiplexer.cpp + CTCPListenSocket.cpp + CTCPSocket.cpp + CTCPSocketFactory.cpp + IDataSocket.cpp + IListenSocket.cpp + ISocket.cpp + XSocket.cpp +) + +if (WIN32) + list(APPEND src ${inc}) +endif() + +set(inc + ../arch + ../base + ../common + ../io + ../mt + ../synergy +) + +if (UNIX) + list(APPEND inc + ../../.. + ) +endif() + +include_directories(${inc}) +add_library(net STATIC ${src}) + +if (UNIX) + target_link_libraries(net mt io) +endif() diff --git a/src/lib/net/CNetworkAddress.cpp b/src/lib/net/CNetworkAddress.cpp new file mode 100644 index 00000000..0bb2b30e --- /dev/null +++ b/src/lib/net/CNetworkAddress.cpp @@ -0,0 +1,211 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CNetworkAddress.h" +#include "XSocket.h" +#include "CArch.h" +#include "XArch.h" +#include + +// +// CNetworkAddress +// + +// name re-resolution adapted from a patch by Brent Priddy. + +CNetworkAddress::CNetworkAddress() : + m_address(NULL), + m_hostname(), + m_port(0) +{ + // note -- make no calls to CNetwork socket interface here; + // we're often called prior to CNetwork::init(). +} + +CNetworkAddress::CNetworkAddress(int port) : + m_address(NULL), + m_hostname(), + m_port(port) +{ + checkPort(); + m_address = ARCH->newAnyAddr(IArchNetwork::kINET); + ARCH->setAddrPort(m_address, m_port); +} + +CNetworkAddress::CNetworkAddress(const CNetworkAddress& addr) : + m_address(addr.m_address != NULL ? ARCH->copyAddr(addr.m_address) : NULL), + m_hostname(addr.m_hostname), + m_port(addr.m_port) +{ + // do nothing +} + +CNetworkAddress::CNetworkAddress(const CString& hostname, int port) : + m_address(NULL), + m_hostname(hostname), + m_port(port) +{ + // check for port suffix + CString::size_type i = m_hostname.rfind(':'); + if (i != CString::npos && i + 1 < m_hostname.size()) { + // found a colon. see if it looks like an IPv6 address. + bool colonNotation = false; + bool dotNotation = false; + bool doubleColon = false; + for (CString::size_type j = 0; j < i; ++j) { + if (m_hostname[j] == ':') { + colonNotation = true; + dotNotation = false; + if (m_hostname[j + 1] == ':') { + doubleColon = true; + } + } + else if (m_hostname[j] == '.' && colonNotation) { + dotNotation = true; + } + } + + // port suffix is ambiguous with IPv6 notation if there's + // a double colon and the end of the address is not in dot + // notation. in that case we assume it's not a port suffix. + // the user can replace the double colon with zeros to + // disambiguate. + if ((!doubleColon || dotNotation) || !colonNotation) { + // parse port from hostname + char* end; + const char* chostname = m_hostname.c_str(); + long suffixPort = strtol(chostname + i + 1, &end, 10); + if (end == chostname + i + 1 || *end != '\0') { + throw XSocketAddress(XSocketAddress::kBadPort, + m_hostname, m_port); + } + + // trim port from hostname + m_hostname.erase(i); + + // save port + m_port = static_cast(suffixPort); + } + } + + // check port number + checkPort(); +} + +CNetworkAddress::~CNetworkAddress() +{ + if (m_address != NULL) { + ARCH->closeAddr(m_address); + } +} + +CNetworkAddress& +CNetworkAddress::operator=(const CNetworkAddress& addr) +{ + CArchNetAddress newAddr = NULL; + if (addr.m_address != NULL) { + newAddr = ARCH->copyAddr(addr.m_address); + } + if (m_address != NULL) { + ARCH->closeAddr(m_address); + } + m_address = newAddr; + m_hostname = addr.m_hostname; + m_port = addr.m_port; + return *this; +} + +void +CNetworkAddress::resolve() +{ + // discard previous address + if (m_address != NULL) { + ARCH->closeAddr(m_address); + m_address = NULL; + } + + try { + // if hostname is empty then use wildcard address otherwise look + // up the name. + if (m_hostname.empty()) { + m_address = ARCH->newAnyAddr(IArchNetwork::kINET); + } + else { + m_address = ARCH->nameToAddr(m_hostname); + } + } + catch (XArchNetworkNameUnknown&) { + throw XSocketAddress(XSocketAddress::kNotFound, m_hostname, m_port); + } + catch (XArchNetworkNameNoAddress&) { + throw XSocketAddress(XSocketAddress::kNoAddress, m_hostname, m_port); + } + catch (XArchNetworkNameUnsupported&) { + throw XSocketAddress(XSocketAddress::kUnsupported, m_hostname, m_port); + } + catch (XArchNetworkName&) { + throw XSocketAddress(XSocketAddress::kUnknown, m_hostname, m_port); + } + + // set port in address + ARCH->setAddrPort(m_address, m_port); +} + +bool +CNetworkAddress::operator==(const CNetworkAddress& addr) const +{ + return ARCH->isEqualAddr(m_address, addr.m_address); +} + +bool +CNetworkAddress::operator!=(const CNetworkAddress& addr) const +{ + return !operator==(addr); +} + +bool +CNetworkAddress::isValid() const +{ + return (m_address != NULL); +} + +const CArchNetAddress& +CNetworkAddress::getAddress() const +{ + return m_address; +} + +int +CNetworkAddress::getPort() const +{ + return m_port; +} + +CString +CNetworkAddress::getHostname() const +{ + return m_hostname; +} + +void +CNetworkAddress::checkPort() +{ + // check port number + if (m_port <= 0 || m_port > 65535) { + throw XSocketAddress(XSocketAddress::kBadPort, m_hostname, m_port); + } +} diff --git a/src/lib/net/CNetworkAddress.h b/src/lib/net/CNetworkAddress.h new file mode 100644 index 00000000..d78cc538 --- /dev/null +++ b/src/lib/net/CNetworkAddress.h @@ -0,0 +1,125 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CNETWORKADDRESS_H +#define CNETWORKADDRESS_H + +#include "CString.h" +#include "BasicTypes.h" +#include "IArchNetwork.h" + +//! Network address type +/*! +This class represents a network address. +*/ +class CNetworkAddress { +public: + /*! + Constructs the invalid address + */ + CNetworkAddress(); + + /*! + Construct the wildcard address with the given port. \c port must + not be zero. + */ + CNetworkAddress(int port); + + /*! + Construct the network address for the given \c hostname and \c port. + If \c hostname can be parsed as a numerical address then that's how + it's used, otherwise it's used as a host name. If \c hostname ends + in ":[0-9]+" then that suffix is extracted and used as the port, + overridding the port parameter. The resulting port must be a valid + port number (zero is not a valid port number) otherwise \c XSocketAddress + is thrown with an error of \c XSocketAddress::kBadPort. The hostname + is not resolved by the c'tor; use \c resolve to do that. + */ + CNetworkAddress(const CString& hostname, int port); + + CNetworkAddress(const CNetworkAddress&); + + ~CNetworkAddress(); + + CNetworkAddress& operator=(const CNetworkAddress&); + + //! @name manipulators + //@{ + + //! Resolve address + /*! + Resolves the hostname to an address. This can be done any number of + times and is done automatically by the c'tor taking a hostname. + Throws XSocketAddress if resolution is unsuccessful, after which + \c isValid returns false until the next call to this method. + */ + void resolve(); + + //@} + //! @name accessors + //@{ + + //! Check address equality + /*! + Returns true if this address is equal to \p address. + */ + bool operator==(const CNetworkAddress& address) const; + + //! Check address inequality + /*! + Returns true if this address is not equal to \p address. + */ + bool operator!=(const CNetworkAddress& address) const; + + //! Check address validity + /*! + Returns true if this is not the invalid address. + */ + bool isValid() const; + + //! Get address + /*! + Returns the address in the platform's native network address + structure. + */ + const CArchNetAddress& getAddress() const; + + //! Get port + /*! + Returns the port passed to the c'tor as a suffix to the hostname, + if that existed, otherwise as passed directly to the c'tor. + */ + int getPort() const; + + //! Get hostname + /*! + Returns the hostname passed to the c'tor sans any port suffix. + */ + CString getHostname() const; + + //@} + +private: + void checkPort(); + +private: + CArchNetAddress m_address; + CString m_hostname; + int m_port; +}; + +#endif diff --git a/src/lib/net/CSocketMultiplexer.cpp b/src/lib/net/CSocketMultiplexer.cpp new file mode 100644 index 00000000..76808bd7 --- /dev/null +++ b/src/lib/net/CSocketMultiplexer.cpp @@ -0,0 +1,362 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CSocketMultiplexer.h" +#include "ISocketMultiplexerJob.h" +#include "CCondVar.h" +#include "CLock.h" +#include "CMutex.h" +#include "CThread.h" +#include "CLog.h" +#include "TMethodJob.h" +#include "CArch.h" +#include "XArch.h" +#include "stdvector.h" + +// +// CSocketMultiplexer +// + +CSocketMultiplexer* CSocketMultiplexer::s_instance = NULL; + +CSocketMultiplexer::CSocketMultiplexer() : + m_mutex(new CMutex), + m_thread(NULL), + m_update(false), + m_jobsReady(new CCondVar(m_mutex, false)), + m_jobListLock(new CCondVar(m_mutex, false)), + m_jobListLockLocked(new CCondVar(m_mutex, false)), + m_jobListLocker(NULL), + m_jobListLockLocker(NULL) +{ + assert(s_instance == NULL); + + // this pointer just has to be unique and not NULL. it will + // never be dereferenced. it's used to identify cursor nodes + // in the jobs list. + m_cursorMark = reinterpret_cast(this); + + // start thread + m_thread = new CThread(new TMethodJob( + this, &CSocketMultiplexer::serviceThread)); + + s_instance = this; +} + +CSocketMultiplexer::~CSocketMultiplexer() +{ + m_thread->cancel(); + m_thread->unblockPollSocket(); + m_thread->wait(); + delete m_thread; + delete m_jobsReady; + delete m_jobListLock; + delete m_jobListLockLocked; + delete m_jobListLocker; + delete m_jobListLockLocker; + delete m_mutex; + + // clean up jobs + for (CSocketJobMap::iterator i = m_socketJobMap.begin(); + i != m_socketJobMap.end(); ++i) { + delete *(i->second); + } + + s_instance = NULL; +} + +CSocketMultiplexer* +CSocketMultiplexer::getInstance() +{ + return s_instance; +} + +void +CSocketMultiplexer::addSocket(ISocket* socket, ISocketMultiplexerJob* job) +{ + assert(socket != NULL); + assert(job != NULL); + + // prevent other threads from locking the job list + lockJobListLock(); + + // break thread out of poll + m_thread->unblockPollSocket(); + + // lock the job list + lockJobList(); + + // insert/replace job + CSocketJobMap::iterator i = m_socketJobMap.find(socket); + if (i == m_socketJobMap.end()) { + // we *must* put the job at the end so the order of jobs in + // the list continue to match the order of jobs in pfds in + // serviceThread(). + CJobCursor j = m_socketJobs.insert(m_socketJobs.end(), job); + m_update = true; + m_socketJobMap.insert(std::make_pair(socket, j)); + } + else { + CJobCursor j = i->second; + if (*j != job) { + delete *j; + *j = job; + } + m_update = true; + } + + // unlock the job list + unlockJobList(); +} + +void +CSocketMultiplexer::removeSocket(ISocket* socket) +{ + assert(socket != NULL); + + // prevent other threads from locking the job list + lockJobListLock(); + + // break thread out of poll + m_thread->unblockPollSocket(); + + // lock the job list + lockJobList(); + + // remove job. rather than removing it from the map we put NULL + // in the list instead so the order of jobs in the list continues + // to match the order of jobs in pfds in serviceThread(). + CSocketJobMap::iterator i = m_socketJobMap.find(socket); + if (i != m_socketJobMap.end()) { + if (*(i->second) != NULL) { + delete *(i->second); + *(i->second) = NULL; + m_update = true; + } + } + + // unlock the job list + unlockJobList(); +} + +void +CSocketMultiplexer::serviceThread(void*) +{ + std::vector pfds; + IArchNetwork::CPollEntry pfd; + + // service the connections + for (;;) { + CThread::testCancel(); + + // wait until there are jobs to handle + { + CLock lock(m_mutex); + while (!(bool)*m_jobsReady) { + m_jobsReady->wait(); + } + } + + // lock the job list + lockJobListLock(); + lockJobList(); + + // collect poll entries + if (m_update) { + m_update = false; + pfds.clear(); + pfds.reserve(m_socketJobMap.size()); + + CJobCursor cursor = newCursor(); + CJobCursor jobCursor = nextCursor(cursor); + while (jobCursor != m_socketJobs.end()) { + ISocketMultiplexerJob* job = *jobCursor; + if (job != NULL) { + pfd.m_socket = job->getSocket(); + pfd.m_events = 0; + if (job->isReadable()) { + pfd.m_events |= IArchNetwork::kPOLLIN; + } + if (job->isWritable()) { + pfd.m_events |= IArchNetwork::kPOLLOUT; + } + pfds.push_back(pfd); + } + jobCursor = nextCursor(cursor); + } + deleteCursor(cursor); + } + + int status; + try { + // check for status + if (!pfds.empty()) { + status = ARCH->pollSocket(&pfds[0], (int)pfds.size(), -1); + } + else { + status = 0; + } + } + catch (XArchNetwork& e) { + LOG((CLOG_WARN "error in socket multiplexer: %s", e.what().c_str())); + status = 0; + } + + if (status != 0) { + // iterate over socket jobs, invoking each and saving the + // new job. + UInt32 i = 0; + CJobCursor cursor = newCursor(); + CJobCursor jobCursor = nextCursor(cursor); + while (i < pfds.size() && jobCursor != m_socketJobs.end()) { + if (*jobCursor != NULL) { + // get poll state + unsigned short revents = pfds[i].m_revents; + bool read = ((revents & IArchNetwork::kPOLLIN) != 0); + bool write = ((revents & IArchNetwork::kPOLLOUT) != 0); + bool error = ((revents & (IArchNetwork::kPOLLERR | + IArchNetwork::kPOLLNVAL)) != 0); + + // run job + ISocketMultiplexerJob* job = *jobCursor; + ISocketMultiplexerJob* newJob = job->run(read, write, error); + + // save job, if different + if (newJob != job) { + CLock lock(m_mutex); + delete job; + *jobCursor = newJob; + m_update = true; + } + ++i; + } + + // next job + jobCursor = nextCursor(cursor); + } + deleteCursor(cursor); + } + + // delete any removed socket jobs + for (CSocketJobMap::iterator i = m_socketJobMap.begin(); + i != m_socketJobMap.end();) { + if (*(i->second) == NULL) { + m_socketJobMap.erase(i++); + m_update = true; + } + else { + ++i; + } + } + + // unlock the job list + unlockJobList(); + } +} + +CSocketMultiplexer::CJobCursor +CSocketMultiplexer::newCursor() +{ + CLock lock(m_mutex); + return m_socketJobs.insert(m_socketJobs.begin(), m_cursorMark); +} + +CSocketMultiplexer::CJobCursor +CSocketMultiplexer::nextCursor(CJobCursor cursor) +{ + CLock lock(m_mutex); + CJobCursor j = m_socketJobs.end(); + CJobCursor i = cursor; + while (++i != m_socketJobs.end()) { + if (*i != m_cursorMark) { + // found a real job (as opposed to a cursor) + j = i; + + // move our cursor just past the job + m_socketJobs.splice(++i, m_socketJobs, cursor); + break; + } + } + return j; +} + +void +CSocketMultiplexer::deleteCursor(CJobCursor cursor) +{ + CLock lock(m_mutex); + m_socketJobs.erase(cursor); +} + +void +CSocketMultiplexer::lockJobListLock() +{ + CLock lock(m_mutex); + + // wait for the lock on the lock + while (*m_jobListLockLocked) { + m_jobListLockLocked->wait(); + } + + // take ownership of the lock on the lock + *m_jobListLockLocked = true; + m_jobListLockLocker = new CThread(CThread::getCurrentThread()); +} + +void +CSocketMultiplexer::lockJobList() +{ + CLock lock(m_mutex); + + // make sure we're the one that called lockJobListLock() + assert(*m_jobListLockLocker == CThread::getCurrentThread()); + + // wait for the job list lock + while (*m_jobListLock) { + m_jobListLock->wait(); + } + + // take ownership of the lock + *m_jobListLock = true; + m_jobListLocker = m_jobListLockLocker; + m_jobListLockLocker = NULL; + + // release the lock on the lock + *m_jobListLockLocked = false; + m_jobListLockLocked->broadcast(); +} + +void +CSocketMultiplexer::unlockJobList() +{ + CLock lock(m_mutex); + + // make sure we're the one that called lockJobList() + assert(*m_jobListLocker == CThread::getCurrentThread()); + + // release the lock + delete m_jobListLocker; + m_jobListLocker = NULL; + *m_jobListLock = false; + m_jobListLock->signal(); + + // set new jobs ready state + bool isReady = !m_socketJobMap.empty(); + if (*m_jobsReady != isReady) { + *m_jobsReady = isReady; + m_jobsReady->signal(); + } +} diff --git a/src/lib/net/CSocketMultiplexer.h b/src/lib/net/CSocketMultiplexer.h new file mode 100644 index 00000000..a92ace17 --- /dev/null +++ b/src/lib/net/CSocketMultiplexer.h @@ -0,0 +1,114 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CSOCKETMULTIPLEXER_H +#define CSOCKETMULTIPLEXER_H + +#include "IArchNetwork.h" +#include "stdlist.h" +#include "stdmap.h" + +template +class CCondVar; +class CMutex; +class CThread; +class ISocket; +class ISocketMultiplexerJob; + +//! Socket multiplexer +/*! +A socket multiplexer services multiple sockets simultaneously. +*/ +class CSocketMultiplexer { +public: + CSocketMultiplexer(); + ~CSocketMultiplexer(); + + //! @name manipulators + //@{ + + void addSocket(ISocket*, ISocketMultiplexerJob*); + + void removeSocket(ISocket*); + + //@} + //! @name accessors + //@{ + + // maybe belongs on ISocketMultiplexer + static CSocketMultiplexer* + getInstance(); + + //@} + +private: + // list of jobs. we use a list so we can safely iterate over it + // while other threads modify it. + typedef std::list CSocketJobs; + typedef CSocketJobs::iterator CJobCursor; + typedef std::map CSocketJobMap; + + // service sockets. the service thread will only access m_sockets + // and m_update while m_pollable and m_polling are true. all other + // threads must only modify these when m_pollable and m_polling are + // false. only the service thread sets m_polling. + void serviceThread(void*); + + // create, iterate, and destroy a cursor. a cursor is used to + // safely iterate through the job list while other threads modify + // the list. it works by inserting a dummy item in the list and + // moving that item through the list. the dummy item will never + // be removed by other edits so an iterator pointing at the item + // remains valid until we remove the dummy item in deleteCursor(). + // nextCursor() finds the next non-dummy item, moves our dummy + // item just past it, and returns an iterator for the non-dummy + // item. all cursor calls lock the mutex for their duration. + CJobCursor newCursor(); + CJobCursor nextCursor(CJobCursor); + void deleteCursor(CJobCursor); + + // lock out locking the job list. this blocks if another thread + // has already locked out locking. once it returns, only the + // calling thread will be able to lock the job list after any + // current lock is released. + void lockJobListLock(); + + // lock the job list. this blocks if the job list is already + // locked. the calling thread must have called requestJobLock. + void lockJobList(); + + // unlock the job list and the lock out on locking. + void unlockJobList(); + +private: + CMutex* m_mutex; + CThread* m_thread; + bool m_update; + CCondVar* m_jobsReady; + CCondVar* m_jobListLock; + CCondVar* m_jobListLockLocked; + CThread* m_jobListLocker; + CThread* m_jobListLockLocker; + + CSocketJobs m_socketJobs; + CSocketJobMap m_socketJobMap; + ISocketMultiplexerJob* m_cursorMark; + + static CSocketMultiplexer* s_instance; +}; + +#endif diff --git a/src/lib/net/CTCPListenSocket.cpp b/src/lib/net/CTCPListenSocket.cpp new file mode 100644 index 00000000..3fe9d5aa --- /dev/null +++ b/src/lib/net/CTCPListenSocket.cpp @@ -0,0 +1,146 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CTCPListenSocket.h" +#include "CNetworkAddress.h" +#include "CSocketMultiplexer.h" +#include "CTCPSocket.h" +#include "TSocketMultiplexerMethodJob.h" +#include "XSocket.h" +#include "XIO.h" +#include "CLock.h" +#include "CMutex.h" +#include "IEventQueue.h" +#include "CArch.h" +#include "XArch.h" + +// +// CTCPListenSocket +// + +CTCPListenSocket::CTCPListenSocket() +{ + m_mutex = new CMutex; + try { + m_socket = ARCH->newSocket(IArchNetwork::kINET, IArchNetwork::kSTREAM); + } + catch (XArchNetwork& e) { + throw XSocketCreate(e.what()); + } +} + +CTCPListenSocket::~CTCPListenSocket() +{ + try { + if (m_socket != NULL) { + CSocketMultiplexer::getInstance()->removeSocket(this); + ARCH->closeSocket(m_socket); + } + } + catch (...) { + // ignore + } + delete m_mutex; +} + +void +CTCPListenSocket::bind(const CNetworkAddress& addr) +{ + try { + CLock lock(m_mutex); + ARCH->setReuseAddrOnSocket(m_socket, true); + ARCH->bindSocket(m_socket, addr.getAddress()); + ARCH->listenOnSocket(m_socket); + CSocketMultiplexer::getInstance()->addSocket(this, + new TSocketMultiplexerMethodJob( + this, &CTCPListenSocket::serviceListening, + m_socket, true, false)); + } + catch (XArchNetworkAddressInUse& e) { + throw XSocketAddressInUse(e.what()); + } + catch (XArchNetwork& e) { + throw XSocketBind(e.what()); + } +} + +void +CTCPListenSocket::close() +{ + CLock lock(m_mutex); + if (m_socket == NULL) { + throw XIOClosed(); + } + try { + CSocketMultiplexer::getInstance()->removeSocket(this); + ARCH->closeSocket(m_socket); + m_socket = NULL; + } + catch (XArchNetwork& e) { + throw XSocketIOClose(e.what()); + } +} + +void* +CTCPListenSocket::getEventTarget() const +{ + return const_cast(reinterpret_cast(this)); +} + +IDataSocket* +CTCPListenSocket::accept() +{ + IDataSocket* socket = NULL; + try { + socket = new CTCPSocket(ARCH->acceptSocket(m_socket, NULL)); + if (socket != NULL) { + CSocketMultiplexer::getInstance()->addSocket(this, + new TSocketMultiplexerMethodJob( + this, &CTCPListenSocket::serviceListening, + m_socket, true, false)); + } + return socket; + } + catch (XArchNetwork&) { + if (socket != NULL) { + delete socket; + } + return NULL; + } + catch (std::exception &ex) { + if (socket != NULL) { + delete socket; + } + throw ex; + } +} + +ISocketMultiplexerJob* +CTCPListenSocket::serviceListening(ISocketMultiplexerJob* job, + bool read, bool, bool error) +{ + if (error) { + close(); + return NULL; + } + if (read) { + EVENTQUEUE->addEvent(CEvent(getConnectingEvent(), this, NULL)); + // stop polling on this socket until the client accepts + return NULL; + } + return job; +} diff --git a/src/lib/net/CTCPListenSocket.h b/src/lib/net/CTCPListenSocket.h new file mode 100644 index 00000000..f0aa794a --- /dev/null +++ b/src/lib/net/CTCPListenSocket.h @@ -0,0 +1,54 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CTCPLISTENSOCKET_H +#define CTCPLISTENSOCKET_H + +#include "IListenSocket.h" +#include "IArchNetwork.h" + +class CMutex; +class ISocketMultiplexerJob; + +//! TCP listen socket +/*! +A listen socket using TCP. +*/ +class CTCPListenSocket : public IListenSocket { +public: + CTCPListenSocket(); + ~CTCPListenSocket(); + + // ISocket overrides + virtual void bind(const CNetworkAddress&); + virtual void close(); + virtual void* getEventTarget() const; + + // IListenSocket overrides + virtual IDataSocket* accept(); + +private: + ISocketMultiplexerJob* + serviceListening(ISocketMultiplexerJob*, + bool, bool, bool); + +private: + CArchSocket m_socket; + CMutex* m_mutex; +}; + +#endif diff --git a/src/lib/net/CTCPSocket.cpp b/src/lib/net/CTCPSocket.cpp new file mode 100644 index 00000000..fb3e1833 --- /dev/null +++ b/src/lib/net/CTCPSocket.cpp @@ -0,0 +1,545 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CTCPSocket.h" +#include "CNetworkAddress.h" +#include "CSocketMultiplexer.h" +#include "TSocketMultiplexerMethodJob.h" +#include "XSocket.h" +#include "CLock.h" +#include "CLog.h" +#include "IEventQueue.h" +#include "IEventJob.h" +#include "CArch.h" +#include "XArch.h" +#include +#include +#include + +// +// CTCPSocket +// + +CTCPSocket::CTCPSocket() : + m_mutex(), + m_flushed(&m_mutex, true) +{ + try { + m_socket = ARCH->newSocket(IArchNetwork::kINET, IArchNetwork::kSTREAM); + } + catch (XArchNetwork& e) { + throw XSocketCreate(e.what()); + } + + init(); +} + +CTCPSocket::CTCPSocket(CArchSocket socket) : + m_mutex(), + m_socket(socket), + m_flushed(&m_mutex, true) +{ + assert(m_socket != NULL); + + // socket starts in connected state + init(); + onConnected(); + setJob(newJob()); +} + +CTCPSocket::~CTCPSocket() +{ + try { + close(); + } + catch (...) { + // ignore + } +} + +void +CTCPSocket::bind(const CNetworkAddress& addr) +{ + try { + ARCH->bindSocket(m_socket, addr.getAddress()); + } + catch (XArchNetworkAddressInUse& e) { + throw XSocketAddressInUse(e.what()); + } + catch (XArchNetwork& e) { + throw XSocketBind(e.what()); + } +} + +void +CTCPSocket::close() +{ + // remove ourself from the multiplexer + setJob(NULL); + + CLock lock(&m_mutex); + + // clear buffers and enter disconnected state + if (m_connected) { + sendEvent(getDisconnectedEvent()); + } + onDisconnected(); + + // close the socket + if (m_socket != NULL) { + CArchSocket socket = m_socket; + m_socket = NULL; + try { + ARCH->closeSocket(socket); + } + catch (XArchNetwork& e) { + // ignore, there's not much we can do + LOG((CLOG_WARN "error closing socket: %s", e.what().c_str())); + } + } +} + +void* +CTCPSocket::getEventTarget() const +{ + return const_cast(reinterpret_cast(this)); +} + +UInt32 +CTCPSocket::read(void* buffer, UInt32 n) +{ + // copy data directly from our input buffer + CLock lock(&m_mutex); + UInt32 size = m_inputBuffer.getSize(); + if (n > size) { + n = size; + } + if (buffer != NULL && n != 0) { + memcpy(buffer, m_inputBuffer.peek(n), n); + } + m_inputBuffer.pop(n); + + // if no more data and we cannot read or write then send disconnected + if (n > 0 && m_inputBuffer.getSize() == 0 && !m_readable && !m_writable) { + sendEvent(getDisconnectedEvent()); + m_connected = false; + } + + return n; +} + +void +CTCPSocket::write(const void* buffer, UInt32 n) +{ + bool wasEmpty; + { + CLock lock(&m_mutex); + + // must not have shutdown output + if (!m_writable) { + sendEvent(getOutputErrorEvent()); + return; + } + + // ignore empty writes + if (n == 0) { + return; + } + + // copy data to the output buffer + wasEmpty = (m_outputBuffer.getSize() == 0); + m_outputBuffer.write(buffer, n); + + // there's data to write + m_flushed = false; + } + + // make sure we're waiting to write + if (wasEmpty) { + setJob(newJob()); + } +} + +void +CTCPSocket::flush() +{ + CLock lock(&m_mutex); + while (m_flushed == false) { + m_flushed.wait(); + } +} + +void +CTCPSocket::shutdownInput() +{ + bool useNewJob = false; + { + CLock lock(&m_mutex); + + // shutdown socket for reading + try { + ARCH->closeSocketForRead(m_socket); + } + catch (XArchNetwork&) { + // ignore + } + + // shutdown buffer for reading + if (m_readable) { + sendEvent(getInputShutdownEvent()); + onInputShutdown(); + useNewJob = true; + } + } + if (useNewJob) { + setJob(newJob()); + } +} + +void +CTCPSocket::shutdownOutput() +{ + bool useNewJob = false; + { + CLock lock(&m_mutex); + + // shutdown socket for writing + try { + ARCH->closeSocketForWrite(m_socket); + } + catch (XArchNetwork&) { + // ignore + } + + // shutdown buffer for writing + if (m_writable) { + sendEvent(getOutputShutdownEvent()); + onOutputShutdown(); + useNewJob = true; + } + } + if (useNewJob) { + setJob(newJob()); + } +} + +bool +CTCPSocket::isReady() const +{ + CLock lock(&m_mutex); + return (m_inputBuffer.getSize() > 0); +} + +UInt32 +CTCPSocket::getSize() const +{ + CLock lock(&m_mutex); + return m_inputBuffer.getSize(); +} + +void +CTCPSocket::connect(const CNetworkAddress& addr) +{ + { + CLock lock(&m_mutex); + + // fail on attempts to reconnect + if (m_socket == NULL || m_connected) { + sendConnectionFailedEvent("busy"); + return; + } + + try { + if (ARCH->connectSocket(m_socket, addr.getAddress())) { + sendEvent(getConnectedEvent()); + onConnected(); + } + else { + // connection is in progress + m_writable = true; + } + } + catch (XArchNetwork& e) { + throw XSocketConnect(e.what()); + } + } + setJob(newJob()); +} + +void +CTCPSocket::init() +{ + // default state + m_connected = false; + m_readable = false; + m_writable = false; + + try { + // turn off Nagle algorithm. we send lots of very short messages + // that should be sent without (much) delay. for example, the + // mouse motion messages are much less useful if they're delayed. + ARCH->setNoDelayOnSocket(m_socket, true); + } + catch (XArchNetwork& e) { + try { + ARCH->closeSocket(m_socket); + m_socket = NULL; + } + catch (XArchNetwork&) { + // ignore + } + throw XSocketCreate(e.what()); + } +} + +void +CTCPSocket::setJob(ISocketMultiplexerJob* job) +{ + // multiplexer will delete the old job + if (job == NULL) { + CSocketMultiplexer::getInstance()->removeSocket(this); + } + else { + CSocketMultiplexer::getInstance()->addSocket(this, job); + } +} + +ISocketMultiplexerJob* +CTCPSocket::newJob() +{ + // note -- must have m_mutex locked on entry + + if (m_socket == NULL) { + return NULL; + } + else if (!m_connected) { + assert(!m_readable); + if (!(m_readable || m_writable)) { + return NULL; + } + return new TSocketMultiplexerMethodJob( + this, &CTCPSocket::serviceConnecting, + m_socket, m_readable, m_writable); + } + else { + if (!(m_readable || (m_writable && (m_outputBuffer.getSize() > 0)))) { + return NULL; + } + return new TSocketMultiplexerMethodJob( + this, &CTCPSocket::serviceConnected, + m_socket, m_readable, + m_writable && (m_outputBuffer.getSize() > 0)); + } +} + +void +CTCPSocket::sendConnectionFailedEvent(const char* msg) +{ + CConnectionFailedInfo* info = new CConnectionFailedInfo(msg); + EVENTQUEUE->addEvent(CEvent(getConnectionFailedEvent(), + getEventTarget(), info, CEvent::kDontFreeData)); +} + +void +CTCPSocket::sendEvent(CEvent::Type type) +{ + EVENTQUEUE->addEvent(CEvent(type, getEventTarget(), NULL)); +} + +void +CTCPSocket::onConnected() +{ + m_connected = true; + m_readable = true; + m_writable = true; +} + +void +CTCPSocket::onInputShutdown() +{ + m_inputBuffer.pop(m_inputBuffer.getSize()); + m_readable = false; +} + +void +CTCPSocket::onOutputShutdown() +{ + m_outputBuffer.pop(m_outputBuffer.getSize()); + m_writable = false; + + // we're now flushed + m_flushed = true; + m_flushed.broadcast(); +} + +void +CTCPSocket::onDisconnected() +{ + // disconnected + onInputShutdown(); + onOutputShutdown(); + m_connected = false; +} + +ISocketMultiplexerJob* +CTCPSocket::serviceConnecting(ISocketMultiplexerJob* job, + bool, bool write, bool error) +{ + CLock lock(&m_mutex); + + // should only check for errors if error is true but checking a new + // socket (and a socket that's connecting should be new) for errors + // should be safe and Mac OS X appears to have a bug where a + // non-blocking stream socket that fails to connect immediately is + // reported by select as being writable (i.e. connected) even when + // the connection has failed. this is easily demonstrated on OS X + // 10.3.4 by starting a synergy client and telling to connect to + // another system that's not running a synergy server. it will + // claim to have connected then quickly disconnect (i guess because + // read returns 0 bytes). unfortunately, synergy attempts to + // reconnect immediately, the process repeats and we end up + // spinning the CPU. luckily, OS X does set SO_ERROR on the + // socket correctly when the connection has failed so checking for + // errors works. (curiously, sometimes OS X doesn't report + // connection refused. when that happens it at least doesn't + // report the socket as being writable so synergy is able to time + // out the attempt.) + if (error || true) { + try { + // connection may have failed or succeeded + ARCH->throwErrorOnSocket(m_socket); + } + catch (XArchNetwork& e) { + sendConnectionFailedEvent(e.what().c_str()); + onDisconnected(); + return newJob(); + } + } + + if (write) { + sendEvent(getConnectedEvent()); + onConnected(); + return newJob(); + } + + return job; +} + +ISocketMultiplexerJob* +CTCPSocket::serviceConnected(ISocketMultiplexerJob* job, + bool read, bool write, bool error) +{ + CLock lock(&m_mutex); + + if (error) { + sendEvent(getDisconnectedEvent()); + onDisconnected(); + return newJob(); + } + + bool needNewJob = false; + + if (write) { + try { + // write data + UInt32 n = m_outputBuffer.getSize(); + const void* buffer = m_outputBuffer.peek(n); + n = (UInt32)ARCH->writeSocket(m_socket, buffer, n); + + // discard written data + if (n > 0) { + m_outputBuffer.pop(n); + if (m_outputBuffer.getSize() == 0) { + sendEvent(getOutputFlushedEvent()); + m_flushed = true; + m_flushed.broadcast(); + needNewJob = true; + } + } + } + catch (XArchNetworkShutdown&) { + // remote read end of stream hungup. our output side + // has therefore shutdown. + onOutputShutdown(); + sendEvent(getOutputShutdownEvent()); + if (!m_readable && m_inputBuffer.getSize() == 0) { + sendEvent(getDisconnectedEvent()); + m_connected = false; + } + needNewJob = true; + } + catch (XArchNetworkDisconnected&) { + // stream hungup + onDisconnected(); + sendEvent(getDisconnectedEvent()); + needNewJob = true; + } + catch (XArchNetwork& e) { + // other write error + LOG((CLOG_WARN "error writing socket: %s", e.what().c_str())); + onDisconnected(); + sendEvent(getOutputErrorEvent()); + sendEvent(getDisconnectedEvent()); + needNewJob = true; + } + } + + if (read && m_readable) { + try { + UInt8 buffer[4096]; + size_t n = ARCH->readSocket(m_socket, buffer, sizeof(buffer)); + if (n > 0) { + bool wasEmpty = (m_inputBuffer.getSize() == 0); + + // slurp up as much as possible + do { + m_inputBuffer.write(buffer, (UInt32)n); + n = ARCH->readSocket(m_socket, buffer, sizeof(buffer)); + } while (n > 0); + + // send input ready if input buffer was empty + if (wasEmpty) { + sendEvent(getInputReadyEvent()); + } + } + else { + // remote write end of stream hungup. our input side + // has therefore shutdown but don't flush our buffer + // since there's still data to be read. + sendEvent(getInputShutdownEvent()); + if (!m_writable && m_inputBuffer.getSize() == 0) { + sendEvent(getDisconnectedEvent()); + m_connected = false; + } + m_readable = false; + needNewJob = true; + } + } + catch (XArchNetworkDisconnected&) { + // stream hungup + sendEvent(getDisconnectedEvent()); + onDisconnected(); + needNewJob = true; + } + catch (XArchNetwork& e) { + // ignore other read error + LOG((CLOG_WARN "error reading socket: %s", e.what().c_str())); + } + } + + return needNewJob ? newJob() : job; +} diff --git a/src/lib/net/CTCPSocket.h b/src/lib/net/CTCPSocket.h new file mode 100644 index 00000000..17447e6e --- /dev/null +++ b/src/lib/net/CTCPSocket.h @@ -0,0 +1,89 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CTCPSOCKET_H +#define CTCPSOCKET_H + +#include "IDataSocket.h" +#include "CStreamBuffer.h" +#include "CCondVar.h" +#include "CMutex.h" +#include "IArchNetwork.h" + +class CMutex; +class CThread; +class ISocketMultiplexerJob; + +//! TCP data socket +/*! +A data socket using TCP. +*/ +class CTCPSocket : public IDataSocket { +public: + CTCPSocket(); + CTCPSocket(CArchSocket); + ~CTCPSocket(); + + // ISocket overrides + virtual void bind(const CNetworkAddress&); + virtual void close(); + virtual void* getEventTarget() const; + + // IStream overrides + virtual UInt32 read(void* buffer, UInt32 n); + virtual void write(const void* buffer, UInt32 n); + virtual void flush(); + virtual void shutdownInput(); + virtual void shutdownOutput(); + virtual bool isReady() const; + virtual UInt32 getSize() const; + + // IDataSocket overrides + virtual void connect(const CNetworkAddress&); + +private: + void init(); + + void setJob(ISocketMultiplexerJob*); + ISocketMultiplexerJob* newJob(); + void sendConnectionFailedEvent(const char*); + void sendEvent(CEvent::Type); + + void onConnected(); + void onInputShutdown(); + void onOutputShutdown(); + void onDisconnected(); + + ISocketMultiplexerJob* + serviceConnecting(ISocketMultiplexerJob*, + bool, bool, bool); + ISocketMultiplexerJob* + serviceConnected(ISocketMultiplexerJob*, + bool, bool, bool); + +private: + CMutex m_mutex; + CArchSocket m_socket; + CStreamBuffer m_inputBuffer; + CStreamBuffer m_outputBuffer; + CCondVar m_flushed; + bool m_connected; + bool m_readable; + bool m_writable; +}; + +#endif diff --git a/src/lib/net/CTCPSocketFactory.cpp b/src/lib/net/CTCPSocketFactory.cpp new file mode 100644 index 00000000..2833072b --- /dev/null +++ b/src/lib/net/CTCPSocketFactory.cpp @@ -0,0 +1,46 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CTCPSocketFactory.h" +#include "CTCPSocket.h" +#include "CTCPListenSocket.h" + +// +// CTCPSocketFactory +// + +CTCPSocketFactory::CTCPSocketFactory() +{ + // do nothing +} + +CTCPSocketFactory::~CTCPSocketFactory() +{ + // do nothing +} + +IDataSocket* +CTCPSocketFactory::create() const +{ + return new CTCPSocket; +} + +IListenSocket* +CTCPSocketFactory::createListen() const +{ + return new CTCPListenSocket; +} diff --git a/src/lib/net/CTCPSocketFactory.h b/src/lib/net/CTCPSocketFactory.h new file mode 100644 index 00000000..302ee423 --- /dev/null +++ b/src/lib/net/CTCPSocketFactory.h @@ -0,0 +1,34 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CTCPSOCKETFACTORY_H +#define CTCPSOCKETFACTORY_H + +#include "ISocketFactory.h" + +//! Socket factory for TCP sockets +class CTCPSocketFactory : public ISocketFactory { +public: + CTCPSocketFactory(); + virtual ~CTCPSocketFactory(); + + // ISocketFactory overrides + virtual IDataSocket* create() const; + virtual IListenSocket* createListen() const; +}; + +#endif diff --git a/src/lib/net/IDataSocket.cpp b/src/lib/net/IDataSocket.cpp new file mode 100644 index 00000000..0fe37af4 --- /dev/null +++ b/src/lib/net/IDataSocket.cpp @@ -0,0 +1,55 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IDataSocket.h" +#include "CEventQueue.h" + +// +// IDataSocket +// + +CEvent::Type IDataSocket::s_connectedEvent = CEvent::kUnknown; +CEvent::Type IDataSocket::s_failedEvent = CEvent::kUnknown; + +CEvent::Type +IDataSocket::getConnectedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_connectedEvent, + "IDataSocket::connected"); +} + +CEvent::Type +IDataSocket::getConnectionFailedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_failedEvent, + "IDataSocket::failed"); +} + +void +IDataSocket::close() +{ + // this is here to work around a VC++6 bug. see the header file. + assert(0 && "bad call"); +} + +void* +IDataSocket::getEventTarget() const +{ + // this is here to work around a VC++6 bug. see the header file. + assert(0 && "bad call"); + return NULL; +} diff --git a/src/lib/net/IDataSocket.h b/src/lib/net/IDataSocket.h new file mode 100644 index 00000000..7a27a63b --- /dev/null +++ b/src/lib/net/IDataSocket.h @@ -0,0 +1,94 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IDATASOCKET_H +#define IDATASOCKET_H + +#include "ISocket.h" +#include "IStream.h" +#include "CString.h" + +//! Data stream socket interface +/*! +This interface defines the methods common to all network sockets that +represent a full-duplex data stream. +*/ +class IDataSocket : public ISocket, public IStream { +public: + class CConnectionFailedInfo { + public: + CConnectionFailedInfo(const char* what) : m_what(what) { } + CString m_what; + }; + + //! @name manipulators + //@{ + + //! Connect socket + /*! + Attempt to connect to a remote endpoint. This returns immediately + and sends a connected event when successful or a connection failed + event when it fails. The stream acts as if shutdown for input and + output until the stream connects. + */ + virtual void connect(const CNetworkAddress&) = 0; + + //@} + //! @name accessors + //@{ + + //! Get connected event type + /*! + Returns the socket connected event type. A socket sends this + event when a remote connection has been established. + */ + static CEvent::Type getConnectedEvent(); + + //! Get connection failed event type + /*! + Returns the socket connection failed event type. A socket sends + this event when an attempt to connect to a remote port has failed. + The data is a pointer to a CConnectionFailedInfo. + */ + static CEvent::Type getConnectionFailedEvent(); + + //@} + + // ISocket overrides + // close() and getEventTarget() aren't pure to work around a bug + // in VC++6. it claims the methods are unused locals and warns + // that it's removing them. it's presumably tickled by inheriting + // methods with identical signatures from both superclasses. + virtual void bind(const CNetworkAddress&) = 0; + virtual void close(); + virtual void* getEventTarget() const; + + // IStream overrides + virtual UInt32 read(void* buffer, UInt32 n) = 0; + virtual void write(const void* buffer, UInt32 n) = 0; + virtual void flush() = 0; + virtual void shutdownInput() = 0; + virtual void shutdownOutput() = 0; + virtual bool isReady() const = 0; + virtual UInt32 getSize() const = 0; + +private: + static CEvent::Type s_connectedEvent; + static CEvent::Type s_failedEvent; +}; + +#endif diff --git a/src/lib/net/IListenSocket.cpp b/src/lib/net/IListenSocket.cpp new file mode 100644 index 00000000..97683081 --- /dev/null +++ b/src/lib/net/IListenSocket.cpp @@ -0,0 +1,32 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IListenSocket.h" +#include "CEventQueue.h" + +// +// IListenSocket +// + +CEvent::Type IListenSocket::s_connectingEvent = CEvent::kUnknown; + +CEvent::Type +IListenSocket::getConnectingEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_connectingEvent, + "IListenSocket::connecting"); +} diff --git a/src/lib/net/IListenSocket.h b/src/lib/net/IListenSocket.h new file mode 100644 index 00000000..b68b9af0 --- /dev/null +++ b/src/lib/net/IListenSocket.h @@ -0,0 +1,65 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ILISTENSOCKET_H +#define ILISTENSOCKET_H + +#include "ISocket.h" + +class IDataSocket; + +//! Listen socket interface +/*! +This interface defines the methods common to all network sockets that +listen for incoming connections. +*/ +class IListenSocket : public ISocket { +public: + //! @name manipulators + //@{ + + //! Accept connection + /*! + Accept a connection, returning a socket representing the full-duplex + data stream. Returns NULL if no socket is waiting to be accepted. + This is only valid after a call to \c bind(). + */ + virtual IDataSocket* accept() = 0; + + //@} + //! @name accessors + //@{ + + //! Get connecting event type + /*! + Returns the socket connecting event type. A socket sends this + event when a remote connection is waiting to be accepted. + */ + static CEvent::Type getConnectingEvent(); + + //@} + + // ISocket overrides + virtual void bind(const CNetworkAddress&) = 0; + virtual void close() = 0; + virtual void* getEventTarget() const = 0; + +private: + static CEvent::Type s_connectingEvent; +}; + +#endif diff --git a/src/lib/net/ISocket.cpp b/src/lib/net/ISocket.cpp new file mode 100644 index 00000000..5ca90451 --- /dev/null +++ b/src/lib/net/ISocket.cpp @@ -0,0 +1,32 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "ISocket.h" +#include "CEventQueue.h" + +// +// ISocket +// + +CEvent::Type ISocket::s_disconnectedEvent = CEvent::kUnknown; + +CEvent::Type +ISocket::getDisconnectedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_disconnectedEvent, + "ISocket::disconnected"); +} diff --git a/src/lib/net/ISocket.h b/src/lib/net/ISocket.h new file mode 100644 index 00000000..8891240d --- /dev/null +++ b/src/lib/net/ISocket.h @@ -0,0 +1,72 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ISOCKET_H +#define ISOCKET_H + +#include "IInterface.h" +#include "CEvent.h" + +class CNetworkAddress; + +//! Generic socket interface +/*! +This interface defines the methods common to all network sockets. +Generated events use \c this as the target. +*/ +class ISocket : public IInterface { +public: + //! @name manipulators + //@{ + + //! Bind socket to address + /*! + Binds the socket to a particular address. + */ + virtual void bind(const CNetworkAddress&) = 0; + + //! Close socket + /*! + Closes the socket. This should flush the output stream. + */ + virtual void close() = 0; + + //@} + //! @name accessors + //@{ + + //! Get event target + /*! + Returns the event target for events generated by this socket. + */ + virtual void* getEventTarget() const = 0; + + //! Get disconnected event type + /*! + Returns the socket disconnected event type. A socket sends this + event when the remote side of the socket has disconnected or + shutdown both input and output. + */ + static CEvent::Type getDisconnectedEvent(); + + //@} + +private: + static CEvent::Type s_disconnectedEvent; +}; + +#endif diff --git a/src/lib/net/ISocketFactory.h b/src/lib/net/ISocketFactory.h new file mode 100644 index 00000000..2d9625fb --- /dev/null +++ b/src/lib/net/ISocketFactory.h @@ -0,0 +1,45 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ISOCKETFACTORY_H +#define ISOCKETFACTORY_H + +#include "IInterface.h" + +class IDataSocket; +class IListenSocket; + +//! Socket factory +/*! +This interface defines the methods common to all factories used to +create sockets. +*/ +class ISocketFactory : public IInterface { +public: + //! @name accessors + //@{ + + //! Create data socket + virtual IDataSocket* create() const = 0; + + //! Create listen socket + virtual IListenSocket* createListen() const = 0; + + //@} +}; + +#endif diff --git a/src/lib/net/ISocketMultiplexerJob.h b/src/lib/net/ISocketMultiplexerJob.h new file mode 100644 index 00000000..d62cefe5 --- /dev/null +++ b/src/lib/net/ISocketMultiplexerJob.h @@ -0,0 +1,78 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ISOCKETMULTIPLEXERJOB_H +#define ISOCKETMULTIPLEXERJOB_H + +#include "IArchNetwork.h" +#include "IInterface.h" + +//! Socket multiplexer job +/*! +A socket multiplexer job handles events on a socket. +*/ +class ISocketMultiplexerJob : public IInterface { +public: + //! @name manipulators + //@{ + + //! Handle socket event + /*! + Called by a socket multiplexer when the socket becomes readable, + writable, or has an error. It should return itself if the same + job can continue to service events, a new job if the socket must + be serviced differently, or NULL if the socket should no longer + be serviced. The socket is readable if \p readable is true, + writable if \p writable is true, and in error if \p error is + true. + + This call must not attempt to directly change the job for this + socket by calling \c addSocket() or \c removeSocket() on the + multiplexer. It must instead return the new job. It can, + however, add or remove jobs for other sockets. + */ + virtual ISocketMultiplexerJob* + run(bool readable, bool writable, bool error) = 0; + + //@} + //! @name accessors + //@{ + + //! Get the socket + /*! + Return the socket to multiplex + */ + virtual CArchSocket getSocket() const = 0; + + //! Check for interest in readability + /*! + Return true if the job is interested in being run if the socket + becomes readable. + */ + virtual bool isReadable() const = 0; + + //! Check for interest in writability + /*! + Return true if the job is interested in being run if the socket + becomes writable. + */ + virtual bool isWritable() const = 0; + + //@} +}; + +#endif diff --git a/src/lib/net/TSocketMultiplexerMethodJob.h b/src/lib/net/TSocketMultiplexerMethodJob.h new file mode 100644 index 00000000..b1e28d48 --- /dev/null +++ b/src/lib/net/TSocketMultiplexerMethodJob.h @@ -0,0 +1,111 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef TSOCKERMULTIPLEXERMETHODJOB_H +#define TSOCKERMULTIPLEXERMETHODJOB_H + +#include "ISocketMultiplexerJob.h" +#include "CArch.h" + +//! Use a method as a socket multiplexer job +/*! +A socket multiplexer job class that invokes a member function. +*/ +template +class TSocketMultiplexerMethodJob : public ISocketMultiplexerJob { +public: + typedef ISocketMultiplexerJob* + (T::*Method)(ISocketMultiplexerJob*, bool, bool, bool); + + //! run() invokes \c object->method(arg) + TSocketMultiplexerMethodJob(T* object, Method method, + CArchSocket socket, bool readable, bool writeable); + virtual ~TSocketMultiplexerMethodJob(); + + // IJob overrides + virtual ISocketMultiplexerJob* + run(bool readable, bool writable, bool error); + virtual CArchSocket getSocket() const; + virtual bool isReadable() const; + virtual bool isWritable() const; + +private: + T* m_object; + Method m_method; + CArchSocket m_socket; + bool m_readable; + bool m_writable; + void* m_arg; +}; + +template +inline +TSocketMultiplexerMethodJob::TSocketMultiplexerMethodJob(T* object, + Method method, CArchSocket socket, + bool readable, bool writable) : + m_object(object), + m_method(method), + m_socket(ARCH->copySocket(socket)), + m_readable(readable), + m_writable(writable) +{ + // do nothing +} + +template +inline +TSocketMultiplexerMethodJob::~TSocketMultiplexerMethodJob() +{ + ARCH->closeSocket(m_socket); +} + +template +inline +ISocketMultiplexerJob* +TSocketMultiplexerMethodJob::run(bool read, bool write, bool error) +{ + if (m_object != NULL) { + return (m_object->*m_method)(this, read, write, error); + } + return NULL; +} + +template +inline +CArchSocket +TSocketMultiplexerMethodJob::getSocket() const +{ + return m_socket; +} + +template +inline +bool +TSocketMultiplexerMethodJob::isReadable() const +{ + return m_readable; +} + +template +inline +bool +TSocketMultiplexerMethodJob::isWritable() const +{ + return m_writable; +} + +#endif diff --git a/src/lib/net/XSocket.cpp b/src/lib/net/XSocket.cpp new file mode 100644 index 00000000..1fbb94f2 --- /dev/null +++ b/src/lib/net/XSocket.cpp @@ -0,0 +1,116 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "XSocket.h" +#include "CStringUtil.h" + +// +// XSocketAddress +// + +XSocketAddress::XSocketAddress(EError error, + const CString& hostname, int port) throw() : + m_error(error), + m_hostname(hostname), + m_port(port) +{ + // do nothing +} + +XSocketAddress::EError +XSocketAddress::getError() const throw() +{ + return m_error; +} + +CString +XSocketAddress::getHostname() const throw() +{ + return m_hostname; +} + +int +XSocketAddress::getPort() const throw() +{ + return m_port; +} + +CString +XSocketAddress::getWhat() const throw() +{ + static const char* s_errorID[] = { + "XSocketAddressUnknown", + "XSocketAddressNotFound", + "XSocketAddressNoAddress", + "XSocketAddressUnsupported", + "XSocketAddressBadPort" + }; + static const char* s_errorMsg[] = { + "unknown error for: %{1}:%{2}", + "address not found for: %{1}", + "no address for: %{1}", + "unsupported address for: %{1}", + "invalid port" // m_port may not be set to the bad port + }; + return format(s_errorID[m_error], s_errorMsg[m_error], + m_hostname.c_str(), + CStringUtil::print("%d", m_port).c_str()); +} + + +// +// XSocketIOClose +// + +CString +XSocketIOClose::getWhat() const throw() +{ + return format("XSocketIOClose", "close: %{1}", what()); +} + + +// +// XSocketBind +// + +CString +XSocketBind::getWhat() const throw() +{ + return format("XSocketBind", "cannot bind address: %{1}", what()); +} + + +// +// XSocketConnect +// + +CString +XSocketConnect::getWhat() const throw() +{ + return format("XSocketConnect", "cannot connect socket: %{1}", what()); +} + + +// +// XSocketCreate +// + +CString +XSocketCreate::getWhat() const throw() +{ + return format("XSocketCreate", "cannot create socket: %{1}", what()); +} diff --git a/src/lib/net/XSocket.h b/src/lib/net/XSocket.h new file mode 100644 index 00000000..ca7ac80b --- /dev/null +++ b/src/lib/net/XSocket.h @@ -0,0 +1,99 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef XSOCKET_H +#define XSOCKET_H + +#include "XIO.h" +#include "XBase.h" +#include "CString.h" +#include "BasicTypes.h" + +//! Generic socket exception +XBASE_SUBCLASS(XSocket, XBase); + +//! Socket bad address exception +/*! +Thrown when attempting to create an invalid network address. +*/ +class XSocketAddress : public XSocket { +public: + //! Failure codes + enum EError { + kUnknown, //!< Unknown error + kNotFound, //!< The hostname is unknown + kNoAddress, //!< The hostname is valid but has no IP address + kUnsupported, //!< The hostname is valid but has no supported address + kBadPort //!< The port is invalid + }; + + XSocketAddress(EError, const CString& hostname, int port) throw(); + + //! @name accessors + //@{ + + //! Get the error code + EError getError() const throw(); + //! Get the hostname + CString getHostname() const throw(); + //! Get the port + int getPort() const throw(); + + //@} + +protected: + // XBase overrides + virtual CString getWhat() const throw(); + +private: + EError m_error; + CString m_hostname; + int m_port; +}; + +//! I/O closing exception +/*! +Thrown if a stream cannot be closed. +*/ +XBASE_SUBCLASS_FORMAT(XSocketIOClose, XIOClose); + +//! Socket cannot bind address exception +/*! +Thrown when a socket cannot be bound to an address. +*/ +XBASE_SUBCLASS_FORMAT(XSocketBind, XSocket); + +//! Socket address in use exception +/*! +Thrown when a socket cannot be bound to an address because the address +is already in use. +*/ +XBASE_SUBCLASS(XSocketAddressInUse, XSocketBind); + +//! Cannot connect socket exception +/*! +Thrown when a socket cannot connect to a remote endpoint. +*/ +XBASE_SUBCLASS_FORMAT(XSocketConnect, XSocket); + +//! Cannot create socket exception +/*! +Thrown when a socket cannot be created (by the operating system). +*/ +XBASE_SUBCLASS_FORMAT(XSocketCreate, XSocket); + +#endif diff --git a/src/lib/platform/CMSWindowsClipboard.cpp b/src/lib/platform/CMSWindowsClipboard.cpp new file mode 100644 index 00000000..7ba83480 --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboard.cpp @@ -0,0 +1,226 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsClipboard.h" +#include "CMSWindowsClipboardTextConverter.h" +#include "CMSWindowsClipboardUTF16Converter.h" +#include "CMSWindowsClipboardBitmapConverter.h" +#include "CMSWindowsClipboardHTMLConverter.h" +#include "CLog.h" +#include "CArchMiscWindows.h" +#include "CMSWindowsClipboardFacade.h" + +// +// CMSWindowsClipboard +// + +UINT CMSWindowsClipboard::s_ownershipFormat = 0; + +CMSWindowsClipboard::CMSWindowsClipboard(HWND window) : + m_window(window), + m_time(0), + m_facade(new CMSWindowsClipboardFacade()), + m_deleteFacade(true) +{ + // add converters, most desired first + m_converters.push_back(new CMSWindowsClipboardUTF16Converter); + if (CArchMiscWindows::isWindows95Family()) { + // windows nt family converts to/from unicode automatically. + // let it do so to avoid text encoding issues. + m_converters.push_back(new CMSWindowsClipboardTextConverter); + } + m_converters.push_back(new CMSWindowsClipboardBitmapConverter); + m_converters.push_back(new CMSWindowsClipboardHTMLConverter); +} + +CMSWindowsClipboard::~CMSWindowsClipboard() +{ + clearConverters(); + + // dependency injection causes confusion over ownership, so we need + // logic to decide whether or not we delete the facade. there must + // be a more elegant way of doing this. + if (m_deleteFacade) + delete m_facade; +} + +bool +CMSWindowsClipboard::emptyUnowned() +{ + LOG((CLOG_DEBUG "empty clipboard")); + + // empty the clipboard (and take ownership) + if (!EmptyClipboard()) { + // unable to cause this in integ tests, but this error has never + // actually been reported by users. + LOG((CLOG_DEBUG "failed to grab clipboard")); + return false; + } + + return true; +} + +bool +CMSWindowsClipboard::empty() +{ + if (!emptyUnowned()) { + return false; + } + + // mark clipboard as being owned by synergy + HGLOBAL data = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, 1); + SetClipboardData(getOwnershipFormat(), data); + + return true; +} + +void +CMSWindowsClipboard::add(EFormat format, const CString& data) +{ + LOG((CLOG_DEBUG "add %d bytes to clipboard format: %d", data.size(), format)); + + // convert data to win32 form + for (ConverterList::const_iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + IMSWindowsClipboardConverter* converter = *index; + + // skip converters for other formats + if (converter->getFormat() == format) { + HANDLE win32Data = converter->fromIClipboard(data); + if (win32Data != NULL) { + UINT win32Format = converter->getWin32Format(); + m_facade->write(win32Data, win32Format); + } + } + } +} + +bool +CMSWindowsClipboard::open(Time time) const +{ + LOG((CLOG_DEBUG "open clipboard")); + + if (!OpenClipboard(m_window)) { + // unable to cause this in integ tests; but this can happen! + // * http://synergy-foss.org/pm/issues/86 + // * http://synergy-foss.org/pm/issues/1256 + // logging improved to see if we can catch more info next time. + LOG((CLOG_WARN "failed to open clipboard: %d", GetLastError())); + return false; + } + + m_time = time; + + return true; +} + +void +CMSWindowsClipboard::close() const +{ + LOG((CLOG_DEBUG "close clipboard")); + CloseClipboard(); +} + +IClipboard::Time +CMSWindowsClipboard::getTime() const +{ + return m_time; +} + +bool +CMSWindowsClipboard::has(EFormat format) const +{ + for (ConverterList::const_iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + IMSWindowsClipboardConverter* converter = *index; + if (converter->getFormat() == format) { + if (IsClipboardFormatAvailable(converter->getWin32Format())) { + return true; + } + } + } + return false; +} + +CString +CMSWindowsClipboard::get(EFormat format) const +{ + // find the converter for the first clipboard format we can handle + IMSWindowsClipboardConverter* converter = NULL; + UINT win32Format = EnumClipboardFormats(0); + while (converter == NULL && win32Format != 0) { + for (ConverterList::const_iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + converter = *index; + if (converter->getWin32Format() == win32Format && + converter->getFormat() == format) { + break; + } + converter = NULL; + } + win32Format = EnumClipboardFormats(win32Format); + } + + // if no converter then we don't recognize any formats + if (converter == NULL) { + return CString(); + } + + // get a handle to the clipboard data + HANDLE win32Data = GetClipboardData(converter->getWin32Format()); + if (win32Data == NULL) { + // nb: can't cause this using integ tests; this is only caused when + // the selected converter returns an invalid format -- which you + // cannot cause using public functions. + return CString(); + } + + // convert + return converter->toIClipboard(win32Data); +} + +void +CMSWindowsClipboard::clearConverters() +{ + for (ConverterList::iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + delete *index; + } + m_converters.clear(); +} + +bool +CMSWindowsClipboard::isOwnedBySynergy() +{ + // create ownership format if we haven't yet + if (s_ownershipFormat == 0) { + s_ownershipFormat = RegisterClipboardFormat(TEXT("SynergyOwnership")); + } + return (IsClipboardFormatAvailable(getOwnershipFormat()) != 0); +} + +UINT +CMSWindowsClipboard::getOwnershipFormat() +{ + // create ownership format if we haven't yet + if (s_ownershipFormat == 0) { + s_ownershipFormat = RegisterClipboardFormat(TEXT("SynergyOwnership")); + } + + // return the format + return s_ownershipFormat; +} diff --git a/src/lib/platform/CMSWindowsClipboard.h b/src/lib/platform/CMSWindowsClipboard.h new file mode 100644 index 00000000..f00d7647 --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboard.h @@ -0,0 +1,114 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSCLIPBOARD_H +#define CMSWINDOWSCLIPBOARD_H + +#include "IClipboard.h" +#include "CMSWindowsClipboardFacade.h" +#include "stdvector.h" +#define WIN32_LEAN_AND_MEAN +#include + +class IMSWindowsClipboardConverter; +class IMSWindowsClipboardFacade; + +//! Microsoft windows clipboard implementation +class CMSWindowsClipboard : public IClipboard { +public: + CMSWindowsClipboard(HWND window); + CMSWindowsClipboard(HWND window, IMSWindowsClipboardFacade &facade); + virtual ~CMSWindowsClipboard(); + + //! Empty clipboard without ownership + /*! + Take ownership of the clipboard and clear all data from it. + This must be called between a successful open() and close(). + Return false if the clipboard ownership could not be taken; + the clipboard should not be emptied in this case. Unlike + empty(), isOwnedBySynergy() will return false when emptied + this way. This is useful when synergy wants to put data on + clipboard but pretend (to itself) that some other app did it. + When using empty(), synergy assumes the data came from the + server and doesn't need to be sent back. emptyUnowned() + makes synergy send the data to the server. + */ + bool emptyUnowned(); + + //! Test if clipboard is owned by synergy + static bool isOwnedBySynergy(); + + // IClipboard overrides + virtual bool empty(); + virtual void add(EFormat, const CString& data); + virtual bool open(Time) const; + virtual void close() const; + virtual Time getTime() const; + virtual bool has(EFormat) const; + virtual CString get(EFormat) const; + + void setFacade(IMSWindowsClipboardFacade& facade) { m_facade = &facade; m_deleteFacade = false; } + +private: + void clearConverters(); + + UINT convertFormatToWin32(EFormat) const; + HANDLE convertTextToWin32(const CString& data) const; + CString convertTextFromWin32(HANDLE) const; + + static UINT getOwnershipFormat(); + +private: + typedef std::vector ConverterList; + + HWND m_window; + mutable Time m_time; + ConverterList m_converters; + static UINT s_ownershipFormat; + IMSWindowsClipboardFacade* m_facade; + bool m_deleteFacade; +}; + +//! Clipboard format converter interface +/*! +This interface defines the methods common to all win32 clipboard format +converters. +*/ +class IMSWindowsClipboardConverter : public IInterface { +public: + // accessors + + // return the clipboard format this object converts from/to + virtual IClipboard::EFormat + getFormat() const = 0; + + // return the atom representing the win32 clipboard format that + // this object converts from/to + virtual UINT getWin32Format() const = 0; + + // convert from the IClipboard format to the win32 clipboard format. + // the input data must be in the IClipboard format returned by + // getFormat(). the return data will be in the win32 clipboard + // format returned by getWin32Format(), allocated by GlobalAlloc(). + virtual HANDLE fromIClipboard(const CString&) const = 0; + + // convert from the win32 clipboard format to the IClipboard format + // (i.e., the reverse of fromIClipboard()). + virtual CString toIClipboard(HANDLE data) const = 0; +}; + +#endif diff --git a/src/lib/platform/CMSWindowsClipboardAnyTextConverter.cpp b/src/lib/platform/CMSWindowsClipboardAnyTextConverter.cpp new file mode 100644 index 00000000..c0d0e9ce --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboardAnyTextConverter.cpp @@ -0,0 +1,148 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsClipboardAnyTextConverter.h" + +// +// CMSWindowsClipboardAnyTextConverter +// + +CMSWindowsClipboardAnyTextConverter::CMSWindowsClipboardAnyTextConverter() +{ + // do nothing +} + +CMSWindowsClipboardAnyTextConverter::~CMSWindowsClipboardAnyTextConverter() +{ + // do nothing +} + +IClipboard::EFormat +CMSWindowsClipboardAnyTextConverter::getFormat() const +{ + return IClipboard::kText; +} + +HANDLE +CMSWindowsClipboardAnyTextConverter::fromIClipboard(const CString& data) const +{ + // convert linefeeds and then convert to desired encoding + CString text = doFromIClipboard(convertLinefeedToWin32(data)); + UInt32 size = (UInt32)text.size(); + + // copy to memory handle + HGLOBAL gData = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, size); + if (gData != NULL) { + // get a pointer to the allocated memory + char* dst = (char*)GlobalLock(gData); + if (dst != NULL) { + memcpy(dst, text.data(), size); + GlobalUnlock(gData); + } + else { + GlobalFree(gData); + gData = NULL; + } + } + + return gData; +} + +CString +CMSWindowsClipboardAnyTextConverter::toIClipboard(HANDLE data) const +{ + // get datator + const char* src = (const char*)GlobalLock(data); + UInt32 srcSize = (UInt32)GlobalSize(data); + if (src == NULL || srcSize <= 1) { + return CString(); + } + + // convert text + CString text = doToIClipboard(CString(src, srcSize)); + + // release handle + GlobalUnlock(data); + + // convert newlines + return convertLinefeedToUnix(text); +} + +CString +CMSWindowsClipboardAnyTextConverter::convertLinefeedToWin32( + const CString& src) const +{ + // note -- we assume src is a valid UTF-8 string + + // count newlines in string + UInt32 numNewlines = 0; + UInt32 n = (UInt32)src.size(); + for (const char* scan = src.c_str(); n > 0; ++scan, --n) { + if (*scan == '\n') { + ++numNewlines; + } + } + if (numNewlines == 0) { + return src; + } + + // allocate new string + CString dst; + dst.reserve(src.size() + numNewlines); + + // copy string, converting newlines + n = (UInt32)src.size(); + for (const char* scan = src.c_str(); n > 0; ++scan, --n) { + if (scan[0] == '\n') { + dst += '\r'; + } + dst += scan[0]; + } + + return dst; +} + +CString +CMSWindowsClipboardAnyTextConverter::convertLinefeedToUnix( + const CString& src) const +{ + // count newlines in string + UInt32 numNewlines = 0; + UInt32 n = (UInt32)src.size(); + for (const char* scan = src.c_str(); n > 0; ++scan, --n) { + if (scan[0] == '\r' && scan[1] == '\n') { + ++numNewlines; + } + } + if (numNewlines == 0) { + return src; + } + + // allocate new string + CString dst; + dst.reserve(src.size()); + + // copy string, converting newlines + n = (UInt32)src.size(); + for (const char* scan = src.c_str(); n > 0; ++scan, --n) { + if (scan[0] != '\r' || scan[1] != '\n') { + dst += scan[0]; + } + } + + return dst; +} diff --git a/src/lib/platform/CMSWindowsClipboardAnyTextConverter.h b/src/lib/platform/CMSWindowsClipboardAnyTextConverter.h new file mode 100644 index 00000000..c2802127 --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboardAnyTextConverter.h @@ -0,0 +1,59 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSCLIPBOARDANYTEXTCONVERTER_H +#define CMSWINDOWSCLIPBOARDANYTEXTCONVERTER_H + +#include "CMSWindowsClipboard.h" + +//! Convert to/from some text encoding +class CMSWindowsClipboardAnyTextConverter : + public IMSWindowsClipboardConverter { +public: + CMSWindowsClipboardAnyTextConverter(); + virtual ~CMSWindowsClipboardAnyTextConverter(); + + // IMSWindowsClipboardConverter overrides + virtual IClipboard::EFormat + getFormat() const; + virtual UINT getWin32Format() const = 0; + virtual HANDLE fromIClipboard(const CString&) const; + virtual CString toIClipboard(HANDLE) const; + +protected: + //! Convert from IClipboard format + /*! + Do UTF-8 conversion only. Memory handle allocation and + linefeed conversion is done by this class. doFromIClipboard() + must include the nul terminator in the returned string (not + including the CString's nul terminator). + */ + virtual CString doFromIClipboard(const CString&) const = 0; + + //! Convert to IClipboard format + /*! + Do UTF-8 conversion only. Memory handle allocation and + linefeed conversion is done by this class. + */ + virtual CString doToIClipboard(const CString&) const = 0; + +private: + CString convertLinefeedToWin32(const CString&) const; + CString convertLinefeedToUnix(const CString&) const; +}; + +#endif diff --git a/src/lib/platform/CMSWindowsClipboardBitmapConverter.cpp b/src/lib/platform/CMSWindowsClipboardBitmapConverter.cpp new file mode 100644 index 00000000..98345ab9 --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboardBitmapConverter.cpp @@ -0,0 +1,150 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsClipboardBitmapConverter.h" +#include "CLog.h" + +// +// CMSWindowsClipboardBitmapConverter +// + +CMSWindowsClipboardBitmapConverter::CMSWindowsClipboardBitmapConverter() +{ + // do nothing +} + +CMSWindowsClipboardBitmapConverter::~CMSWindowsClipboardBitmapConverter() +{ + // do nothing +} + +IClipboard::EFormat +CMSWindowsClipboardBitmapConverter::getFormat() const +{ + return IClipboard::kBitmap; +} + +UINT +CMSWindowsClipboardBitmapConverter::getWin32Format() const +{ + return CF_DIB; +} + +HANDLE +CMSWindowsClipboardBitmapConverter::fromIClipboard(const CString& data) const +{ + // copy to memory handle + HGLOBAL gData = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, data.size()); + if (gData != NULL) { + // get a pointer to the allocated memory + char* dst = (char*)GlobalLock(gData); + if (dst != NULL) { + memcpy(dst, data.data(), data.size()); + GlobalUnlock(gData); + } + else { + GlobalFree(gData); + gData = NULL; + } + } + + return gData; +} + +CString +CMSWindowsClipboardBitmapConverter::toIClipboard(HANDLE data) const +{ + // get datator + const char* src = (const char*)GlobalLock(data); + if (src == NULL) { + return CString(); + } + UInt32 srcSize = (UInt32)GlobalSize(data); + + // check image type + const BITMAPINFO* bitmap = reinterpret_cast(src); + LOG((CLOG_INFO "bitmap: %dx%d %d", bitmap->bmiHeader.biWidth, bitmap->bmiHeader.biHeight, (int)bitmap->bmiHeader.biBitCount)); + if (bitmap->bmiHeader.biPlanes == 1 && + (bitmap->bmiHeader.biBitCount == 24 || + bitmap->bmiHeader.biBitCount == 32) && + bitmap->bmiHeader.biCompression == BI_RGB) { + // already in canonical form + CString image(src, srcSize); + GlobalUnlock(data); + return image; + } + + // create a destination DIB section + LOG((CLOG_INFO "convert image from: depth=%d comp=%d", bitmap->bmiHeader.biBitCount, bitmap->bmiHeader.biCompression)); + void* raw; + BITMAPINFOHEADER info; + LONG w = bitmap->bmiHeader.biWidth; + LONG h = bitmap->bmiHeader.biHeight; + info.biSize = sizeof(BITMAPINFOHEADER); + info.biWidth = w; + info.biHeight = h; + info.biPlanes = 1; + info.biBitCount = 32; + info.biCompression = BI_RGB; + info.biSizeImage = 0; + info.biXPelsPerMeter = 1000; + info.biYPelsPerMeter = 1000; + info.biClrUsed = 0; + info.biClrImportant = 0; + HDC dc = GetDC(NULL); + HBITMAP dst = CreateDIBSection(dc, (BITMAPINFO*)&info, + DIB_RGB_COLORS, &raw, NULL, 0); + + // find the start of the pixel data + const char* srcBits = (const char*)bitmap + bitmap->bmiHeader.biSize; + if (bitmap->bmiHeader.biBitCount >= 16) { + if (bitmap->bmiHeader.biCompression == BI_BITFIELDS && + (bitmap->bmiHeader.biBitCount == 16 || + bitmap->bmiHeader.biBitCount == 32)) { + srcBits += 3 * sizeof(DWORD); + } + } + else if (bitmap->bmiHeader.biClrUsed != 0) { + srcBits += bitmap->bmiHeader.biClrUsed * sizeof(RGBQUAD); + } + else { + //http://msdn.microsoft.com/en-us/library/ke55d167(VS.80).aspx + srcBits += (1i64 << bitmap->bmiHeader.biBitCount) * sizeof(RGBQUAD); + } + + // copy source image to destination image + HDC dstDC = CreateCompatibleDC(dc); + HGDIOBJ oldBitmap = SelectObject(dstDC, dst); + SetDIBitsToDevice(dstDC, 0, 0, w, h, 0, 0, 0, h, + srcBits, bitmap, DIB_RGB_COLORS); + SelectObject(dstDC, oldBitmap); + DeleteDC(dstDC); + GdiFlush(); + + // extract data + CString image((const char*)&info, info.biSize); + image.append((const char*)raw, 4 * w * h); + + // clean up GDI + DeleteObject(dst); + ReleaseDC(NULL, dc); + + // release handle + GlobalUnlock(data); + + return image; +} diff --git a/src/lib/platform/CMSWindowsClipboardBitmapConverter.h b/src/lib/platform/CMSWindowsClipboardBitmapConverter.h new file mode 100644 index 00000000..cf435bf9 --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboardBitmapConverter.h @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSCLIPBOARDBITMAPCONVERTER_H +#define CMSWINDOWSCLIPBOARDBITMAPCONVERTER_H + +#include "CMSWindowsClipboard.h" + +//! Convert to/from some text encoding +class CMSWindowsClipboardBitmapConverter : + public IMSWindowsClipboardConverter { +public: + CMSWindowsClipboardBitmapConverter(); + virtual ~CMSWindowsClipboardBitmapConverter(); + + // IMSWindowsClipboardConverter overrides + virtual IClipboard::EFormat + getFormat() const; + virtual UINT getWin32Format() const; + virtual HANDLE fromIClipboard(const CString&) const; + virtual CString toIClipboard(HANDLE) const; +}; + +#endif diff --git a/src/lib/platform/CMSWindowsClipboardFacade.cpp b/src/lib/platform/CMSWindowsClipboardFacade.cpp new file mode 100644 index 00000000..0639b9bf --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboardFacade.cpp @@ -0,0 +1,29 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsClipboard.h" +#include "CMSWindowsClipboardFacade.h" + +void CMSWindowsClipboardFacade::write(HANDLE win32Data, UINT win32Format) +{ + if (SetClipboardData(win32Format, win32Data) == NULL) { + // free converted data if we couldn't put it on + // the clipboard. + // nb: couldn't cause this in integ tests. + GlobalFree(win32Data); + } +} diff --git a/src/lib/platform/CMSWindowsClipboardFacade.h b/src/lib/platform/CMSWindowsClipboardFacade.h new file mode 100644 index 00000000..db21e1bc --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboardFacade.h @@ -0,0 +1,30 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSCLIPBOARDFACADE_H +#define CMSWINDOWSCLIPBOARDFACADE_H + +#include "IMSWindowsClipboardFacade.h" +#include "IClipboard.h" + +class CMSWindowsClipboardFacade : public IMSWindowsClipboardFacade +{ +public: + virtual void write(HANDLE win32Data, UINT win32Format); +}; + +#endif \ No newline at end of file diff --git a/src/lib/platform/CMSWindowsClipboardHTMLConverter.cpp b/src/lib/platform/CMSWindowsClipboardHTMLConverter.cpp new file mode 100644 index 00000000..278755d1 --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboardHTMLConverter.cpp @@ -0,0 +1,118 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsClipboardHTMLConverter.h" +#include "CStringUtil.h" + +// +// CMSWindowsClipboardHTMLConverter +// + +CMSWindowsClipboardHTMLConverter::CMSWindowsClipboardHTMLConverter() +{ + m_format = RegisterClipboardFormat("HTML Format"); +} + +CMSWindowsClipboardHTMLConverter::~CMSWindowsClipboardHTMLConverter() +{ + // do nothing +} + +IClipboard::EFormat +CMSWindowsClipboardHTMLConverter::getFormat() const +{ + return IClipboard::kHTML; +} + +UINT +CMSWindowsClipboardHTMLConverter::getWin32Format() const +{ + return m_format; +} + +CString +CMSWindowsClipboardHTMLConverter::doFromIClipboard(const CString& data) const +{ + // prepare to CF_HTML format prefix and suffix + CString prefix("Version:0.9\r\nStartHTML:0000000105\r\n" + "EndHTML:ZZZZZZZZZZ\r\n" + "StartFragment:XXXXXXXXXX\r\nEndFragment:YYYYYYYYYY\r\n" + ""); + CString suffix("\r\n"); + + // Get byte offsets for header + UInt32 StartFragment = (UInt32)prefix.size(); + UInt32 EndFragment = StartFragment + (UInt32)data.size(); + // StartHTML is constant by the design of the prefix + UInt32 EndHTML = EndFragment + (UInt32)suffix.size(); + + prefix.replace(prefix.find("XXXXXXXXXX"), 10, + CStringUtil::print("%010u", StartFragment)); + prefix.replace(prefix.find("YYYYYYYYYY"), 10, + CStringUtil::print("%010u", EndFragment)); + prefix.replace(prefix.find("ZZZZZZZZZZ"), 10, + CStringUtil::print("%010u", EndHTML)); + + // concatenate + prefix += data; + prefix += suffix; + return prefix; +} + +CString +CMSWindowsClipboardHTMLConverter::doToIClipboard(const CString& data) const +{ + // get fragment start/end args + CString startArg = findArg(data, "StartFragment"); + CString endArg = findArg(data, "EndFragment"); + if (startArg.empty() || endArg.empty()) { + return CString(); + } + + // convert args to integers + SInt32 start = (SInt32)atoi(startArg.c_str()); + SInt32 end = (SInt32)atoi(endArg.c_str()); + if (start <= 0 || end <= 0 || start >= end) { + return CString(); + } + + // extract the fragment + return data.substr(start, end - start); +} + +CString +CMSWindowsClipboardHTMLConverter::findArg( + const CString& data, const CString& name) const +{ + CString::size_type i = data.find(name); + if (i == CString::npos) { + return CString(); + } + i = data.find_first_of(":\r\n", i); + if (i == CString::npos || data[i] != ':') { + return CString(); + } + i = data.find_first_of("0123456789\r\n", i + 1); + if (i == CString::npos || data[i] == '\r' || data[i] == '\n') { + return CString(); + } + CString::size_type j = data.find_first_not_of("0123456789", i); + if (j == CString::npos) { + j = data.size(); + } + return data.substr(i, j - i); +} diff --git a/src/lib/platform/CMSWindowsClipboardHTMLConverter.h b/src/lib/platform/CMSWindowsClipboardHTMLConverter.h new file mode 100644 index 00000000..0a58f369 --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboardHTMLConverter.h @@ -0,0 +1,47 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSCLIPBOARDHTMLCONVERTER_H +#define CMSWINDOWSCLIPBOARDHTMLCONVERTER_H + +#include "CMSWindowsClipboardAnyTextConverter.h" + +//! Convert to/from HTML encoding +class CMSWindowsClipboardHTMLConverter : + public CMSWindowsClipboardAnyTextConverter { +public: + CMSWindowsClipboardHTMLConverter(); + virtual ~CMSWindowsClipboardHTMLConverter(); + + // IMSWindowsClipboardConverter overrides + virtual IClipboard::EFormat + getFormat() const; + virtual UINT getWin32Format() const; + +protected: + // CMSWindowsClipboardAnyTextConverter overrides + virtual CString doFromIClipboard(const CString&) const; + virtual CString doToIClipboard(const CString&) const; + +private: + CString findArg(const CString& data, const CString& name) const; + +private: + UINT m_format; +}; + +#endif diff --git a/src/lib/platform/CMSWindowsClipboardTextConverter.cpp b/src/lib/platform/CMSWindowsClipboardTextConverter.cpp new file mode 100644 index 00000000..d0d7a4bd --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboardTextConverter.cpp @@ -0,0 +1,58 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsClipboardTextConverter.h" +#include "CUnicode.h" + +// +// CMSWindowsClipboardTextConverter +// + +CMSWindowsClipboardTextConverter::CMSWindowsClipboardTextConverter() +{ + // do nothing +} + +CMSWindowsClipboardTextConverter::~CMSWindowsClipboardTextConverter() +{ + // do nothing +} + +UINT +CMSWindowsClipboardTextConverter::getWin32Format() const +{ + return CF_TEXT; +} + +CString +CMSWindowsClipboardTextConverter::doFromIClipboard(const CString& data) const +{ + // convert and add nul terminator + return CUnicode::UTF8ToText(data) += '\0'; +} + +CString +CMSWindowsClipboardTextConverter::doToIClipboard(const CString& data) const +{ + // convert and truncate at first nul terminator + CString dst = CUnicode::textToUTF8(data); + CString::size_type n = dst.find('\0'); + if (n != CString::npos) { + dst.erase(n); + } + return dst; +} diff --git a/src/lib/platform/CMSWindowsClipboardTextConverter.h b/src/lib/platform/CMSWindowsClipboardTextConverter.h new file mode 100644 index 00000000..0ecb3fb8 --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboardTextConverter.h @@ -0,0 +1,39 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSCLIPBOARDTEXTCONVERTER_H +#define CMSWINDOWSCLIPBOARDTEXTCONVERTER_H + +#include "CMSWindowsClipboardAnyTextConverter.h" + +//! Convert to/from locale text encoding +class CMSWindowsClipboardTextConverter : + public CMSWindowsClipboardAnyTextConverter { +public: + CMSWindowsClipboardTextConverter(); + virtual ~CMSWindowsClipboardTextConverter(); + + // IMSWindowsClipboardConverter overrides + virtual UINT getWin32Format() const; + +protected: + // CMSWindowsClipboardAnyTextConverter overrides + virtual CString doFromIClipboard(const CString&) const; + virtual CString doToIClipboard(const CString&) const; +}; + +#endif diff --git a/src/lib/platform/CMSWindowsClipboardUTF16Converter.cpp b/src/lib/platform/CMSWindowsClipboardUTF16Converter.cpp new file mode 100644 index 00000000..46fd42b2 --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboardUTF16Converter.cpp @@ -0,0 +1,58 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsClipboardUTF16Converter.h" +#include "CUnicode.h" + +// +// CMSWindowsClipboardUTF16Converter +// + +CMSWindowsClipboardUTF16Converter::CMSWindowsClipboardUTF16Converter() +{ + // do nothing +} + +CMSWindowsClipboardUTF16Converter::~CMSWindowsClipboardUTF16Converter() +{ + // do nothing +} + +UINT +CMSWindowsClipboardUTF16Converter::getWin32Format() const +{ + return CF_UNICODETEXT; +} + +CString +CMSWindowsClipboardUTF16Converter::doFromIClipboard(const CString& data) const +{ + // convert and add nul terminator + return CUnicode::UTF8ToUTF16(data).append(sizeof(wchar_t), 0); +} + +CString +CMSWindowsClipboardUTF16Converter::doToIClipboard(const CString& data) const +{ + // convert and strip nul terminator + CString dst = CUnicode::UTF16ToUTF8(data); + CString::size_type n = dst.find('\0'); + if (n != CString::npos) { + dst.erase(n); + } + return dst; +} diff --git a/src/lib/platform/CMSWindowsClipboardUTF16Converter.h b/src/lib/platform/CMSWindowsClipboardUTF16Converter.h new file mode 100644 index 00000000..2b7a15e7 --- /dev/null +++ b/src/lib/platform/CMSWindowsClipboardUTF16Converter.h @@ -0,0 +1,39 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSCLIPBOARDUTF16CONVERTER_H +#define CMSWINDOWSCLIPBOARDUTF16CONVERTER_H + +#include "CMSWindowsClipboardAnyTextConverter.h" + +//! Convert to/from UTF-16 encoding +class CMSWindowsClipboardUTF16Converter : + public CMSWindowsClipboardAnyTextConverter { +public: + CMSWindowsClipboardUTF16Converter(); + virtual ~CMSWindowsClipboardUTF16Converter(); + + // IMSWindowsClipboardConverter overrides + virtual UINT getWin32Format() const; + +protected: + // CMSWindowsClipboardAnyTextConverter overrides + virtual CString doFromIClipboard(const CString&) const; + virtual CString doToIClipboard(const CString&) const; +}; + +#endif diff --git a/src/lib/platform/CMSWindowsDebugOutputter.cpp b/src/lib/platform/CMSWindowsDebugOutputter.cpp new file mode 100644 index 00000000..571d0ed5 --- /dev/null +++ b/src/lib/platform/CMSWindowsDebugOutputter.cpp @@ -0,0 +1,58 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CMSWindowsDebugOutputter.h" + +#define WIN32_LEAN_AND_MEAN +#include + +#include + +CMSWindowsDebugOutputter::CMSWindowsDebugOutputter() +{ +} + +CMSWindowsDebugOutputter::~CMSWindowsDebugOutputter() +{ +} + +void +CMSWindowsDebugOutputter::open(const char* title) +{ +} + +void +CMSWindowsDebugOutputter::close() +{ +} + +void +CMSWindowsDebugOutputter::show(bool showIfEmpty) +{ +} + +bool +CMSWindowsDebugOutputter::write(ELevel level, const char* msg) +{ + OutputDebugString((std::string(msg) + "\n").c_str()); + return true; +} + +void +CMSWindowsDebugOutputter::flush() +{ +} diff --git a/src/lib/platform/CMSWindowsDebugOutputter.h b/src/lib/platform/CMSWindowsDebugOutputter.h new file mode 100644 index 00000000..caadc416 --- /dev/null +++ b/src/lib/platform/CMSWindowsDebugOutputter.h @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "ILogOutputter.h" + +//! Write log to debugger +/*! +This outputter writes output to the debugger. In Visual Studio, this +can be seen in the Output window. +*/ +class CMSWindowsDebugOutputter : public ILogOutputter { +public: + CMSWindowsDebugOutputter(); + virtual ~CMSWindowsDebugOutputter(); + + // ILogOutputter overrides + virtual void open(const char* title); + virtual void close(); + virtual void show(bool showIfEmpty); + virtual bool write(ELevel level, const char* message); + virtual void flush(); +}; diff --git a/src/lib/platform/CMSWindowsDesks.cpp b/src/lib/platform/CMSWindowsDesks.cpp new file mode 100644 index 00000000..ee94672c --- /dev/null +++ b/src/lib/platform/CMSWindowsDesks.cpp @@ -0,0 +1,1043 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsDesks.h" +#include "CMSWindowsScreen.h" +#include "CSynergyHook.h" +#include "IScreenSaver.h" +#include "XScreen.h" +#include "CLock.h" +#include "CThread.h" +#include "CLog.h" +#include "IEventQueue.h" +#include "IJob.h" +#include "TMethodEventJob.h" +#include "TMethodJob.h" +#include "CArchMiscWindows.h" +#include + +// these are only defined when WINVER >= 0x0500 +#if !defined(SPI_GETMOUSESPEED) +#define SPI_GETMOUSESPEED 112 +#endif +#if !defined(SPI_SETMOUSESPEED) +#define SPI_SETMOUSESPEED 113 +#endif +#if !defined(SPI_GETSCREENSAVERRUNNING) +#define SPI_GETSCREENSAVERRUNNING 114 +#endif + +// X button stuff +#if !defined(WM_XBUTTONDOWN) +#define WM_XBUTTONDOWN 0x020B +#define WM_XBUTTONUP 0x020C +#define WM_XBUTTONDBLCLK 0x020D +#define WM_NCXBUTTONDOWN 0x00AB +#define WM_NCXBUTTONUP 0x00AC +#define WM_NCXBUTTONDBLCLK 0x00AD +#define MOUSEEVENTF_XDOWN 0x0080 +#define MOUSEEVENTF_XUP 0x0100 +#define XBUTTON1 0x0001 +#define XBUTTON2 0x0002 +#endif +#if !defined(VK_XBUTTON1) +#define VK_XBUTTON1 0x05 +#define VK_XBUTTON2 0x06 +#endif + +// ; +#define SYNERGY_MSG_SWITCH SYNERGY_HOOK_LAST_MSG + 1 +// ; +#define SYNERGY_MSG_ENTER SYNERGY_HOOK_LAST_MSG + 2 +// ; +#define SYNERGY_MSG_LEAVE SYNERGY_HOOK_LAST_MSG + 3 +// wParam = flags, HIBYTE(lParam) = virtual key, LOBYTE(lParam) = scan code +#define SYNERGY_MSG_FAKE_KEY SYNERGY_HOOK_LAST_MSG + 4 + // flags, XBUTTON id +#define SYNERGY_MSG_FAKE_BUTTON SYNERGY_HOOK_LAST_MSG + 5 +// x; y +#define SYNERGY_MSG_FAKE_MOVE SYNERGY_HOOK_LAST_MSG + 6 +// xDelta; yDelta +#define SYNERGY_MSG_FAKE_WHEEL SYNERGY_HOOK_LAST_MSG + 7 +// POINT*; +#define SYNERGY_MSG_CURSOR_POS SYNERGY_HOOK_LAST_MSG + 8 +// IKeyState*; +#define SYNERGY_MSG_SYNC_KEYS SYNERGY_HOOK_LAST_MSG + 9 +// install; +#define SYNERGY_MSG_SCREENSAVER SYNERGY_HOOK_LAST_MSG + 10 +// dx; dy +#define SYNERGY_MSG_FAKE_REL_MOVE SYNERGY_HOOK_LAST_MSG + 11 +// enable; +#define SYNERGY_MSG_FAKE_INPUT SYNERGY_HOOK_LAST_MSG + 12 + +// +// CMSWindowsDesks +// + +CMSWindowsDesks::CMSWindowsDesks( + bool isPrimary, bool noHooks, HINSTANCE hookLibrary, + const IScreenSaver* screensaver, IJob* updateKeys) : + m_isPrimary(isPrimary), + m_noHooks(noHooks), + m_is95Family(CArchMiscWindows::isWindows95Family()), + m_isModernFamily(CArchMiscWindows::isWindowsModern()), + m_isOnScreen(m_isPrimary), + m_x(0), m_y(0), + m_w(0), m_h(0), + m_xCenter(0), m_yCenter(0), + m_multimon(false), + m_timer(NULL), + m_screensaver(screensaver), + m_screensaverNotify(false), + m_activeDesk(NULL), + m_activeDeskName(), + m_mutex(), + m_deskReady(&m_mutex, false), + m_updateKeys(updateKeys) +{ + if (hookLibrary != NULL) + queryHookLibrary(hookLibrary); + + m_cursor = createBlankCursor(); + m_deskClass = createDeskWindowClass(m_isPrimary); + m_keyLayout = GetKeyboardLayout(GetCurrentThreadId()); + resetOptions(); +} + +CMSWindowsDesks::~CMSWindowsDesks() +{ + disable(); + destroyClass(m_deskClass); + destroyCursor(m_cursor); + delete m_updateKeys; +} + +void +CMSWindowsDesks::enable() +{ + m_threadID = GetCurrentThreadId(); + + // set the active desk and (re)install the hooks + checkDesk(); + + // install the desk timer. this timer periodically checks + // which desk is active and reinstalls the hooks as necessary. + // we wouldn't need this if windows notified us of a desktop + // change but as far as i can tell it doesn't. + m_timer = EVENTQUEUE->newTimer(0.2, NULL); + EVENTQUEUE->adoptHandler(CEvent::kTimer, m_timer, + new TMethodEventJob( + this, &CMSWindowsDesks::handleCheckDesk)); + + updateKeys(); +} + +void +CMSWindowsDesks::disable() +{ + // remove timer + if (m_timer != NULL) { + EVENTQUEUE->removeHandler(CEvent::kTimer, m_timer); + EVENTQUEUE->deleteTimer(m_timer); + m_timer = NULL; + } + + // destroy desks + removeDesks(); + + m_isOnScreen = m_isPrimary; +} + +void +CMSWindowsDesks::enter() +{ + sendMessage(SYNERGY_MSG_ENTER, 0, 0); +} + +void +CMSWindowsDesks::leave(HKL keyLayout) +{ + sendMessage(SYNERGY_MSG_LEAVE, (WPARAM)keyLayout, 0); +} + +void +CMSWindowsDesks::resetOptions() +{ + m_leaveForegroundOption = false; +} + +void +CMSWindowsDesks::setOptions(const COptionsList& options) +{ + for (UInt32 i = 0, n = (UInt32)options.size(); i < n; i += 2) { + if (options[i] == kOptionWin32KeepForeground) { + m_leaveForegroundOption = (options[i + 1] != 0); + LOG((CLOG_DEBUG1 "%s the foreground window", m_leaveForegroundOption ? "Don\'t grab" : "Grab")); + } + } +} + +void +CMSWindowsDesks::updateKeys() +{ + sendMessage(SYNERGY_MSG_SYNC_KEYS, 0, 0); +} + +void +CMSWindowsDesks::setShape(SInt32 x, SInt32 y, + SInt32 width, SInt32 height, + SInt32 xCenter, SInt32 yCenter, bool isMultimon) +{ + m_x = x; + m_y = y; + m_w = width; + m_h = height; + m_xCenter = xCenter; + m_yCenter = yCenter; + m_multimon = isMultimon; +} + +void +CMSWindowsDesks::installScreensaverHooks(bool install) +{ + if (m_isPrimary && m_screensaverNotify != install) { + m_screensaverNotify = install; + sendMessage(SYNERGY_MSG_SCREENSAVER, install, 0); + } +} + +void +CMSWindowsDesks::fakeInputBegin() +{ + sendMessage(SYNERGY_MSG_FAKE_INPUT, 1, 0); +} + +void +CMSWindowsDesks::fakeInputEnd() +{ + sendMessage(SYNERGY_MSG_FAKE_INPUT, 0, 0); +} + +void +CMSWindowsDesks::getCursorPos(SInt32& x, SInt32& y) const +{ + POINT pos; + sendMessage(SYNERGY_MSG_CURSOR_POS, reinterpret_cast(&pos), 0); + x = pos.x; + y = pos.y; +} + +void +CMSWindowsDesks::fakeKeyEvent( + KeyButton button, UINT virtualKey, + bool press, bool /*isAutoRepeat*/) const +{ + // win 95 family doesn't understand handed modifier virtual keys + if (m_is95Family) { + switch (virtualKey) { + case VK_LSHIFT: + case VK_RSHIFT: + virtualKey = VK_SHIFT; + break; + + case VK_LCONTROL: + case VK_RCONTROL: + virtualKey = VK_CONTROL; + break; + + case VK_LMENU: + case VK_RMENU: + virtualKey = VK_MENU; + break; + } + } + + // synthesize event + DWORD flags = 0; + if (((button & 0x100u) != 0)) { + flags |= KEYEVENTF_EXTENDEDKEY; + } + if (!press) { + flags |= KEYEVENTF_KEYUP; + } + sendMessage(SYNERGY_MSG_FAKE_KEY, flags, + MAKEWORD(static_cast(button & 0xffu), + static_cast(virtualKey & 0xffu))); +} + +void +CMSWindowsDesks::fakeMouseButton(ButtonID button, bool press) +{ + // the system will swap the meaning of left/right for us if + // the user has configured a left-handed mouse but we don't + // want it to swap since we want the handedness of the + // server's mouse. so pre-swap for a left-handed mouse. + if (GetSystemMetrics(SM_SWAPBUTTON)) { + switch (button) { + case kButtonLeft: + button = kButtonRight; + break; + + case kButtonRight: + button = kButtonLeft; + break; + } + } + + // map button id to button flag and button data + DWORD data = 0; + DWORD flags; + switch (button) { + case kButtonLeft: + flags = press ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP; + break; + + case kButtonMiddle: + flags = press ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP; + break; + + case kButtonRight: + flags = press ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP; + break; + + case kButtonExtra0 + 0: + data = XBUTTON1; + flags = press ? MOUSEEVENTF_XDOWN : MOUSEEVENTF_XUP; + break; + + case kButtonExtra0 + 1: + data = XBUTTON2; + flags = press ? MOUSEEVENTF_XDOWN : MOUSEEVENTF_XUP; + break; + + default: + return; + } + + // do it + sendMessage(SYNERGY_MSG_FAKE_BUTTON, flags, data); +} + +void +CMSWindowsDesks::fakeMouseMove(SInt32 x, SInt32 y) const +{ + sendMessage(SYNERGY_MSG_FAKE_MOVE, + static_cast(x), + static_cast(y)); +} + +void +CMSWindowsDesks::fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const +{ + sendMessage(SYNERGY_MSG_FAKE_REL_MOVE, + static_cast(dx), + static_cast(dy)); +} + +void +CMSWindowsDesks::fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const +{ + sendMessage(SYNERGY_MSG_FAKE_WHEEL, xDelta, yDelta); +} + +void +CMSWindowsDesks::sendMessage(UINT msg, WPARAM wParam, LPARAM lParam) const +{ + if (m_activeDesk != NULL && m_activeDesk->m_window != NULL) { + PostThreadMessage(m_activeDesk->m_threadID, msg, wParam, lParam); + waitForDesk(); + } +} + +void +CMSWindowsDesks::queryHookLibrary(HINSTANCE hookLibrary) +{ + // look up functions + if (m_isPrimary && !m_noHooks) { + m_install = (InstallFunc)GetProcAddress(hookLibrary, "install"); + m_uninstall = (UninstallFunc)GetProcAddress(hookLibrary, "uninstall"); + m_installScreensaver = + (InstallScreenSaverFunc)GetProcAddress( + hookLibrary, "installScreenSaver"); + m_uninstallScreensaver = + (UninstallScreenSaverFunc)GetProcAddress( + hookLibrary, "uninstallScreenSaver"); + if (m_install == NULL || + m_uninstall == NULL || + m_installScreensaver == NULL || + m_uninstallScreensaver == NULL) { + LOG((CLOG_ERR "Invalid hook library")); + throw XScreenOpenFailure(); + } + } + else { + m_install = NULL; + m_uninstall = NULL; + m_installScreensaver = NULL; + m_uninstallScreensaver = NULL; + } +} + +HCURSOR +CMSWindowsDesks::createBlankCursor() const +{ + // create a transparent cursor + int cw = GetSystemMetrics(SM_CXCURSOR); + int ch = GetSystemMetrics(SM_CYCURSOR); + UInt8* cursorAND = new UInt8[ch * ((cw + 31) >> 2)]; + UInt8* cursorXOR = new UInt8[ch * ((cw + 31) >> 2)]; + memset(cursorAND, 0xff, ch * ((cw + 31) >> 2)); + memset(cursorXOR, 0x00, ch * ((cw + 31) >> 2)); + HCURSOR c = CreateCursor(CMSWindowsScreen::getWindowInstance(), + 0, 0, cw, ch, cursorAND, cursorXOR); + delete[] cursorXOR; + delete[] cursorAND; + return c; +} + +void +CMSWindowsDesks::destroyCursor(HCURSOR cursor) const +{ + if (cursor != NULL) { + DestroyCursor(cursor); + } +} + +ATOM +CMSWindowsDesks::createDeskWindowClass(bool isPrimary) const +{ + WNDCLASSEX classInfo; + classInfo.cbSize = sizeof(classInfo); + classInfo.style = CS_DBLCLKS | CS_NOCLOSE; + classInfo.lpfnWndProc = isPrimary ? + &CMSWindowsDesks::primaryDeskProc : + &CMSWindowsDesks::secondaryDeskProc; + classInfo.cbClsExtra = 0; + classInfo.cbWndExtra = 0; + classInfo.hInstance = CMSWindowsScreen::getWindowInstance(); + classInfo.hIcon = NULL; + classInfo.hCursor = m_cursor; + classInfo.hbrBackground = NULL; + classInfo.lpszMenuName = NULL; + classInfo.lpszClassName = "SynergyDesk"; + classInfo.hIconSm = NULL; + return RegisterClassEx(&classInfo); +} + +void +CMSWindowsDesks::destroyClass(ATOM windowClass) const +{ + if (windowClass != 0) { + UnregisterClass(reinterpret_cast(windowClass), + CMSWindowsScreen::getWindowInstance()); + } +} + +HWND +CMSWindowsDesks::createWindow(ATOM windowClass, const char* name) const +{ + HWND window = CreateWindowEx(WS_EX_TOPMOST | + WS_EX_TRANSPARENT | + WS_EX_TOOLWINDOW, + reinterpret_cast(windowClass), + name, + WS_POPUP, + 0, 0, 1, 1, + NULL, NULL, + CMSWindowsScreen::getWindowInstance(), + NULL); + if (window == NULL) { + LOG((CLOG_ERR "failed to create window: %d", GetLastError())); + throw XScreenOpenFailure(); + } + return window; +} + +void +CMSWindowsDesks::destroyWindow(HWND hwnd) const +{ + if (hwnd != NULL) { + DestroyWindow(hwnd); + } +} + +LRESULT CALLBACK +CMSWindowsDesks::primaryDeskProc( + HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +LRESULT CALLBACK +CMSWindowsDesks::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); + } + + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +void +CMSWindowsDesks::deskMouseMove(SInt32 x, SInt32 y) const +{ + // motion is simple (i.e. it's on the primary monitor) if there + // is only one monitor. it's also simple if we're not on the + // windows 95 family since those platforms don't have a broken + // mouse_event() function (see the comment below). + bool simple = (!m_multimon || !m_is95Family); + if (!simple) { + // also simple if motion is within the primary monitor + simple = (x >= 0 && x < GetSystemMetrics(SM_CXSCREEN) && + y >= 0 && y < GetSystemMetrics(SM_CYSCREEN)); + } + + // move the mouse directly to target position if motion is simple + if (simple) { + // when using absolute positioning with mouse_event(), + // the normalized device coordinates range over only + // the primary screen. + SInt32 w = GetSystemMetrics(SM_CXSCREEN); + SInt32 h = GetSystemMetrics(SM_CYSCREEN); + mouse_event(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, + (DWORD)((65535.0f * x) / (w - 1) + 0.5f), + (DWORD)((65535.0f * y) / (h - 1) + 0.5f), + 0, 0); + } + + // windows 98 and Me are broken. you cannot set the absolute + // position of the mouse except on the primary monitor but you + // can do relative moves onto any monitor. this is, in microsoft's + // words, "by design." apparently the designers of windows 2000 + // we're a little less lazy and did it right. + // + // microsoft recommends in Q193003 to absolute position the cursor + // somewhere on the primary monitor then relative move to the + // desired location. this doesn't work for us because when the + // user drags a scrollbar, a window, etc. it causes the dragged + // item to jump back and forth between the position on the primary + // monitor and the desired position. while it always ends up in + // the right place, the effect is disconcerting. + // + // instead we'll get the cursor's current position and do just a + // relative move from there to the desired position. + else { + POINT pos; + GetCursorPos(&pos); + deskMouseRelativeMove(x - pos.x, y - pos.y); + } +} + +void +CMSWindowsDesks::deskMouseRelativeMove(SInt32 dx, SInt32 dy) const +{ + // relative moves are subject to cursor acceleration which we don't + // want.so we disable acceleration, do the relative move, then + // restore acceleration. there's a slight chance we'll end up in + // the wrong place if the user moves the cursor using this system's + // mouse while simultaneously moving the mouse on the server + // system. that defeats the purpose of synergy so we'll assume + // that won't happen. even if it does, the next mouse move will + // correct the position. + + // save mouse speed & acceleration + int oldSpeed[4]; + bool accelChanged = + SystemParametersInfo(SPI_GETMOUSE,0, oldSpeed, 0) && + SystemParametersInfo(SPI_GETMOUSESPEED, 0, oldSpeed + 3, 0); + + // use 1:1 motion + if (accelChanged) { + int newSpeed[4] = { 0, 0, 0, 1 }; + accelChanged = + SystemParametersInfo(SPI_SETMOUSE, 0, newSpeed, 0) || + SystemParametersInfo(SPI_SETMOUSESPEED, 0, newSpeed + 3, 0); + } + + // move relative to mouse position + mouse_event(MOUSEEVENTF_MOVE, dx, dy, 0, 0); + + // restore mouse speed & acceleration + if (accelChanged) { + SystemParametersInfo(SPI_SETMOUSE, 0, oldSpeed, 0); + SystemParametersInfo(SPI_SETMOUSESPEED, 0, oldSpeed + 3, 0); + } +} + +void +CMSWindowsDesks::deskEnter(CDesk* desk) +{ + if (!m_isPrimary) { + ReleaseCapture(); + } + ShowCursor(TRUE); + SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | + SWP_NOACTIVATE | SWP_HIDEWINDOW); + + // restore the foreground window + // XXX -- this raises the window to the top of the Z-order. we + // want it to stay wherever it was to properly support X-mouse + // (mouse over activation) but i've no idea how to do that. + // the obvious workaround of using SetWindowPos() to move it back + // after being raised doesn't work. + DWORD thisThread = + GetWindowThreadProcessId(desk->m_window, NULL); + DWORD thatThread = + GetWindowThreadProcessId(desk->m_foregroundWindow, NULL); + AttachThreadInput(thatThread, thisThread, TRUE); + SetForegroundWindow(desk->m_foregroundWindow); + AttachThreadInput(thatThread, thisThread, FALSE); + EnableWindow(desk->m_window, desk->m_lowLevel ? FALSE : TRUE); + desk->m_foregroundWindow = NULL; +} + +void +CMSWindowsDesks::deskLeave(CDesk* desk, HKL keyLayout) +{ + ShowCursor(FALSE); + if (m_isPrimary) { + // map a window to hide the cursor and to use whatever keyboard + // layout we choose rather than the keyboard layout of the last + // active window. + int x, y, w, h; + if (desk->m_lowLevel) { + // with a low level hook the cursor will never budge so + // just a 1x1 window is sufficient. + x = m_xCenter; + y = m_yCenter; + w = 1; + h = 1; + } + else { + // with regular hooks the cursor will jitter as it's moved + // by the user then back to the center by us. to be sure + // we never lose it, cover all the monitors with the window. + x = m_x; + y = m_y; + w = m_w; + h = m_h; + } + SetWindowPos(desk->m_window, HWND_TOPMOST, x, y, w, h, + SWP_NOACTIVATE | SWP_SHOWWINDOW); + + // if not using low-level hooks we have to also activate the + // window to ensure we don't lose keyboard focus. + // FIXME -- see if this can be avoided. if so then always + // disable the window (see handling of SYNERGY_MSG_SWITCH). + if (!desk->m_lowLevel) { + SetActiveWindow(desk->m_window); + } + + // if using low-level hooks then disable the foreground window + // so it can't mess up any of our keyboard events. the console + // program, for example, will cause characters to be reported as + // unshifted, regardless of the shift key state. interestingly + // we do see the shift key go down and up. + // + // note that we must enable the window to activate it and we + // need to disable the window on deskEnter. + else { + desk->m_foregroundWindow = getForegroundWindow(); + if (desk->m_foregroundWindow != NULL) { + EnableWindow(desk->m_window, TRUE); + SetActiveWindow(desk->m_window); + DWORD thisThread = + GetWindowThreadProcessId(desk->m_window, NULL); + DWORD thatThread = + GetWindowThreadProcessId(desk->m_foregroundWindow, NULL); + AttachThreadInput(thatThread, thisThread, TRUE); + SetForegroundWindow(desk->m_window); + AttachThreadInput(thatThread, thisThread, FALSE); + } + } + + // switch to requested keyboard layout + ActivateKeyboardLayout(keyLayout, 0); + } + else { + // move hider window under the cursor center, raise, and show it + SetWindowPos(desk->m_window, HWND_TOPMOST, + m_xCenter, m_yCenter, 1, 1, + 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); + + // warp the mouse to the cursor center + LOG((CLOG_DEBUG2 "warping cursor to center: %+d,%+d", m_xCenter, m_yCenter)); + deskMouseMove(m_xCenter, m_yCenter); + } +} + +void +CMSWindowsDesks::deskThread(void* vdesk) +{ + MSG msg; + + // use given desktop for this thread + CDesk* desk = reinterpret_cast(vdesk); + desk->m_threadID = GetCurrentThreadId(); + desk->m_window = NULL; + desk->m_foregroundWindow = NULL; + if (desk->m_desk != NULL && SetThreadDesktop(desk->m_desk) != 0) { + // create a message queue + PeekMessage(&msg, NULL, 0,0, PM_NOREMOVE); + + // create a window. we use this window to hide the cursor. + try { + desk->m_window = createWindow(m_deskClass, "SynergyDesk"); + LOG((CLOG_DEBUG "desk %s window is 0x%08x", desk->m_name.c_str(), desk->m_window)); + } + catch (...) { + // ignore + LOG((CLOG_DEBUG "can't create desk window for %s", desk->m_name.c_str())); + } + } + + // tell main thread that we're ready + { + CLock lock(&m_mutex); + m_deskReady = true; + m_deskReady.broadcast(); + } + + while (GetMessage(&msg, NULL, 0, 0)) { + switch (msg.message) { + default: + TranslateMessage(&msg); + DispatchMessage(&msg); + continue; + + case SYNERGY_MSG_SWITCH: + if (m_isPrimary && !m_noHooks) { + m_uninstall(); + if (m_screensaverNotify) { + m_uninstallScreensaver(); + m_installScreensaver(); + } + switch (m_install()) { + case kHOOK_FAILED: + // we won't work on this desk + desk->m_lowLevel = false; + break; + + case kHOOK_OKAY: + desk->m_lowLevel = false; + break; + + case kHOOK_OKAY_LL: + desk->m_lowLevel = true; + break; + } + + // a window on the primary screen with low-level hooks + // should never activate. + if (desk->m_window) + EnableWindow(desk->m_window, desk->m_lowLevel ? FALSE : TRUE); + } + break; + + case SYNERGY_MSG_ENTER: + m_isOnScreen = true; + deskEnter(desk); + break; + + case SYNERGY_MSG_LEAVE: + m_isOnScreen = false; + m_keyLayout = (HKL)msg.wParam; + deskLeave(desk, m_keyLayout); + break; + + case SYNERGY_MSG_FAKE_KEY: + keybd_event(HIBYTE(msg.lParam), LOBYTE(msg.lParam), (DWORD)msg.wParam, 0); + break; + + case SYNERGY_MSG_FAKE_BUTTON: + if (msg.wParam != 0) { + mouse_event((DWORD)msg.wParam, 0, 0, (DWORD)msg.lParam, 0); + } + break; + + case SYNERGY_MSG_FAKE_MOVE: + deskMouseMove(static_cast(msg.wParam), + static_cast(msg.lParam)); + break; + + case SYNERGY_MSG_FAKE_REL_MOVE: + deskMouseRelativeMove(static_cast(msg.wParam), + static_cast(msg.lParam)); + break; + + case SYNERGY_MSG_FAKE_WHEEL: + // XXX -- add support for x-axis scrolling + if (msg.lParam != 0) { + mouse_event(MOUSEEVENTF_WHEEL, 0, 0, (DWORD)msg.lParam, 0); + } + break; + + case SYNERGY_MSG_CURSOR_POS: { + POINT* pos = reinterpret_cast(msg.wParam); + if (!GetCursorPos(pos)) { + pos->x = m_xCenter; + pos->y = m_yCenter; + } + break; + } + + case SYNERGY_MSG_SYNC_KEYS: + m_updateKeys->run(); + break; + + case SYNERGY_MSG_SCREENSAVER: + if (!m_noHooks) { + if (msg.wParam != 0) { + m_installScreensaver(); + } + else { + m_uninstallScreensaver(); + } + } + break; + + case SYNERGY_MSG_FAKE_INPUT: + keybd_event(SYNERGY_HOOK_FAKE_INPUT_VIRTUAL_KEY, + SYNERGY_HOOK_FAKE_INPUT_SCANCODE, + msg.wParam ? 0 : KEYEVENTF_KEYUP, 0); + break; + } + + // notify that message was processed + CLock lock(&m_mutex); + m_deskReady = true; + m_deskReady.broadcast(); + } + + // clean up + deskEnter(desk); + if (desk->m_window != NULL) { + DestroyWindow(desk->m_window); + } + if (desk->m_desk != NULL) { + closeDesktop(desk->m_desk); + } +} + +CMSWindowsDesks::CDesk* +CMSWindowsDesks::addDesk(const CString& name, HDESK hdesk) +{ + CDesk* desk = new CDesk; + desk->m_name = name; + desk->m_desk = hdesk; + desk->m_targetID = GetCurrentThreadId(); + desk->m_thread = new CThread(new TMethodJob( + this, &CMSWindowsDesks::deskThread, desk)); + waitForDesk(); + m_desks.insert(std::make_pair(name, desk)); + return desk; +} + +void +CMSWindowsDesks::removeDesks() +{ + for (CDesks::iterator index = m_desks.begin(); + index != m_desks.end(); ++index) { + CDesk* desk = index->second; + PostThreadMessage(desk->m_threadID, WM_QUIT, 0, 0); + desk->m_thread->wait(); + delete desk->m_thread; + delete desk; + } + m_desks.clear(); + m_activeDesk = NULL; + m_activeDeskName = ""; +} + +void +CMSWindowsDesks::checkDesk() +{ + // get current desktop. if we already know about it then return. + CDesk* desk; + HDESK hdesk = openInputDesktop(); + CString name = getDesktopName(hdesk); + CDesks::const_iterator index = m_desks.find(name); + if (index == m_desks.end()) { + desk = addDesk(name, hdesk); + // hold on to hdesk until thread exits so the desk can't + // be removed by the system + } + else { + closeDesktop(hdesk); + desk = index->second; + } + + // if active desktop changed then tell the old and new desk threads + // about the change. don't switch desktops when the screensaver is + // active becaue we'd most likely switch to the screensaver desktop + // which would have the side effect of forcing the screensaver to + // stop. + if (name != m_activeDeskName && !m_screensaver->isActive()) { + // show cursor on previous desk + bool wasOnScreen = m_isOnScreen; + if (!wasOnScreen) { + sendMessage(SYNERGY_MSG_ENTER, 0, 0); + } + + // check for desk accessibility change. we don't get events + // from an inaccessible desktop so when we switch from an + // inaccessible desktop to an accessible one we have to + // update the keyboard state. + LOG((CLOG_DEBUG "switched to desk \"%s\"", name.c_str())); + bool syncKeys = false; + bool isAccessible = isDeskAccessible(desk); + if (isDeskAccessible(m_activeDesk) != isAccessible) { + if (isAccessible) { + LOG((CLOG_DEBUG "desktop is now accessible")); + syncKeys = true; + } + else { + LOG((CLOG_DEBUG "desktop is now inaccessible")); + } + } + + // switch desk + m_activeDesk = desk; + m_activeDeskName = name; + sendMessage(SYNERGY_MSG_SWITCH, 0, 0); + + // hide cursor on new desk + if (!wasOnScreen) { + sendMessage(SYNERGY_MSG_LEAVE, (WPARAM)m_keyLayout, 0); + } + + // update keys if necessary + if (syncKeys) { + updateKeys(); + } + } + else if (name != m_activeDeskName) { + // screen saver might have started + PostThreadMessage(m_threadID, SYNERGY_MSG_SCREEN_SAVER, TRUE, 0); + } +} + +bool +CMSWindowsDesks::isDeskAccessible(const CDesk* desk) const +{ + return (desk != NULL && desk->m_desk != NULL); +} + +void +CMSWindowsDesks::waitForDesk() const +{ + CMSWindowsDesks* self = const_cast(this); + + CLock lock(&m_mutex); + while (!(bool)m_deskReady) { + m_deskReady.wait(); + } + self->m_deskReady = false; +} + +void +CMSWindowsDesks::handleCheckDesk(const CEvent&, void*) +{ + checkDesk(); + + // also check if screen saver is running if on a modern OS and + // this is the primary screen. + if (m_isPrimary && m_isModernFamily) { + BOOL running; + SystemParametersInfo(SPI_GETSCREENSAVERRUNNING, 0, &running, FALSE); + PostThreadMessage(m_threadID, SYNERGY_MSG_SCREEN_SAVER, running, 0); + } +} + +HDESK +CMSWindowsDesks::openInputDesktop() +{ + if (m_is95Family) { + // there's only one desktop on windows 95 et al. + return GetThreadDesktop(GetCurrentThreadId()); + } + else { + return OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, TRUE, + DESKTOP_CREATEWINDOW | + DESKTOP_HOOKCONTROL | + GENERIC_WRITE); + } +} + +void +CMSWindowsDesks::closeDesktop(HDESK desk) +{ + // on 95/98/me we don't need to close the desktop returned by + // openInputDesktop(). + if (desk != NULL && !m_is95Family) { + CloseDesktop(desk); + } +} + +CString +CMSWindowsDesks::getDesktopName(HDESK desk) +{ + if (desk == NULL) { + return CString(); + } + else if (m_is95Family) { + return "desktop"; + } + else { + DWORD size; + GetUserObjectInformation(desk, UOI_NAME, NULL, 0, &size); + TCHAR* name = (TCHAR*)alloca(size + sizeof(TCHAR)); + GetUserObjectInformation(desk, UOI_NAME, name, size, &size); + CString result(name); + return result; + } +} + +HWND +CMSWindowsDesks::getForegroundWindow() const +{ + // Ideally we'd return NULL as much as possible, only returning + // the actual foreground window when we know it's going to mess + // up our keyboard input. For now we'll just let the user + // decide. + if (m_leaveForegroundOption) { + return NULL; + } + return GetForegroundWindow(); +} diff --git a/src/lib/platform/CMSWindowsDesks.h b/src/lib/platform/CMSWindowsDesks.h new file mode 100644 index 00000000..0029c7d5 --- /dev/null +++ b/src/lib/platform/CMSWindowsDesks.h @@ -0,0 +1,303 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSDESKS_H +#define CMSWINDOWSDESKS_H + +#include "CSynergyHook.h" +#include "KeyTypes.h" +#include "MouseTypes.h" +#include "OptionTypes.h" +#include "CCondVar.h" +#include "CMutex.h" +#include "CString.h" +#include "stdmap.h" +#define WIN32_LEAN_AND_MEAN +#include + +class CEvent; +class CEventQueueTimer; +class CThread; +class IJob; +class IScreenSaver; + +//! Microsoft Windows desk handling +/*! +Desks in Microsoft Windows are only remotely like desktops on X11 +systems. A desk is another virtual surface for windows but desks +impose serious restrictions: a thread can interact with only one +desk at a time, you can't switch desks if the thread has any hooks +installed or owns any windows, windows cannot exist on multiple +desks at once, etc. Basically, they're useless except for running +the login window or the screensaver, which is what they're used +for. Synergy must deal with them mainly because of the login +window and screensaver but users can create their own desks and +synergy should work on those too. + +This class encapsulates all the desk nastiness. Clients of this +object don't have to know anything about desks. +*/ +class CMSWindowsDesks { +public: + //! Constructor + /*! + \p isPrimary is true iff the desk is for a primary screen. + \p screensaver points to a screensaver object and it's used + only to check if the screensaver is active. The \p updateKeys + job is adopted and is called when the key state should be + updated in a thread attached to the current desk. + \p hookLibrary must be a handle to the hook library. + */ + CMSWindowsDesks(bool isPrimary, bool noHooks, HINSTANCE hookLibrary, + const IScreenSaver* screensaver, IJob* updateKeys); + ~CMSWindowsDesks(); + + //! @name manipulators + //@{ + + //! Enable desk tracking + /*! + Enables desk tracking. While enabled, this object checks to see + if the desk has changed and ensures that the hooks are installed + on the new desk. \c setShape should be called at least once + before calling \c enable. + */ + void enable(); + + //! Disable desk tracking + /*! + Disables desk tracking. \sa enable. + */ + void disable(); + + //! Notify of entering a desk + /*! + Prepares a desk for when the cursor enters it. + */ + void enter(); + + //! Notify of leaving a desk + /*! + Prepares a desk for when the cursor leaves it. + */ + void leave(HKL keyLayout); + + //! Notify of options changes + /*! + Resets all options to their default values. + */ + void resetOptions(); + + //! Notify of options changes + /*! + Set options to given values. Ignores unknown options and doesn't + modify options that aren't given in \c options. + */ + void setOptions(const COptionsList& options); + + //! Update the key state + /*! + Causes the key state to get updated to reflect the physical keyboard + state and current keyboard mapping. + */ + void updateKeys(); + + //! Tell desk about new size + /*! + This tells the desks that the display size has changed. + */ + void setShape(SInt32 x, SInt32 y, + SInt32 width, SInt32 height, + SInt32 xCenter, SInt32 yCenter, bool isMultimon); + + //! Install/uninstall screensaver hooks + /*! + If \p install is true then the screensaver hooks are installed and, + if desk tracking is enabled, updated whenever the desk changes. If + \p install is false then the screensaver hooks are uninstalled. + */ + void installScreensaverHooks(bool install); + + //! Start ignoring user input + /*! + Starts ignoring user input so we don't pick up our own synthesized events. + */ + void fakeInputBegin(); + + //! Stop ignoring user input + /*! + Undoes whatever \c fakeInputBegin() did. + */ + void fakeInputEnd(); + + //@} + //! @name accessors + //@{ + + //! Get cursor position + /*! + Return the current position of the cursor in \c x and \c y. + */ + void getCursorPos(SInt32& x, SInt32& y) const; + + //! Fake key press/release + /*! + Synthesize a press or release of key \c button. + */ + void fakeKeyEvent(KeyButton button, UINT virtualKey, + bool press, bool isAutoRepeat) const; + + //! Fake mouse press/release + /*! + Synthesize a press or release of mouse button \c id. + */ + void fakeMouseButton(ButtonID id, bool press); + + //! Fake mouse move + /*! + Synthesize a mouse move to the absolute coordinates \c x,y. + */ + void fakeMouseMove(SInt32 x, SInt32 y) const; + + //! Fake mouse move + /*! + Synthesize a mouse move to the relative coordinates \c dx,dy. + */ + void fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const; + + //! Fake mouse wheel + /*! + Synthesize a mouse wheel event of amount \c delta in direction \c axis. + */ + void fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const; + + //@} + +private: + class CDesk { + public: + CString m_name; + CThread* m_thread; + DWORD m_threadID; + DWORD m_targetID; + HDESK m_desk; + HWND m_window; + HWND m_foregroundWindow; + bool m_lowLevel; + }; + typedef std::map CDesks; + + // initialization and shutdown operations + void queryHookLibrary(HINSTANCE hookLibrary); + HCURSOR createBlankCursor() const; + void destroyCursor(HCURSOR cursor) const; + ATOM createDeskWindowClass(bool isPrimary) const; + void destroyClass(ATOM windowClass) const; + HWND createWindow(ATOM windowClass, const char* name) const; + void destroyWindow(HWND) const; + + // message handlers + void deskMouseMove(SInt32 x, SInt32 y) const; + void deskMouseRelativeMove(SInt32 dx, SInt32 dy) const; + void deskEnter(CDesk* desk); + void deskLeave(CDesk* desk, HKL keyLayout); + void deskThread(void* vdesk); + void xinputThread(); + + // desk switch checking and handling + CDesk* addDesk(const CString& name, HDESK hdesk); + void removeDesks(); + void checkDesk(); + bool isDeskAccessible(const CDesk* desk) const; + void handleCheckDesk(const CEvent& event, void*); + + // communication with desk threads + void waitForDesk() const; + void sendMessage(UINT, WPARAM, LPARAM) const; + + // work around for messed up keyboard events from low-level hooks + HWND getForegroundWindow() const; + + // desk API wrappers + HDESK openInputDesktop(); + void closeDesktop(HDESK); + CString getDesktopName(HDESK); + + // our desk window procs + static LRESULT CALLBACK primaryDeskProc(HWND, UINT, WPARAM, LPARAM); + static LRESULT CALLBACK secondaryDeskProc(HWND, UINT, WPARAM, LPARAM); + +private: + // true if screen is being used as a primary screen, false otherwise + bool m_isPrimary; + + // true if hooks are not to be installed (useful for debugging) + bool m_noHooks; + + // true if windows 95/98/me + bool m_is95Family; + + // true if windows 98/2k or higher (i.e. not 95/nt) + bool m_isModernFamily; + + // true if mouse has entered the screen + bool m_isOnScreen; + + // our resources + ATOM m_deskClass; + HCURSOR m_cursor; + + // screen shape stuff + SInt32 m_x, m_y; + SInt32 m_w, m_h; + SInt32 m_xCenter, m_yCenter; + + // true if system appears to have multiple monitors + bool m_multimon; + + // the timer used to check for desktop switching + CEventQueueTimer* m_timer; + + // screen saver stuff + DWORD m_threadID; + const IScreenSaver* m_screensaver; + bool m_screensaverNotify; + + // the current desk and it's name + CDesk* m_activeDesk; + CString m_activeDeskName; + + // one desk per desktop and a cond var to communicate with it + CMutex m_mutex; + CCondVar m_deskReady; + CDesks m_desks; + + // hook library stuff + InstallFunc m_install; + UninstallFunc m_uninstall; + InstallScreenSaverFunc m_installScreensaver; + UninstallScreenSaverFunc m_uninstallScreensaver; + + // keyboard stuff + IJob* m_updateKeys; + HKL m_keyLayout; + + // options + bool m_leaveForegroundOption; +}; + +#endif diff --git a/src/lib/platform/CMSWindowsEventQueueBuffer.cpp b/src/lib/platform/CMSWindowsEventQueueBuffer.cpp new file mode 100644 index 00000000..4f64deb3 --- /dev/null +++ b/src/lib/platform/CMSWindowsEventQueueBuffer.cpp @@ -0,0 +1,141 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsEventQueueBuffer.h" +#include "CThread.h" +#include "IEventQueue.h" +#include "CArchMiscWindows.h" + +// +// CEventQueueTimer +// + +class CEventQueueTimer { }; + + +// +// CMSWindowsEventQueueBuffer +// + +CMSWindowsEventQueueBuffer::CMSWindowsEventQueueBuffer() +{ + // remember thread. we'll be posting messages to it. + m_thread = GetCurrentThreadId(); + + // create a message type for custom events + m_userEvent = RegisterWindowMessage("SYNERGY_USER_EVENT"); + + // get message type for daemon quit + m_daemonQuit = CArchMiscWindows::getDaemonQuitMessage(); + + // make sure this thread has a message queue + MSG dummy; + PeekMessage(&dummy, NULL, WM_USER, WM_USER, PM_NOREMOVE); +} + +CMSWindowsEventQueueBuffer::~CMSWindowsEventQueueBuffer() +{ + // do nothing +} + +void +CMSWindowsEventQueueBuffer::waitForEvent(double timeout) +{ + // check if messages are available first. if we don't do this then + // MsgWaitForMultipleObjects() will block even if the queue isn't + // empty if the messages in the queue were there before the last + // call to GetMessage()/PeekMessage(). + if (HIWORD(GetQueueStatus(QS_ALLINPUT)) != 0) { + return; + } + + // convert timeout + DWORD t; + if (timeout < 0.0) { + t = INFINITE; + } + else { + t = (DWORD)(1000.0 * timeout); + } + + // wait for a message. we cannot be interrupted by thread + // cancellation but that's okay because we're run in the main + // thread and we never cancel that thread. + HANDLE dummy[1]; + MsgWaitForMultipleObjects(0, dummy, FALSE, t, QS_ALLINPUT); +} + +IEventQueueBuffer::Type +CMSWindowsEventQueueBuffer::getEvent(CEvent& event, UInt32& dataID) +{ + // peek at messages first. waiting for QS_ALLINPUT will return + // if a message has been sent to our window but GetMessage will + // dispatch that message behind our backs and block. PeekMessage + // will also dispatch behind our backs but won't block. + if (!PeekMessage(&m_event, NULL, 0, 0, PM_NOREMOVE) && + !PeekMessage(&m_event, (HWND)-1, 0, 0, PM_NOREMOVE)) { + return kNone; + } + + // BOOL. yeah, right. + BOOL result = GetMessage(&m_event, NULL, 0, 0); + if (result == -1) { + return kNone; + } + else if (result == 0) { + event = CEvent(CEvent::kQuit); + return kSystem; + } + else if (m_daemonQuit != 0 && m_event.message == m_daemonQuit) { + event = CEvent(CEvent::kQuit); + return kSystem; + } + else if (m_event.message == m_userEvent) { + dataID = static_cast(m_event.wParam); + return kUser; + } + else { + event = CEvent(CEvent::kSystem, + IEventQueue::getSystemTarget(), &m_event); + return kSystem; + } +} + +bool +CMSWindowsEventQueueBuffer::addEvent(UInt32 dataID) +{ + return (PostThreadMessage(m_thread, m_userEvent, + static_cast(dataID), 0) != 0); +} + +bool +CMSWindowsEventQueueBuffer::isEmpty() const +{ + return (HIWORD(GetQueueStatus(QS_ALLINPUT)) == 0); +} + +CEventQueueTimer* +CMSWindowsEventQueueBuffer::newTimer(double, bool) const +{ + return new CEventQueueTimer; +} + +void +CMSWindowsEventQueueBuffer::deleteTimer(CEventQueueTimer* timer) const +{ + delete timer; +} diff --git a/src/lib/platform/CMSWindowsEventQueueBuffer.h b/src/lib/platform/CMSWindowsEventQueueBuffer.h new file mode 100644 index 00000000..b1b42851 --- /dev/null +++ b/src/lib/platform/CMSWindowsEventQueueBuffer.h @@ -0,0 +1,47 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSEVENTQUEUEBUFFER_H +#define CMSWINDOWSEVENTQUEUEBUFFER_H + +#include "IEventQueueBuffer.h" +#define WIN32_LEAN_AND_MEAN +#include + +//! Event queue buffer for Win32 +class CMSWindowsEventQueueBuffer : public IEventQueueBuffer { +public: + CMSWindowsEventQueueBuffer(); + virtual ~CMSWindowsEventQueueBuffer(); + + // IEventQueueBuffer overrides + virtual void waitForEvent(double timeout); + virtual Type getEvent(CEvent& event, UInt32& dataID); + virtual bool addEvent(UInt32 dataID); + virtual bool isEmpty() const; + virtual CEventQueueTimer* + newTimer(double duration, bool oneShot) const; + virtual void deleteTimer(CEventQueueTimer*) const; + +private: + DWORD m_thread; + UINT m_userEvent; + MSG m_event; + UINT m_daemonQuit; +}; + +#endif diff --git a/src/lib/platform/CMSWindowsHookLibraryLoader.cpp b/src/lib/platform/CMSWindowsHookLibraryLoader.cpp new file mode 100644 index 00000000..c6913955 --- /dev/null +++ b/src/lib/platform/CMSWindowsHookLibraryLoader.cpp @@ -0,0 +1,69 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsHookLibraryLoader.h" +#include "XScreen.h" +#include "CLog.h" + +CMSWindowsHookLibraryLoader::CMSWindowsHookLibraryLoader() : + m_init(NULL), + m_cleanup(NULL), + m_setSides(NULL), + m_setZone(NULL), + m_setMode(NULL) +{ +} + +CMSWindowsHookLibraryLoader::~CMSWindowsHookLibraryLoader() +{ + // TODO: take ownership of m_ and delete them. +} + +HINSTANCE +CMSWindowsHookLibraryLoader::openHookLibrary(const char* name) +{ + // load the hook library + HINSTANCE hookLibrary = LoadLibrary(name); + if (hookLibrary == NULL) { + LOG((CLOG_ERR "failed to load hook library, %s.dll is missing", name)); + throw XScreenOpenFailure(); + } + + // look up functions + m_setSides = (SetSidesFunc)GetProcAddress(hookLibrary, "setSides"); + m_setZone = (SetZoneFunc)GetProcAddress(hookLibrary, "setZone"); + m_setMode = (SetModeFunc)GetProcAddress(hookLibrary, "setMode"); + m_init = (InitFunc)GetProcAddress(hookLibrary, "init"); + m_cleanup = (CleanupFunc)GetProcAddress(hookLibrary, "cleanup"); + if (m_setSides == NULL || + m_setZone == NULL || + m_setMode == NULL || + m_init == NULL || + m_cleanup == NULL) { + LOG((CLOG_ERR "invalid hook library, use a newer %s.dll", name)); + throw XScreenOpenFailure(); + } + + // initialize hook library + if (m_init(GetCurrentThreadId()) == 0) { + LOG((CLOG_ERR "failed to init %s.dll, another program may be using it", name)); + LOG((CLOG_INFO "restarting your computer may solve this error")); + throw XScreenOpenFailure(); + } + + return hookLibrary; +} \ No newline at end of file diff --git a/src/lib/platform/CMSWindowsHookLibraryLoader.h b/src/lib/platform/CMSWindowsHookLibraryLoader.h new file mode 100644 index 00000000..e37fd6e0 --- /dev/null +++ b/src/lib/platform/CMSWindowsHookLibraryLoader.h @@ -0,0 +1,45 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSHOOKLIBRARYLOADER_H +#define CMSWINDOWSHOOKLIBRARYLOADER_H + +#define WIN32_LEAN_AND_MEAN +#include +#include "CSynergyHook.h" + +//! Loads Windows hook DLLs. +class CMSWindowsHookLibraryLoader +{ +public: + CMSWindowsHookLibraryLoader(); + virtual ~CMSWindowsHookLibraryLoader(); + + HINSTANCE openHookLibrary(const char* name); + + // TODO: either make these private or expose properly + InitFunc m_init; + CleanupFunc m_cleanup; + SetSidesFunc m_setSides; + SetZoneFunc m_setZone; + SetModeFunc m_setMode; + +private: + HINSTANCE m_hookLibrary; +}; + +#endif \ No newline at end of file diff --git a/src/lib/platform/CMSWindowsKeyState.cpp b/src/lib/platform/CMSWindowsKeyState.cpp new file mode 100644 index 00000000..bbccd723 --- /dev/null +++ b/src/lib/platform/CMSWindowsKeyState.cpp @@ -0,0 +1,1467 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsKeyState.h" +#include "CMSWindowsDesks.h" +#include "CThread.h" +#include "CFunctionJob.h" +#include "CLog.h" +#include "CStringUtil.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" +#include "CArchMiscWindows.h" + +// extended mouse buttons +#if !defined(VK_XBUTTON1) +#define VK_XBUTTON1 0x05 +#define VK_XBUTTON2 0x06 +#endif + +// +// CMSWindowsKeyState +// + +// map virtual keys to synergy key enumeration +const KeyID CMSWindowsKeyState::s_virtualKey[] = +{ + /* 0x000 */ { kKeyNone }, // reserved + /* 0x001 */ { kKeyNone }, // VK_LBUTTON + /* 0x002 */ { kKeyNone }, // VK_RBUTTON + /* 0x003 */ { kKeyNone }, // VK_CANCEL + /* 0x004 */ { kKeyNone }, // VK_MBUTTON + /* 0x005 */ { kKeyNone }, // VK_XBUTTON1 + /* 0x006 */ { kKeyNone }, // VK_XBUTTON2 + /* 0x007 */ { kKeyNone }, // undefined + /* 0x008 */ { kKeyBackSpace }, // VK_BACK + /* 0x009 */ { kKeyTab }, // VK_TAB + /* 0x00a */ { kKeyNone }, // undefined + /* 0x00b */ { kKeyNone }, // undefined + /* 0x00c */ { kKeyClear }, // VK_CLEAR + /* 0x00d */ { kKeyReturn }, // VK_RETURN + /* 0x00e */ { kKeyNone }, // undefined + /* 0x00f */ { kKeyNone }, // undefined + /* 0x010 */ { kKeyShift_L }, // VK_SHIFT + /* 0x011 */ { kKeyControl_L }, // VK_CONTROL + /* 0x012 */ { kKeyAlt_L }, // VK_MENU + /* 0x013 */ { kKeyPause }, // VK_PAUSE + /* 0x014 */ { kKeyCapsLock }, // VK_CAPITAL + /* 0x015 */ { kKeyHangulKana }, // VK_HANGUL, VK_KANA + /* 0x016 */ { kKeyNone }, // undefined + /* 0x017 */ { kKeyNone }, // VK_JUNJA + /* 0x018 */ { kKeyNone }, // VK_FINAL + /* 0x019 */ { kKeyHanjaKanzi }, // VK_KANJI + /* 0x01a */ { kKeyNone }, // undefined + /* 0x01b */ { kKeyEscape }, // VK_ESCAPE + /* 0x01c */ { kKeyHenkan }, // VK_CONVERT + /* 0x01d */ { kKeyNone }, // VK_NONCONVERT + /* 0x01e */ { kKeyNone }, // VK_ACCEPT + /* 0x01f */ { kKeyNone }, // VK_MODECHANGE + /* 0x020 */ { kKeyNone }, // VK_SPACE + /* 0x021 */ { kKeyKP_PageUp }, // VK_PRIOR + /* 0x022 */ { kKeyKP_PageDown },// VK_NEXT + /* 0x023 */ { kKeyKP_End }, // VK_END + /* 0x024 */ { kKeyKP_Home }, // VK_HOME + /* 0x025 */ { kKeyKP_Left }, // VK_LEFT + /* 0x026 */ { kKeyKP_Up }, // VK_UP + /* 0x027 */ { kKeyKP_Right }, // VK_RIGHT + /* 0x028 */ { kKeyKP_Down }, // VK_DOWN + /* 0x029 */ { kKeySelect }, // VK_SELECT + /* 0x02a */ { kKeyNone }, // VK_PRINT + /* 0x02b */ { kKeyExecute }, // VK_EXECUTE + /* 0x02c */ { kKeyPrint }, // VK_SNAPSHOT + /* 0x02d */ { kKeyKP_Insert }, // VK_INSERT + /* 0x02e */ { kKeyKP_Delete }, // VK_DELETE + /* 0x02f */ { kKeyHelp }, // VK_HELP + /* 0x030 */ { kKeyNone }, // VK_0 + /* 0x031 */ { kKeyNone }, // VK_1 + /* 0x032 */ { kKeyNone }, // VK_2 + /* 0x033 */ { kKeyNone }, // VK_3 + /* 0x034 */ { kKeyNone }, // VK_4 + /* 0x035 */ { kKeyNone }, // VK_5 + /* 0x036 */ { kKeyNone }, // VK_6 + /* 0x037 */ { kKeyNone }, // VK_7 + /* 0x038 */ { kKeyNone }, // VK_8 + /* 0x039 */ { kKeyNone }, // VK_9 + /* 0x03a */ { kKeyNone }, // undefined + /* 0x03b */ { kKeyNone }, // undefined + /* 0x03c */ { kKeyNone }, // undefined + /* 0x03d */ { kKeyNone }, // undefined + /* 0x03e */ { kKeyNone }, // undefined + /* 0x03f */ { kKeyNone }, // undefined + /* 0x040 */ { kKeyNone }, // undefined + /* 0x041 */ { kKeyNone }, // VK_A + /* 0x042 */ { kKeyNone }, // VK_B + /* 0x043 */ { kKeyNone }, // VK_C + /* 0x044 */ { kKeyNone }, // VK_D + /* 0x045 */ { kKeyNone }, // VK_E + /* 0x046 */ { kKeyNone }, // VK_F + /* 0x047 */ { kKeyNone }, // VK_G + /* 0x048 */ { kKeyNone }, // VK_H + /* 0x049 */ { kKeyNone }, // VK_I + /* 0x04a */ { kKeyNone }, // VK_J + /* 0x04b */ { kKeyNone }, // VK_K + /* 0x04c */ { kKeyNone }, // VK_L + /* 0x04d */ { kKeyNone }, // VK_M + /* 0x04e */ { kKeyNone }, // VK_N + /* 0x04f */ { kKeyNone }, // VK_O + /* 0x050 */ { kKeyNone }, // VK_P + /* 0x051 */ { kKeyNone }, // VK_Q + /* 0x052 */ { kKeyNone }, // VK_R + /* 0x053 */ { kKeyNone }, // VK_S + /* 0x054 */ { kKeyNone }, // VK_T + /* 0x055 */ { kKeyNone }, // VK_U + /* 0x056 */ { kKeyNone }, // VK_V + /* 0x057 */ { kKeyNone }, // VK_W + /* 0x058 */ { kKeyNone }, // VK_X + /* 0x059 */ { kKeyNone }, // VK_Y + /* 0x05a */ { kKeyNone }, // VK_Z + /* 0x05b */ { kKeySuper_L }, // VK_LWIN + /* 0x05c */ { kKeySuper_R }, // VK_RWIN + /* 0x05d */ { kKeyMenu }, // VK_APPS + /* 0x05e */ { kKeyNone }, // undefined + /* 0x05f */ { kKeySleep }, // VK_SLEEP + /* 0x060 */ { kKeyKP_0 }, // VK_NUMPAD0 + /* 0x061 */ { kKeyKP_1 }, // VK_NUMPAD1 + /* 0x062 */ { kKeyKP_2 }, // VK_NUMPAD2 + /* 0x063 */ { kKeyKP_3 }, // VK_NUMPAD3 + /* 0x064 */ { kKeyKP_4 }, // VK_NUMPAD4 + /* 0x065 */ { kKeyKP_5 }, // VK_NUMPAD5 + /* 0x066 */ { kKeyKP_6 }, // VK_NUMPAD6 + /* 0x067 */ { kKeyKP_7 }, // VK_NUMPAD7 + /* 0x068 */ { kKeyKP_8 }, // VK_NUMPAD8 + /* 0x069 */ { kKeyKP_9 }, // VK_NUMPAD9 + /* 0x06a */ { kKeyKP_Multiply },// VK_MULTIPLY + /* 0x06b */ { kKeyKP_Add }, // VK_ADD + /* 0x06c */ { kKeyKP_Separator },// VK_SEPARATOR + /* 0x06d */ { kKeyKP_Subtract },// VK_SUBTRACT + /* 0x06e */ { kKeyKP_Decimal }, // VK_DECIMAL + /* 0x06f */ { kKeyNone }, // VK_DIVIDE + /* 0x070 */ { kKeyF1 }, // VK_F1 + /* 0x071 */ { kKeyF2 }, // VK_F2 + /* 0x072 */ { kKeyF3 }, // VK_F3 + /* 0x073 */ { kKeyF4 }, // VK_F4 + /* 0x074 */ { kKeyF5 }, // VK_F5 + /* 0x075 */ { kKeyF6 }, // VK_F6 + /* 0x076 */ { kKeyF7 }, // VK_F7 + /* 0x077 */ { kKeyF8 }, // VK_F8 + /* 0x078 */ { kKeyF9 }, // VK_F9 + /* 0x079 */ { kKeyF10 }, // VK_F10 + /* 0x07a */ { kKeyF11 }, // VK_F11 + /* 0x07b */ { kKeyF12 }, // VK_F12 + /* 0x07c */ { kKeyF13 }, // VK_F13 + /* 0x07d */ { kKeyF14 }, // VK_F14 + /* 0x07e */ { kKeyF15 }, // VK_F15 + /* 0x07f */ { kKeyF16 }, // VK_F16 + /* 0x080 */ { kKeyF17 }, // VK_F17 + /* 0x081 */ { kKeyF18 }, // VK_F18 + /* 0x082 */ { kKeyF19 }, // VK_F19 + /* 0x083 */ { kKeyF20 }, // VK_F20 + /* 0x084 */ { kKeyF21 }, // VK_F21 + /* 0x085 */ { kKeyF22 }, // VK_F22 + /* 0x086 */ { kKeyF23 }, // VK_F23 + /* 0x087 */ { kKeyF24 }, // VK_F24 + /* 0x088 */ { kKeyNone }, // unassigned + /* 0x089 */ { kKeyNone }, // unassigned + /* 0x08a */ { kKeyNone }, // unassigned + /* 0x08b */ { kKeyNone }, // unassigned + /* 0x08c */ { kKeyNone }, // unassigned + /* 0x08d */ { kKeyNone }, // unassigned + /* 0x08e */ { kKeyNone }, // unassigned + /* 0x08f */ { kKeyNone }, // unassigned + /* 0x090 */ { kKeyNumLock }, // VK_NUMLOCK + /* 0x091 */ { kKeyScrollLock }, // VK_SCROLL + /* 0x092 */ { kKeyNone }, // unassigned + /* 0x093 */ { kKeyNone }, // unassigned + /* 0x094 */ { kKeyNone }, // unassigned + /* 0x095 */ { kKeyNone }, // unassigned + /* 0x096 */ { kKeyNone }, // unassigned + /* 0x097 */ { kKeyNone }, // unassigned + /* 0x098 */ { kKeyNone }, // unassigned + /* 0x099 */ { kKeyNone }, // unassigned + /* 0x09a */ { kKeyNone }, // unassigned + /* 0x09b */ { kKeyNone }, // unassigned + /* 0x09c */ { kKeyNone }, // unassigned + /* 0x09d */ { kKeyNone }, // unassigned + /* 0x09e */ { kKeyNone }, // unassigned + /* 0x09f */ { kKeyNone }, // unassigned + /* 0x0a0 */ { kKeyShift_L }, // VK_LSHIFT + /* 0x0a1 */ { kKeyShift_R }, // VK_RSHIFT + /* 0x0a2 */ { kKeyControl_L }, // VK_LCONTROL + /* 0x0a3 */ { kKeyControl_R }, // VK_RCONTROL + /* 0x0a4 */ { kKeyAlt_L }, // VK_LMENU + /* 0x0a5 */ { kKeyAlt_R }, // VK_RMENU + /* 0x0a6 */ { kKeyNone }, // VK_BROWSER_BACK + /* 0x0a7 */ { kKeyNone }, // VK_BROWSER_FORWARD + /* 0x0a8 */ { kKeyNone }, // VK_BROWSER_REFRESH + /* 0x0a9 */ { kKeyNone }, // VK_BROWSER_STOP + /* 0x0aa */ { kKeyNone }, // VK_BROWSER_SEARCH + /* 0x0ab */ { kKeyNone }, // VK_BROWSER_FAVORITES + /* 0x0ac */ { kKeyNone }, // VK_BROWSER_HOME + /* 0x0ad */ { kKeyNone }, // VK_VOLUME_MUTE + /* 0x0ae */ { kKeyNone }, // VK_VOLUME_DOWN + /* 0x0af */ { kKeyNone }, // VK_VOLUME_UP + /* 0x0b0 */ { kKeyNone }, // VK_MEDIA_NEXT_TRACK + /* 0x0b1 */ { kKeyNone }, // VK_MEDIA_PREV_TRACK + /* 0x0b2 */ { kKeyNone }, // VK_MEDIA_STOP + /* 0x0b3 */ { kKeyNone }, // VK_MEDIA_PLAY_PAUSE + /* 0x0b4 */ { kKeyNone }, // VK_LAUNCH_MAIL + /* 0x0b5 */ { kKeyNone }, // VK_LAUNCH_MEDIA_SELECT + /* 0x0b6 */ { kKeyNone }, // VK_LAUNCH_APP1 + /* 0x0b7 */ { kKeyNone }, // VK_LAUNCH_APP2 + /* 0x0b8 */ { kKeyNone }, // unassigned + /* 0x0b9 */ { kKeyNone }, // unassigned + /* 0x0ba */ { kKeyNone }, // OEM specific + /* 0x0bb */ { kKeyNone }, // OEM specific + /* 0x0bc */ { kKeyNone }, // OEM specific + /* 0x0bd */ { kKeyNone }, // OEM specific + /* 0x0be */ { kKeyNone }, // OEM specific + /* 0x0bf */ { kKeyNone }, // OEM specific + /* 0x0c0 */ { kKeyNone }, // OEM specific + /* 0x0c1 */ { kKeyNone }, // unassigned + /* 0x0c2 */ { kKeyNone }, // unassigned + /* 0x0c3 */ { kKeyNone }, // unassigned + /* 0x0c4 */ { kKeyNone }, // unassigned + /* 0x0c5 */ { kKeyNone }, // unassigned + /* 0x0c6 */ { kKeyNone }, // unassigned + /* 0x0c7 */ { kKeyNone }, // unassigned + /* 0x0c8 */ { kKeyNone }, // unassigned + /* 0x0c9 */ { kKeyNone }, // unassigned + /* 0x0ca */ { kKeyNone }, // unassigned + /* 0x0cb */ { kKeyNone }, // unassigned + /* 0x0cc */ { kKeyNone }, // unassigned + /* 0x0cd */ { kKeyNone }, // unassigned + /* 0x0ce */ { kKeyNone }, // unassigned + /* 0x0cf */ { kKeyNone }, // unassigned + /* 0x0d0 */ { kKeyNone }, // unassigned + /* 0x0d1 */ { kKeyNone }, // unassigned + /* 0x0d2 */ { kKeyNone }, // unassigned + /* 0x0d3 */ { kKeyNone }, // unassigned + /* 0x0d4 */ { kKeyNone }, // unassigned + /* 0x0d5 */ { kKeyNone }, // unassigned + /* 0x0d6 */ { kKeyNone }, // unassigned + /* 0x0d7 */ { kKeyNone }, // unassigned + /* 0x0d8 */ { kKeyNone }, // unassigned + /* 0x0d9 */ { kKeyNone }, // unassigned + /* 0x0da */ { kKeyNone }, // unassigned + /* 0x0db */ { kKeyNone }, // OEM specific + /* 0x0dc */ { kKeyNone }, // OEM specific + /* 0x0dd */ { kKeyNone }, // OEM specific + /* 0x0de */ { kKeyNone }, // OEM specific + /* 0x0df */ { kKeyNone }, // OEM specific + /* 0x0e0 */ { kKeyNone }, // OEM specific + /* 0x0e1 */ { kKeyNone }, // OEM specific + /* 0x0e2 */ { kKeyNone }, // OEM specific + /* 0x0e3 */ { kKeyNone }, // OEM specific + /* 0x0e4 */ { kKeyNone }, // OEM specific + /* 0x0e5 */ { kKeyNone }, // unassigned + /* 0x0e6 */ { kKeyNone }, // OEM specific + /* 0x0e7 */ { kKeyNone }, // unassigned + /* 0x0e8 */ { kKeyNone }, // unassigned + /* 0x0e9 */ { kKeyNone }, // OEM specific + /* 0x0ea */ { kKeyNone }, // OEM specific + /* 0x0eb */ { kKeyNone }, // OEM specific + /* 0x0ec */ { kKeyNone }, // OEM specific + /* 0x0ed */ { kKeyNone }, // OEM specific + /* 0x0ee */ { kKeyNone }, // OEM specific + /* 0x0ef */ { kKeyNone }, // OEM specific + /* 0x0f0 */ { kKeyNone }, // OEM specific + /* 0x0f1 */ { kKeyNone }, // OEM specific + /* 0x0f2 */ { kKeyHiraganaKatakana }, // VK_OEM_COPY + /* 0x0f3 */ { kKeyZenkaku }, // VK_OEM_AUTO + /* 0x0f4 */ { kKeyZenkaku }, // VK_OEM_ENLW + /* 0x0f5 */ { kKeyNone }, // OEM specific + /* 0x0f6 */ { kKeyNone }, // VK_ATTN + /* 0x0f7 */ { kKeyNone }, // VK_CRSEL + /* 0x0f8 */ { kKeyNone }, // VK_EXSEL + /* 0x0f9 */ { kKeyNone }, // VK_EREOF + /* 0x0fa */ { kKeyNone }, // VK_PLAY + /* 0x0fb */ { kKeyNone }, // VK_ZOOM + /* 0x0fc */ { kKeyNone }, // reserved + /* 0x0fd */ { kKeyNone }, // VK_PA1 + /* 0x0fe */ { kKeyNone }, // VK_OEM_CLEAR + /* 0x0ff */ { kKeyNone }, // reserved + + /* 0x100 */ { kKeyNone }, // reserved + /* 0x101 */ { kKeyNone }, // VK_LBUTTON + /* 0x102 */ { kKeyNone }, // VK_RBUTTON + /* 0x103 */ { kKeyBreak }, // VK_CANCEL + /* 0x104 */ { kKeyNone }, // VK_MBUTTON + /* 0x105 */ { kKeyNone }, // VK_XBUTTON1 + /* 0x106 */ { kKeyNone }, // VK_XBUTTON2 + /* 0x107 */ { kKeyNone }, // undefined + /* 0x108 */ { kKeyNone }, // VK_BACK + /* 0x109 */ { kKeyNone }, // VK_TAB + /* 0x10a */ { kKeyNone }, // undefined + /* 0x10b */ { kKeyNone }, // undefined + /* 0x10c */ { kKeyClear }, // VK_CLEAR + /* 0x10d */ { kKeyKP_Enter }, // VK_RETURN + /* 0x10e */ { kKeyNone }, // undefined + /* 0x10f */ { kKeyNone }, // undefined + /* 0x110 */ { kKeyShift_R }, // VK_SHIFT + /* 0x111 */ { kKeyControl_R }, // VK_CONTROL + /* 0x112 */ { kKeyAlt_R }, // VK_MENU + /* 0x113 */ { kKeyNone }, // VK_PAUSE + /* 0x114 */ { kKeyNone }, // VK_CAPITAL + /* 0x115 */ { kKeyNone }, // VK_KANA + /* 0x116 */ { kKeyNone }, // VK_HANGUL + /* 0x117 */ { kKeyNone }, // VK_JUNJA + /* 0x118 */ { kKeyNone }, // VK_FINAL + /* 0x119 */ { kKeyNone }, // VK_KANJI + /* 0x11a */ { kKeyNone }, // undefined + /* 0x11b */ { kKeyNone }, // VK_ESCAPE + /* 0x11c */ { kKeyNone }, // VK_CONVERT + /* 0x11d */ { kKeyNone }, // VK_NONCONVERT + /* 0x11e */ { kKeyNone }, // VK_ACCEPT + /* 0x11f */ { kKeyNone }, // VK_MODECHANGE + /* 0x120 */ { kKeyNone }, // VK_SPACE + /* 0x121 */ { kKeyPageUp }, // VK_PRIOR + /* 0x122 */ { kKeyPageDown }, // VK_NEXT + /* 0x123 */ { kKeyEnd }, // VK_END + /* 0x124 */ { kKeyHome }, // VK_HOME + /* 0x125 */ { kKeyLeft }, // VK_LEFT + /* 0x126 */ { kKeyUp }, // VK_UP + /* 0x127 */ { kKeyRight }, // VK_RIGHT + /* 0x128 */ { kKeyDown }, // VK_DOWN + /* 0x129 */ { kKeySelect }, // VK_SELECT + /* 0x12a */ { kKeyNone }, // VK_PRINT + /* 0x12b */ { kKeyExecute }, // VK_EXECUTE + /* 0x12c */ { kKeyPrint }, // VK_SNAPSHOT + /* 0x12d */ { kKeyInsert }, // VK_INSERT + /* 0x12e */ { kKeyDelete }, // VK_DELETE + /* 0x12f */ { kKeyHelp }, // VK_HELP + /* 0x130 */ { kKeyNone }, // VK_0 + /* 0x131 */ { kKeyNone }, // VK_1 + /* 0x132 */ { kKeyNone }, // VK_2 + /* 0x133 */ { kKeyNone }, // VK_3 + /* 0x134 */ { kKeyNone }, // VK_4 + /* 0x135 */ { kKeyNone }, // VK_5 + /* 0x136 */ { kKeyNone }, // VK_6 + /* 0x137 */ { kKeyNone }, // VK_7 + /* 0x138 */ { kKeyNone }, // VK_8 + /* 0x139 */ { kKeyNone }, // VK_9 + /* 0x13a */ { kKeyNone }, // undefined + /* 0x13b */ { kKeyNone }, // undefined + /* 0x13c */ { kKeyNone }, // undefined + /* 0x13d */ { kKeyNone }, // undefined + /* 0x13e */ { kKeyNone }, // undefined + /* 0x13f */ { kKeyNone }, // undefined + /* 0x140 */ { kKeyNone }, // undefined + /* 0x141 */ { kKeyNone }, // VK_A + /* 0x142 */ { kKeyNone }, // VK_B + /* 0x143 */ { kKeyNone }, // VK_C + /* 0x144 */ { kKeyNone }, // VK_D + /* 0x145 */ { kKeyNone }, // VK_E + /* 0x146 */ { kKeyNone }, // VK_F + /* 0x147 */ { kKeyNone }, // VK_G + /* 0x148 */ { kKeyNone }, // VK_H + /* 0x149 */ { kKeyNone }, // VK_I + /* 0x14a */ { kKeyNone }, // VK_J + /* 0x14b */ { kKeyNone }, // VK_K + /* 0x14c */ { kKeyNone }, // VK_L + /* 0x14d */ { kKeyNone }, // VK_M + /* 0x14e */ { kKeyNone }, // VK_N + /* 0x14f */ { kKeyNone }, // VK_O + /* 0x150 */ { kKeyNone }, // VK_P + /* 0x151 */ { kKeyNone }, // VK_Q + /* 0x152 */ { kKeyNone }, // VK_R + /* 0x153 */ { kKeyNone }, // VK_S + /* 0x154 */ { kKeyNone }, // VK_T + /* 0x155 */ { kKeyNone }, // VK_U + /* 0x156 */ { kKeyNone }, // VK_V + /* 0x157 */ { kKeyNone }, // VK_W + /* 0x158 */ { kKeyNone }, // VK_X + /* 0x159 */ { kKeyNone }, // VK_Y + /* 0x15a */ { kKeyNone }, // VK_Z + /* 0x15b */ { kKeySuper_L }, // VK_LWIN + /* 0x15c */ { kKeySuper_R }, // VK_RWIN + /* 0x15d */ { kKeyMenu }, // VK_APPS + /* 0x15e */ { kKeyNone }, // undefined + /* 0x15f */ { kKeyNone }, // VK_SLEEP + /* 0x160 */ { kKeyNone }, // VK_NUMPAD0 + /* 0x161 */ { kKeyNone }, // VK_NUMPAD1 + /* 0x162 */ { kKeyNone }, // VK_NUMPAD2 + /* 0x163 */ { kKeyNone }, // VK_NUMPAD3 + /* 0x164 */ { kKeyNone }, // VK_NUMPAD4 + /* 0x165 */ { kKeyNone }, // VK_NUMPAD5 + /* 0x166 */ { kKeyNone }, // VK_NUMPAD6 + /* 0x167 */ { kKeyNone }, // VK_NUMPAD7 + /* 0x168 */ { kKeyNone }, // VK_NUMPAD8 + /* 0x169 */ { kKeyNone }, // VK_NUMPAD9 + /* 0x16a */ { kKeyNone }, // VK_MULTIPLY + /* 0x16b */ { kKeyNone }, // VK_ADD + /* 0x16c */ { kKeyKP_Separator },// VK_SEPARATOR + /* 0x16d */ { kKeyNone }, // VK_SUBTRACT + /* 0x16e */ { kKeyNone }, // VK_DECIMAL + /* 0x16f */ { kKeyKP_Divide }, // VK_DIVIDE + /* 0x170 */ { kKeyNone }, // VK_F1 + /* 0x171 */ { kKeyNone }, // VK_F2 + /* 0x172 */ { kKeyNone }, // VK_F3 + /* 0x173 */ { kKeyNone }, // VK_F4 + /* 0x174 */ { kKeyNone }, // VK_F5 + /* 0x175 */ { kKeyNone }, // VK_F6 + /* 0x176 */ { kKeyNone }, // VK_F7 + /* 0x177 */ { kKeyNone }, // VK_F8 + /* 0x178 */ { kKeyNone }, // VK_F9 + /* 0x179 */ { kKeyNone }, // VK_F10 + /* 0x17a */ { kKeyNone }, // VK_F11 + /* 0x17b */ { kKeyNone }, // VK_F12 + /* 0x17c */ { kKeyF13 }, // VK_F13 + /* 0x17d */ { kKeyF14 }, // VK_F14 + /* 0x17e */ { kKeyF15 }, // VK_F15 + /* 0x17f */ { kKeyF16 }, // VK_F16 + /* 0x180 */ { kKeyF17 }, // VK_F17 + /* 0x181 */ { kKeyF18 }, // VK_F18 + /* 0x182 */ { kKeyF19 }, // VK_F19 + /* 0x183 */ { kKeyF20 }, // VK_F20 + /* 0x184 */ { kKeyF21 }, // VK_F21 + /* 0x185 */ { kKeyF22 }, // VK_F22 + /* 0x186 */ { kKeyF23 }, // VK_F23 + /* 0x187 */ { kKeyF24 }, // VK_F24 + /* 0x188 */ { kKeyNone }, // unassigned + /* 0x189 */ { kKeyNone }, // unassigned + /* 0x18a */ { kKeyNone }, // unassigned + /* 0x18b */ { kKeyNone }, // unassigned + /* 0x18c */ { kKeyNone }, // unassigned + /* 0x18d */ { kKeyNone }, // unassigned + /* 0x18e */ { kKeyNone }, // unassigned + /* 0x18f */ { kKeyNone }, // unassigned + /* 0x190 */ { kKeyNumLock }, // VK_NUMLOCK + /* 0x191 */ { kKeyNone }, // VK_SCROLL + /* 0x192 */ { kKeyNone }, // unassigned + /* 0x193 */ { kKeyNone }, // unassigned + /* 0x194 */ { kKeyNone }, // unassigned + /* 0x195 */ { kKeyNone }, // unassigned + /* 0x196 */ { kKeyNone }, // unassigned + /* 0x197 */ { kKeyNone }, // unassigned + /* 0x198 */ { kKeyNone }, // unassigned + /* 0x199 */ { kKeyNone }, // unassigned + /* 0x19a */ { kKeyNone }, // unassigned + /* 0x19b */ { kKeyNone }, // unassigned + /* 0x19c */ { kKeyNone }, // unassigned + /* 0x19d */ { kKeyNone }, // unassigned + /* 0x19e */ { kKeyNone }, // unassigned + /* 0x19f */ { kKeyNone }, // unassigned + /* 0x1a0 */ { kKeyShift_L }, // VK_LSHIFT + /* 0x1a1 */ { kKeyShift_R }, // VK_RSHIFT + /* 0x1a2 */ { kKeyControl_L }, // VK_LCONTROL + /* 0x1a3 */ { kKeyControl_R }, // VK_RCONTROL + /* 0x1a4 */ { kKeyAlt_L }, // VK_LMENU + /* 0x1a5 */ { kKeyAlt_R }, // VK_RMENU + /* 0x1a6 */ { kKeyWWWBack }, // VK_BROWSER_BACK + /* 0x1a7 */ { kKeyWWWForward }, // VK_BROWSER_FORWARD + /* 0x1a8 */ { kKeyWWWRefresh }, // VK_BROWSER_REFRESH + /* 0x1a9 */ { kKeyWWWStop }, // VK_BROWSER_STOP + /* 0x1aa */ { kKeyWWWSearch }, // VK_BROWSER_SEARCH + /* 0x1ab */ { kKeyWWWFavorites },// VK_BROWSER_FAVORITES + /* 0x1ac */ { kKeyWWWHome }, // VK_BROWSER_HOME + /* 0x1ad */ { kKeyAudioMute }, // VK_VOLUME_MUTE + /* 0x1ae */ { kKeyAudioDown }, // VK_VOLUME_DOWN + /* 0x1af */ { kKeyAudioUp }, // VK_VOLUME_UP + /* 0x1b0 */ { kKeyAudioNext }, // VK_MEDIA_NEXT_TRACK + /* 0x1b1 */ { kKeyAudioPrev }, // VK_MEDIA_PREV_TRACK + /* 0x1b2 */ { kKeyAudioStop }, // VK_MEDIA_STOP + /* 0x1b3 */ { kKeyAudioPlay }, // VK_MEDIA_PLAY_PAUSE + /* 0x1b4 */ { kKeyAppMail }, // VK_LAUNCH_MAIL + /* 0x1b5 */ { kKeyAppMedia }, // VK_LAUNCH_MEDIA_SELECT + /* 0x1b6 */ { kKeyAppUser1 }, // VK_LAUNCH_APP1 + /* 0x1b7 */ { kKeyAppUser2 }, // VK_LAUNCH_APP2 + /* 0x1b8 */ { kKeyNone }, // unassigned + /* 0x1b9 */ { kKeyNone }, // unassigned + /* 0x1ba */ { kKeyNone }, // OEM specific + /* 0x1bb */ { kKeyNone }, // OEM specific + /* 0x1bc */ { kKeyNone }, // OEM specific + /* 0x1bd */ { kKeyNone }, // OEM specific + /* 0x1be */ { kKeyNone }, // OEM specific + /* 0x1bf */ { kKeyNone }, // OEM specific + /* 0x1c0 */ { kKeyNone }, // OEM specific + /* 0x1c1 */ { kKeyNone }, // unassigned + /* 0x1c2 */ { kKeyNone }, // unassigned + /* 0x1c3 */ { kKeyNone }, // unassigned + /* 0x1c4 */ { kKeyNone }, // unassigned + /* 0x1c5 */ { kKeyNone }, // unassigned + /* 0x1c6 */ { kKeyNone }, // unassigned + /* 0x1c7 */ { kKeyNone }, // unassigned + /* 0x1c8 */ { kKeyNone }, // unassigned + /* 0x1c9 */ { kKeyNone }, // unassigned + /* 0x1ca */ { kKeyNone }, // unassigned + /* 0x1cb */ { kKeyNone }, // unassigned + /* 0x1cc */ { kKeyNone }, // unassigned + /* 0x1cd */ { kKeyNone }, // unassigned + /* 0x1ce */ { kKeyNone }, // unassigned + /* 0x1cf */ { kKeyNone }, // unassigned + /* 0x1d0 */ { kKeyNone }, // unassigned + /* 0x1d1 */ { kKeyNone }, // unassigned + /* 0x1d2 */ { kKeyNone }, // unassigned + /* 0x1d3 */ { kKeyNone }, // unassigned + /* 0x1d4 */ { kKeyNone }, // unassigned + /* 0x1d5 */ { kKeyNone }, // unassigned + /* 0x1d6 */ { kKeyNone }, // unassigned + /* 0x1d7 */ { kKeyNone }, // unassigned + /* 0x1d8 */ { kKeyNone }, // unassigned + /* 0x1d9 */ { kKeyNone }, // unassigned + /* 0x1da */ { kKeyNone }, // unassigned + /* 0x1db */ { kKeyNone }, // OEM specific + /* 0x1dc */ { kKeyNone }, // OEM specific + /* 0x1dd */ { kKeyNone }, // OEM specific + /* 0x1de */ { kKeyNone }, // OEM specific + /* 0x1df */ { kKeyNone }, // OEM specific + /* 0x1e0 */ { kKeyNone }, // OEM specific + /* 0x1e1 */ { kKeyNone }, // OEM specific + /* 0x1e2 */ { kKeyNone }, // OEM specific + /* 0x1e3 */ { kKeyNone }, // OEM specific + /* 0x1e4 */ { kKeyNone }, // OEM specific + /* 0x1e5 */ { kKeyNone }, // unassigned + /* 0x1e6 */ { kKeyNone }, // OEM specific + /* 0x1e7 */ { kKeyNone }, // unassigned + /* 0x1e8 */ { kKeyNone }, // unassigned + /* 0x1e9 */ { kKeyNone }, // OEM specific + /* 0x1ea */ { kKeyNone }, // OEM specific + /* 0x1eb */ { kKeyNone }, // OEM specific + /* 0x1ec */ { kKeyNone }, // OEM specific + /* 0x1ed */ { kKeyNone }, // OEM specific + /* 0x1ee */ { kKeyNone }, // OEM specific + /* 0x1ef */ { kKeyNone }, // OEM specific + /* 0x1f0 */ { kKeyNone }, // OEM specific + /* 0x1f1 */ { kKeyNone }, // OEM specific + /* 0x1f2 */ { kKeyNone }, // VK_OEM_COPY + /* 0x1f3 */ { kKeyNone }, // VK_OEM_AUTO + /* 0x1f4 */ { kKeyNone }, // VK_OEM_ENLW + /* 0x1f5 */ { kKeyNone }, // OEM specific + /* 0x1f6 */ { kKeyNone }, // VK_ATTN + /* 0x1f7 */ { kKeyNone }, // VK_CRSEL + /* 0x1f8 */ { kKeyNone }, // VK_EXSEL + /* 0x1f9 */ { kKeyNone }, // VK_EREOF + /* 0x1fa */ { kKeyNone }, // VK_PLAY + /* 0x1fb */ { kKeyNone }, // VK_ZOOM + /* 0x1fc */ { kKeyNone }, // reserved + /* 0x1fd */ { kKeyNone }, // VK_PA1 + /* 0x1fe */ { kKeyNone }, // VK_OEM_CLEAR + /* 0x1ff */ { kKeyNone } // reserved +}; + +struct CWin32Modifiers { +public: + UINT m_vk; + KeyModifierMask m_mask; +}; + +static const CWin32Modifiers s_modifiers[] = +{ + { VK_SHIFT, KeyModifierShift }, + { VK_LSHIFT, KeyModifierShift }, + { VK_RSHIFT, KeyModifierShift }, + { VK_CONTROL, KeyModifierControl }, + { VK_LCONTROL, KeyModifierControl }, + { VK_RCONTROL, KeyModifierControl }, + { VK_MENU, KeyModifierAlt }, + { VK_LMENU, KeyModifierAlt }, + { VK_RMENU, KeyModifierAlt }, + { VK_LWIN, KeyModifierSuper }, + { VK_RWIN, KeyModifierSuper } +}; + +CMSWindowsKeyState::CMSWindowsKeyState( + CMSWindowsDesks* desks, void* eventTarget) : + m_is95Family(CArchMiscWindows::isWindows95Family()), + m_eventTarget(eventTarget), + m_desks(desks), + m_keyLayout(GetKeyboardLayout(0)), + m_fixTimer(NULL), + m_lastDown(0), + m_useSavedModifiers(false), + m_savedModifiers(0), + m_originalSavedModifiers(0), + m_eventQueue(*EVENTQUEUE) +{ + init(); +} + +CMSWindowsKeyState::CMSWindowsKeyState( + CMSWindowsDesks* desks, void* eventTarget, IEventQueue& eventQueue, CKeyMap& keyMap) : + CKeyState(eventQueue, keyMap), + m_is95Family(CArchMiscWindows::isWindows95Family()), + m_eventTarget(eventTarget), + m_desks(desks), + m_keyLayout(GetKeyboardLayout(0)), + m_fixTimer(NULL), + m_lastDown(0), + m_useSavedModifiers(false), + m_savedModifiers(0), + m_originalSavedModifiers(0), + m_eventQueue(eventQueue) +{ + init(); +} + +CMSWindowsKeyState::~CMSWindowsKeyState() +{ + disable(); +} + +void +CMSWindowsKeyState::init() +{ + // look up symbol that's available on winNT family but not win95 + HMODULE userModule = GetModuleHandle("user32.dll"); + m_ToUnicodeEx = (ToUnicodeEx_t)GetProcAddress(userModule, "ToUnicodeEx"); +} + +void +CMSWindowsKeyState::disable() +{ + if (m_fixTimer != NULL) { + getEventQueue().removeHandler(CEvent::kTimer, m_fixTimer); + getEventQueue().deleteTimer(m_fixTimer); + m_fixTimer = NULL; + } + m_lastDown = 0; +} + +KeyButton +CMSWindowsKeyState::virtualKeyToButton(UINT virtualKey) const +{ + return m_virtualKeyToButton[virtualKey & 0xffu]; +} + +void +CMSWindowsKeyState::setKeyLayout(HKL keyLayout) +{ + m_keyLayout = keyLayout; +} + +bool +CMSWindowsKeyState::testAutoRepeat(bool press, bool isRepeat, KeyButton button) +{ + if (!isRepeat) { + isRepeat = (press && m_lastDown != 0 && button == m_lastDown); + } + if (press) { + m_lastDown = button; + } + else { + m_lastDown = 0; + } + return isRepeat; +} + +void +CMSWindowsKeyState::saveModifiers() +{ + m_savedModifiers = getActiveModifiers(); + m_originalSavedModifiers = m_savedModifiers; +} + +void +CMSWindowsKeyState::useSavedModifiers(bool enable) +{ + if (enable != m_useSavedModifiers) { + m_useSavedModifiers = enable; + if (!m_useSavedModifiers) { + // transfer any modifier state changes to CKeyState's state + KeyModifierMask mask = m_originalSavedModifiers ^ m_savedModifiers; + getActiveModifiersRValue() = + (getActiveModifiers() & ~mask) | (m_savedModifiers & mask); + } + } +} + +KeyID +CMSWindowsKeyState::mapKeyFromEvent(WPARAM charAndVirtKey, + LPARAM info, KeyModifierMask* maskOut) const +{ + static const KeyModifierMask s_controlAlt = + KeyModifierControl | KeyModifierAlt; + + // extract character, virtual key, and if we didn't use AltGr + char c = (char)((charAndVirtKey & 0xff00u) >> 8); + UINT vkCode = (charAndVirtKey & 0xffu); + bool noAltGr = ((charAndVirtKey & 0xff0000u) != 0); + + // handle some keys via table lookup + KeyID id = getKeyID(vkCode, (KeyButton)((info >> 16) & 0x1ffu)); + + // check if not in table; map character to key id + if (id == kKeyNone && c != 0) { + if ((c & 0x80u) == 0) { + // ASCII + id = static_cast(c) & 0xffu; + } + else { + // character is not really ASCII. instead it's some + // character in the current ANSI code page. try to + // convert that to a Unicode character. if we fail + // then use the single byte character as is. + char src = c; + wchar_t unicode; + if (MultiByteToWideChar(CP_THREAD_ACP, MB_PRECOMPOSED, + &src, 1, &unicode, 1) > 0) { + id = static_cast(unicode); + } + else { + id = static_cast(c) & 0xffu; + } + } + } + + // set modifier mask + if (maskOut != NULL) { + KeyModifierMask active = getActiveModifiers(); + if (!noAltGr && (active & s_controlAlt) == s_controlAlt) { + // if !noAltGr then we're only interested in matching the + // key, not the AltGr. AltGr is down (i.e. control and alt + // are down) but we don't want the client to have to match + // that so we clear it. + active &= ~s_controlAlt; + } + *maskOut = active; + } + + return id; +} + +bool +CMSWindowsKeyState::didGroupsChange() const +{ + GroupList groups; + return (getGroups(groups) && groups != m_groups); +} + +UINT +CMSWindowsKeyState::mapKeyToVirtualKey(KeyID key) const +{ + if (key == kKeyNone) { + return 0; + } + KeyToVKMap::const_iterator i = m_keyToVKMap.find(key); + if (i == m_keyToVKMap.end()) { + return 0; + } + else { + return i->second; + } +} + +void +CMSWindowsKeyState::onKey(KeyButton button, bool down, KeyModifierMask newState) +{ + // handle win32 brokenness and forward to superclass + fixKeys(); + CKeyState::onKey(button, down, newState); + fixKeys(); +} + +void +CMSWindowsKeyState::sendKeyEvent(void* target, + bool press, bool isAutoRepeat, + KeyID key, KeyModifierMask mask, + SInt32 count, KeyButton button) +{ + if (press || isAutoRepeat) { + // send key + if (press && !isAutoRepeat) { + CKeyState::sendKeyEvent(target, true, false, + key, mask, 1, button); + if (count > 0) { + --count; + } + } + if (count >= 1) { + CKeyState::sendKeyEvent(target, true, true, + key, mask, count, button); + } + } + else { + // do key up + CKeyState::sendKeyEvent(target, false, false, key, mask, 1, button); + } +} + +void +CMSWindowsKeyState::fakeKeyDown(KeyID id, KeyModifierMask mask, + KeyButton button) +{ + CKeyState::fakeKeyDown(id, mask, button); +} + +bool +CMSWindowsKeyState::fakeKeyRepeat(KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton button) +{ + return CKeyState::fakeKeyRepeat(id, mask, count, button); +} + +bool +CMSWindowsKeyState::fakeCtrlAltDel() +{ + if (!m_is95Family) { + // to fake ctrl+alt+del on the NT family we broadcast a suitable + // hotkey to all windows on the winlogon desktop. however, the + // current thread must be on that desktop to do the broadcast + // and we can't switch just any thread because some own windows + // or hooks. so start a new thread to do the real work. + HANDLE hEvtSendSas = OpenEvent( EVENT_MODIFY_STATE, FALSE, "Global\\SendSAS" ); + if ( hEvtSendSas ) { + LOG((CLOG_DEBUG "found the SendSAS event - signaling my launcher to simulate ctrl+alt+del")); + SetEvent( hEvtSendSas ); + CloseHandle( hEvtSendSas ); + } + else { + CThread cad(new CFunctionJob(&CMSWindowsKeyState::ctrlAltDelThread)); + cad.wait(); + } + } + else { + // simulate ctrl+alt+del + fakeKeyDown(kKeyDelete, KeyModifierControl | KeyModifierAlt, + virtualKeyToButton(VK_DELETE)); + } + return true; +} + +void +CMSWindowsKeyState::ctrlAltDelThread(void*) +{ + // get the Winlogon desktop at whatever privilege we can + HDESK desk = OpenDesktop("Winlogon", 0, FALSE, MAXIMUM_ALLOWED); + if (desk != NULL) { + if (SetThreadDesktop(desk)) { + PostMessage(HWND_BROADCAST, WM_HOTKEY, 0, + MAKELPARAM(MOD_CONTROL | MOD_ALT, VK_DELETE)); + } + else { + LOG((CLOG_DEBUG "can't switch to Winlogon desk: %d", GetLastError())); + } + CloseDesktop(desk); + } + else { + LOG((CLOG_DEBUG "can't open Winlogon desk: %d", GetLastError())); + } +} + +KeyModifierMask +CMSWindowsKeyState::pollActiveModifiers() const +{ + KeyModifierMask state = 0; + + // get non-toggle modifiers from our own shadow key state + for (size_t i = 0; i < sizeof(s_modifiers) / sizeof(s_modifiers[0]); ++i) { + KeyButton button = virtualKeyToButton(s_modifiers[i].m_vk); + if (button != 0 && isKeyDown(button)) { + state |= s_modifiers[i].m_mask; + } + } + + // we can get toggle modifiers from the system + if ((GetKeyState(VK_CAPITAL) & 0x01) != 0) { + state |= KeyModifierCapsLock; + } + if ((GetKeyState(VK_NUMLOCK) & 0x01) != 0) { + state |= KeyModifierNumLock; + } + if ((GetKeyState(VK_SCROLL) & 0x01) != 0) { + state |= KeyModifierScrollLock; + } + + return state; +} + +SInt32 +CMSWindowsKeyState::pollActiveGroup() const +{ + // determine the thread that'll receive this event + HWND targetWindow = GetForegroundWindow(); + DWORD targetThread = GetWindowThreadProcessId(targetWindow, NULL); + + // get keyboard layout for the thread + HKL hkl = GetKeyboardLayout(targetThread); + + if (!hkl) { + // GetKeyboardLayout failed. Maybe targetWindow is a console window. + // We're getting the keyboard layout of the desktop instead. + targetWindow = GetDesktopWindow(); + targetThread = GetWindowThreadProcessId(targetWindow, NULL); + hkl = GetKeyboardLayout(targetThread); + } + + // get group + GroupMap::const_iterator i = m_groupMap.find(hkl); + if (i == m_groupMap.end()) { + LOG((CLOG_DEBUG1 "can't find keyboard layout %08x", hkl)); + return 0; + } + + return i->second; +} + +void +CMSWindowsKeyState::pollPressedKeys(KeyButtonSet& pressedKeys) const +{ + BYTE keyState[256]; + if (!GetKeyboardState(keyState)) { + LOG((CLOG_ERR "GetKeyboardState returned false on pollPressedKeys")); + return; + } + for (KeyButton i = 1; i < 256; ++i) { + if ((keyState[i] & 0x80) != 0) { + pressedKeys.insert(i); + } + } +} + +void +CMSWindowsKeyState::getKeyMap(CKeyMap& keyMap) +{ + // update keyboard groups + if (getGroups(m_groups)) { + m_groupMap.clear(); + SInt32 numGroups = (SInt32)m_groups.size(); + for (SInt32 g = 0; g < numGroups; ++g) { + m_groupMap[m_groups[g]] = g; + } + } + HKL activeLayout = GetKeyboardLayout(0); + + // clear table + memset(m_virtualKeyToButton, 0, sizeof(m_virtualKeyToButton)); + m_keyToVKMap.clear(); + + CKeyMap::KeyItem item; + SInt32 numGroups = (SInt32)m_groups.size(); + for (SInt32 g = 0; g < numGroups; ++g) { + item.m_group = g; + ActivateKeyboardLayout(m_groups[g], 0); + + // clear tables + memset(m_buttonToVK, 0, sizeof(m_buttonToVK)); + memset(m_buttonToNumpadVK, 0, sizeof(m_buttonToNumpadVK)); + + // map buttons (scancodes) to virtual keys + for (KeyButton i = 1; i < 256; ++i) { + UINT vk = MapVirtualKey(i, 1); + if (vk == 0) { + // unmapped + continue; + } + + // deal with certain virtual keys specially + switch (vk) { + case VK_SHIFT: + vk = VK_LSHIFT; + break; + + case VK_CONTROL: + vk = VK_LCONTROL; + break; + + case VK_MENU: + vk = VK_LMENU; + break; + + case VK_NUMLOCK: + vk = VK_PAUSE; + break; + + case VK_NUMPAD0: + case VK_NUMPAD1: + case VK_NUMPAD2: + case VK_NUMPAD3: + case VK_NUMPAD4: + case VK_NUMPAD5: + case VK_NUMPAD6: + case VK_NUMPAD7: + case VK_NUMPAD8: + case VK_NUMPAD9: + case VK_DECIMAL: + // numpad keys are saved in their own table + m_buttonToNumpadVK[i] = vk; + continue; + + case VK_LWIN: + case VK_RWIN: + // add extended key only for these on 95 family + if (m_is95Family) { + m_buttonToVK[i | 0x100u] = vk; + continue; + } + break; + + case VK_RETURN: + case VK_PRIOR: + case VK_NEXT: + case VK_END: + case VK_HOME: + case VK_LEFT: + case VK_UP: + case VK_RIGHT: + case VK_DOWN: + case VK_INSERT: + case VK_DELETE: + // also add extended key for these + m_buttonToVK[i | 0x100u] = vk; + break; + } + + if (m_buttonToVK[i] == 0) { + m_buttonToVK[i] = vk; + } + } + + // now map virtual keys to buttons. multiple virtual keys may map + // to a single button. if the virtual key matches the one in + // m_buttonToVK then we use the button as is. if not then it's + // either a numpad key and we use the button as is or it's an + // extended button. + for (UINT i = 1; i < 255; ++i) { + // skip virtual keys we don't want + switch (i) { + case VK_LBUTTON: + case VK_RBUTTON: + case VK_MBUTTON: + case VK_XBUTTON1: + case VK_XBUTTON2: + case VK_SHIFT: + case VK_CONTROL: + case VK_MENU: + continue; + } + + // get the button + KeyButton button = static_cast(MapVirtualKey(i, 0)); + if (button == 0) { + continue; + } + + // deal with certain virtual keys specially + switch (i) { + case VK_NUMPAD0: + case VK_NUMPAD1: + case VK_NUMPAD2: + case VK_NUMPAD3: + case VK_NUMPAD4: + case VK_NUMPAD5: + case VK_NUMPAD6: + case VK_NUMPAD7: + case VK_NUMPAD8: + case VK_NUMPAD9: + case VK_DECIMAL: + m_buttonToNumpadVK[button] = i; + break; + + default: + // add extended key if virtual keys don't match + if (m_buttonToVK[button] != i) { + m_buttonToVK[button | 0x100u] = i; + } + break; + } + } + + // add alt+printscreen + if (m_buttonToVK[0x54u] == 0) { + m_buttonToVK[0x54u] = VK_SNAPSHOT; + } + + // set virtual key to button table + if (GetKeyboardLayout(0) == m_groups[g]) { + for (KeyButton i = 0; i < 512; ++i) { + if (m_buttonToVK[i] != 0) { + if (m_virtualKeyToButton[m_buttonToVK[i]] == 0) { + m_virtualKeyToButton[m_buttonToVK[i]] = i; + } + } + if (m_buttonToNumpadVK[i] != 0) { + if (m_virtualKeyToButton[m_buttonToNumpadVK[i]] == 0) { + m_virtualKeyToButton[m_buttonToNumpadVK[i]] = i; + } + } + } + } + + // add numpad keys + for (KeyButton i = 0; i < 512; ++i) { + if (m_buttonToNumpadVK[i] != 0) { + item.m_id = getKeyID(m_buttonToNumpadVK[i], i); + item.m_button = i; + item.m_required = KeyModifierNumLock; + item.m_sensitive = KeyModifierNumLock | KeyModifierShift; + item.m_generates = 0; + item.m_client = m_buttonToNumpadVK[i]; + addKeyEntry(keyMap, item); + } + } + + // add other keys + BYTE keys[256]; + memset(keys, 0, sizeof(keys)); + for (KeyButton i = 0; i < 512; ++i) { + if (m_buttonToVK[i] != 0) { + // initialize item + item.m_id = getKeyID(m_buttonToVK[i], i); + item.m_button = i; + item.m_required = 0; + item.m_sensitive = 0; + item.m_client = m_buttonToVK[i]; + + // get flags for modifier keys + CKeyMap::initModifierKey(item); + + if (item.m_id == 0) { + // translate virtual key to a character with and without + // shift, caps lock, and AltGr. + struct Modifier { + UINT m_vk1; + UINT m_vk2; + BYTE m_state; + KeyModifierMask m_mask; + }; + static const Modifier modifiers[] = { + { VK_SHIFT, VK_SHIFT, 0x80u, KeyModifierShift }, + { VK_CAPITAL, VK_CAPITAL, 0x01u, KeyModifierCapsLock }, + { VK_CONTROL, VK_MENU, 0x80u, KeyModifierControl | + KeyModifierAlt } + }; + static const size_t s_numModifiers = + sizeof(modifiers) / sizeof(modifiers[0]); + static const size_t s_numCombinations = 1 << s_numModifiers; + KeyID id[s_numCombinations]; + + bool anyFound = false; + KeyButton button = static_cast(i & 0xffu); + for (size_t j = 0; j < s_numCombinations; ++j) { + for (size_t k = 0; k < s_numModifiers; ++k) { + //if ((j & (1 << k)) != 0) { + // http://msdn.microsoft.com/en-us/library/ke55d167.aspx + if ((j & (1i64 << k)) != 0) { + keys[modifiers[k].m_vk1] = modifiers[k].m_state; + keys[modifiers[k].m_vk2] = modifiers[k].m_state; + } + else { + keys[modifiers[k].m_vk1] = 0; + keys[modifiers[k].m_vk2] = 0; + } + } + id[j] = getIDForKey(item, button, + m_buttonToVK[i], keys, m_groups[g]); + if (id[j] != 0) { + anyFound = true; + } + } + + if (anyFound) { + // determine what modifiers we're sensitive to. + // we're sensitive if the KeyID changes when the + // modifier does. + item.m_sensitive = 0; + for (size_t k = 0; k < s_numModifiers; ++k) { + for (size_t j = 0; j < s_numCombinations; ++j) { + //if (id[j] != id[j ^ (1u << k)]) { + // http://msdn.microsoft.com/en-us/library/ke55d167.aspx + if (id[j] != id[j ^ (1ui64 << k)]) { + item.m_sensitive |= modifiers[k].m_mask; + break; + } + } + } + + // save each key. the map will automatically discard + // duplicates, like an unshift and shifted version of + // a key that's insensitive to shift. + for (size_t j = 0; j < s_numCombinations; ++j) { + item.m_id = id[j]; + item.m_required = 0; + for (size_t k = 0; k < s_numModifiers; ++k) { + if ((j & (1i64 << k)) != 0) { + item.m_required |= modifiers[k].m_mask; + } + } + addKeyEntry(keyMap, item); + } + } + } + else { + // found in table + switch (m_buttonToVK[i]) { + case VK_TAB: + // add kKeyLeftTab, too + item.m_id = kKeyLeftTab; + item.m_required |= KeyModifierShift; + item.m_sensitive |= KeyModifierShift; + addKeyEntry(keyMap, item); + item.m_id = kKeyTab; + item.m_required &= ~KeyModifierShift; + break; + + case VK_CANCEL: + item.m_required |= KeyModifierControl; + item.m_sensitive |= KeyModifierControl; + break; + + case VK_SNAPSHOT: + item.m_sensitive |= KeyModifierAlt; + if ((i & 0x100u) == 0) { + // non-extended snapshot key requires alt + item.m_required |= KeyModifierAlt; + } + break; + } + addKeyEntry(keyMap, item); + } + } + } + } + + // restore keyboard layout + ActivateKeyboardLayout(activeLayout, 0); +} + +void +CMSWindowsKeyState::fakeKey(const Keystroke& keystroke) +{ + switch (keystroke.m_type) { + case Keystroke::kButton: { + LOG((CLOG_DEBUG1 " %03x (%08x) %s", keystroke.m_data.m_button.m_button, keystroke.m_data.m_button.m_client, keystroke.m_data.m_button.m_press ? "down" : "up")); + KeyButton button = keystroke.m_data.m_button.m_button; + + // windows doesn't send key ups for key repeats + if (keystroke.m_data.m_button.m_repeat && + !keystroke.m_data.m_button.m_press) { + LOG((CLOG_DEBUG1 " discard key repeat release")); + break; + } + + // get the virtual key for the button + UINT vk = keystroke.m_data.m_button.m_client; + + // special handling of VK_SNAPSHOT + if (vk == VK_SNAPSHOT) { + if ((getActiveModifiers() & KeyModifierAlt) != 0) { + // snapshot active window + button = 1; + } + else { + // snapshot full screen + button = 0; + } + } + + // synthesize event + m_desks->fakeKeyEvent(button, vk, + keystroke.m_data.m_button.m_press, + keystroke.m_data.m_button.m_repeat); + break; + } + + case Keystroke::kGroup: + // we don't restore the group. we'd like to but we can't be + // sure the restoring group change will be processed after the + // key events. + if (!keystroke.m_data.m_group.m_restore) { + if (keystroke.m_data.m_group.m_absolute) { + LOG((CLOG_DEBUG1 " group %d", keystroke.m_data.m_group.m_group)); + setWindowGroup(keystroke.m_data.m_group.m_group); + } + else { + LOG((CLOG_DEBUG1 " group %+d", keystroke.m_data.m_group.m_group)); + setWindowGroup(getEffectiveGroup(pollActiveGroup(), + keystroke.m_data.m_group.m_group)); + } + } + break; + } +} + +KeyModifierMask& +CMSWindowsKeyState::getActiveModifiersRValue() +{ + if (m_useSavedModifiers) { + return m_savedModifiers; + } + else { + return CKeyState::getActiveModifiersRValue(); + } +} + +bool +CMSWindowsKeyState::getGroups(GroupList& groups) const +{ + // get keyboard layouts + UInt32 newNumLayouts = GetKeyboardLayoutList(0, NULL); + if (newNumLayouts == 0) { + LOG((CLOG_DEBUG1 "can't get keyboard layouts")); + return false; + } + HKL* newLayouts = new HKL[newNumLayouts]; + newNumLayouts = GetKeyboardLayoutList(newNumLayouts, newLayouts); + if (newNumLayouts == 0) { + LOG((CLOG_DEBUG1 "can't get keyboard layouts")); + delete[] newLayouts; + return false; + } + + groups.clear(); + groups.insert(groups.end(), newLayouts, newLayouts + newNumLayouts); + delete[] newLayouts; + return true; +} + +void +CMSWindowsKeyState::setWindowGroup(SInt32 group) +{ + HWND targetWindow = GetForegroundWindow(); + + bool sysCharSet = true; + // XXX -- determine if m_groups[group] can be used with the system + // character set. + + PostMessage(targetWindow, WM_INPUTLANGCHANGEREQUEST, + sysCharSet ? 1 : 0, (LPARAM)m_groups[group]); + + // XXX -- use a short delay to let the target window process the message + // before it sees the keyboard events. i'm not sure why this is + // necessary since the messages should arrive in order. if we don't + // delay, though, some of our keyboard events may disappear. + Sleep(100); +} + +void +CMSWindowsKeyState::fixKeys() +{ + // fake key releases for the windows keys if we think they're + // down but they're really up. we have to do this because if the + // user presses and releases a windows key without pressing any + // other key while it's down then the system will eat the key + // release. if we don't detect that and synthesize the release + // then the client won't take the usual windows key release action + // (which on windows is to show the start menu). + // + // only check on the windows 95 family since the NT family reports + // the key releases as usual. + if (!m_is95Family) { + return; + } + + KeyButton leftButton = virtualKeyToButton(VK_LWIN); + KeyButton rightButton = virtualKeyToButton(VK_RWIN); + bool leftDown = isKeyDown(leftButton); + bool rightDown = isKeyDown(rightButton); + bool fix = (leftDown || rightDown); + if (fix) { + // check if either button is not really down + bool leftAsyncDown = ((GetAsyncKeyState(VK_LWIN) & 0x8000) != 0); + bool rightAsyncDown = ((GetAsyncKeyState(VK_RWIN) & 0x8000) != 0); + + if (leftAsyncDown != leftDown || rightAsyncDown != rightDown) { + KeyModifierMask state = getActiveModifiers(); + if (!leftAsyncDown && !rightAsyncDown) { + // no win keys are down so remove super modifier + state &= ~KeyModifierSuper; + } + + // report up events + if (leftDown && !leftAsyncDown) { + LOG((CLOG_DEBUG1 "event: fake key release left windows key (0x%03x)", leftButton)); + CKeyState::onKey(leftButton, false, state); + CKeyState::sendKeyEvent(m_eventTarget, false, false, + kKeySuper_L, state, 1, leftButton); + } + if (rightDown && !rightAsyncDown) { + LOG((CLOG_DEBUG1 "event: fake key release right windows key (0x%03x)", rightButton)); + CKeyState::onKey(rightButton, false, state); + CKeyState::sendKeyEvent(m_eventTarget, false, false, + kKeySuper_R, state, 1, rightButton); + } + } + } + + if (fix && m_fixTimer == NULL) { + // schedule check + m_fixTimer = EVENTQUEUE->newTimer(0.1, NULL); + EVENTQUEUE->adoptHandler(CEvent::kTimer, m_fixTimer, + new TMethodEventJob( + this, &CMSWindowsKeyState::handleFixKeys)); + } + else if (!fix && m_fixTimer != NULL) { + // remove scheduled check + EVENTQUEUE->removeHandler(CEvent::kTimer, m_fixTimer); + EVENTQUEUE->deleteTimer(m_fixTimer); + m_fixTimer = NULL; + } +} + +void +CMSWindowsKeyState::handleFixKeys(const CEvent&, void*) +{ + fixKeys(); +} + +KeyID +CMSWindowsKeyState::getKeyID(UINT virtualKey, KeyButton button) +{ + if ((button & 0x100u) != 0) { + virtualKey += 0x100u; + } + return s_virtualKey[virtualKey]; +} + +KeyID +CMSWindowsKeyState::getIDForKey(CKeyMap::KeyItem& item, + KeyButton button, UINT virtualKey, + PBYTE keyState, HKL hkl) const +{ + int n; + KeyID id; + if (m_is95Family) { + // XXX -- how do we get characters not in Latin-1? + WORD ascii; + n = ToAsciiEx(virtualKey, button, keyState, &ascii, 0, hkl); + id = static_cast(ascii & 0xffu); + } + else { + WCHAR unicode[2]; + n = m_ToUnicodeEx(virtualKey, button, keyState, + unicode, sizeof(unicode) / sizeof(unicode[0]), + 0, hkl); + id = static_cast(unicode[0]); + } + switch (n) { + case -1: + return CKeyMap::getDeadKey(id); + + default: + case 0: + // unmapped + return kKeyNone; + + case 1: + return id; + + case 2: + // left over dead key in buffer. this used to recurse, + // but apparently this causes a stack overflow, so just + // return no key instead. + return kKeyNone; + } +} + +void +CMSWindowsKeyState::addKeyEntry(CKeyMap& keyMap, CKeyMap::KeyItem& item) +{ + keyMap.addKeyEntry(item); + if (item.m_group == 0) { + m_keyToVKMap[item.m_id] = static_cast(item.m_client); + } +} diff --git a/src/lib/platform/CMSWindowsKeyState.h b/src/lib/platform/CMSWindowsKeyState.h new file mode 100644 index 00000000..ee57474d --- /dev/null +++ b/src/lib/platform/CMSWindowsKeyState.h @@ -0,0 +1,230 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSKEYSTATE_H +#define CMSWINDOWSKEYSTATE_H + +#include "CKeyState.h" +#include "CString.h" +#include "stdvector.h" +#define WIN32_LEAN_AND_MEAN +#include + +class CEvent; +class CEventQueueTimer; +class CMSWindowsDesks; +class IEventQueue; + +//! Microsoft Windows key mapper +/*! +This class maps KeyIDs to keystrokes. +*/ +class CMSWindowsKeyState : public CKeyState { +public: + CMSWindowsKeyState(CMSWindowsDesks* desks, void* eventTarget); + CMSWindowsKeyState(CMSWindowsDesks* desks, void* eventTarget, IEventQueue& eventQueue, CKeyMap& keyMap); + virtual ~CMSWindowsKeyState(); + + //! @name manipulators + //@{ + + //! Handle screen disabling + /*! + Called when screen is disabled. This is needed to deal with platform + brokenness. + */ + void disable(); + + //! Set the active keyboard layout + /*! + Uses \p keyLayout when querying the keyboard. + */ + void setKeyLayout(HKL keyLayout); + + //! Test and set autorepeat state + /*! + Returns true if the given button is autorepeating and updates internal + state. + */ + bool testAutoRepeat(bool press, bool isRepeat, KeyButton); + + //! Remember modifier state + /*! + Records the current non-toggle modifier state. + */ + void saveModifiers(); + + //! Set effective modifier state + /*! + Temporarily sets the non-toggle modifier state to those saved by the + last call to \c saveModifiers if \p enable is \c true. Restores the + modifier state to the current modifier state if \p enable is \c false. + This is for synthesizing keystrokes on the primary screen when the + cursor is on a secondary screen. When on a secondary screen we capture + all non-toggle modifier state, track the state internally and do not + pass it on. So if Alt+F1 synthesizes Alt+X we need to synthesize + not just X but also Alt, despite the fact that our internal modifier + state indicates Alt is down, because local apps never saw the Alt down + event. + */ + void useSavedModifiers(bool enable); + + //@} + //! @name accessors + //@{ + + //! Map a virtual key to a button + /*! + Returns the button for the \p virtualKey. + */ + KeyButton virtualKeyToButton(UINT virtualKey) const; + + //! Map key event to a key + /*! + Converts a key event into a KeyID and the shadow modifier state + to a modifier mask. + */ + KeyID mapKeyFromEvent(WPARAM charAndVirtKey, + LPARAM info, KeyModifierMask* maskOut) const; + + //! Check if keyboard groups have changed + /*! + Returns true iff the number or order of the keyboard groups have + changed since the last call to updateKeys(). + */ + bool didGroupsChange() const; + + //! Map key to virtual key + /*! + Returns the virtual key for key \p key or 0 if there's no such virtual + key. + */ + UINT mapKeyToVirtualKey(KeyID key) const; + + //! Map virtual key and button to KeyID + /*! + Returns the KeyID for virtual key \p virtualKey and button \p button + (button should include the extended key bit), or kKeyNone if there is + no such key. + */ + static KeyID getKeyID(UINT virtualKey, KeyButton button); + + //@} + + // IKeyState overrides + virtual void fakeKeyDown(KeyID id, KeyModifierMask mask, + KeyButton button); + virtual bool fakeKeyRepeat(KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton button); + virtual bool fakeCtrlAltDel(); + virtual KeyModifierMask + pollActiveModifiers() const; + virtual SInt32 pollActiveGroup() const; + virtual void pollPressedKeys(KeyButtonSet& pressedKeys) const; + + // CKeyState overrides + virtual void onKey(KeyButton button, bool down, + KeyModifierMask newState); + virtual void sendKeyEvent(void* target, + bool press, bool isAutoRepeat, + KeyID key, KeyModifierMask mask, + SInt32 count, KeyButton button); + + // Unit test accessors + KeyButton getLastDown() const { return m_lastDown; } + void setLastDown(KeyButton value) { m_lastDown = value; } + KeyModifierMask getSavedModifiers() const { return m_savedModifiers; } + void setSavedModifiers(KeyModifierMask value) { m_savedModifiers = value; } + +protected: + // CKeyState overrides + virtual void getKeyMap(CKeyMap& keyMap); + virtual void fakeKey(const Keystroke& keystroke); + virtual KeyModifierMask& + getActiveModifiersRValue(); + +private: + typedef std::vector GroupList; + + // send ctrl+alt+del hotkey event on NT family + static void ctrlAltDelThread(void*); + + bool getGroups(GroupList&) const; + void setWindowGroup(SInt32 group); + + void fixKeys(); + void handleFixKeys(const CEvent&, void*); + + KeyID getIDForKey(CKeyMap::KeyItem& item, + KeyButton button, UINT virtualKey, + PBYTE keyState, HKL hkl) const; + + void addKeyEntry(CKeyMap& keyMap, CKeyMap::KeyItem& item); + + void init(); + +private: + // not implemented + CMSWindowsKeyState(const CMSWindowsKeyState&); + CMSWindowsKeyState& operator=(const CMSWindowsKeyState&); + +private: + typedef std::map GroupMap; + typedef std::map KeyToVKMap; + + bool m_is95Family; + void* m_eventTarget; + CMSWindowsDesks* m_desks; + HKL m_keyLayout; + UINT m_buttonToVK[512]; + UINT m_buttonToNumpadVK[512]; + KeyButton m_virtualKeyToButton[256]; + KeyToVKMap m_keyToVKMap; + IEventQueue& m_eventQueue; + + // the timer used to check for fixing key state + CEventQueueTimer* m_fixTimer; + + // the groups (keyboard layouts) + GroupList m_groups; + GroupMap m_groupMap; + + // the last button that we generated a key down event for. this + // is zero if the last key event was a key up. we use this to + // synthesize key repeats since the low level keyboard hook can't + // tell us if an event is a key repeat. + KeyButton m_lastDown; + + // modifier tracking + bool m_useSavedModifiers; + KeyModifierMask m_savedModifiers; + KeyModifierMask m_originalSavedModifiers; + + // pointer to ToUnicodeEx. on win95 family this will be NULL. + typedef int (WINAPI *ToUnicodeEx_t)(UINT wVirtKey, + UINT wScanCode, + PBYTE lpKeyState, + LPWSTR pwszBuff, + int cchBuff, + UINT wFlags, + HKL dwhkl); + ToUnicodeEx_t m_ToUnicodeEx; + + static const KeyID s_virtualKey[]; +}; + +#endif diff --git a/src/lib/platform/CMSWindowsRelauncher.cpp b/src/lib/platform/CMSWindowsRelauncher.cpp new file mode 100644 index 00000000..9827ec06 --- /dev/null +++ b/src/lib/platform/CMSWindowsRelauncher.cpp @@ -0,0 +1,488 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsRelauncher.h" +#include "CThread.h" +#include "TMethodJob.h" +#include "CLog.h" +#include "CArch.h" +#include "Version.h" +#include "CArchDaemonWindows.h" +#include "XArchWindows.h" +#include "CApp.h" +#include "CArgsBase.h" + +#include +#include +#include + +enum { + kOutputBufferSize = 4096 +}; + +typedef VOID (WINAPI *SendSas)(BOOL asUser); + +CMSWindowsRelauncher::CMSWindowsRelauncher(bool autoDetectCommand) : + m_thread(NULL), + m_autoDetectCommand(autoDetectCommand), + m_running(true), + m_commandChanged(false), + m_stdOutWrite(NULL), + m_stdOutRead(NULL) +{ +} + +CMSWindowsRelauncher::~CMSWindowsRelauncher() +{ +} + +void +CMSWindowsRelauncher::startAsync() +{ + m_thread = new CThread(new TMethodJob( + this, &CMSWindowsRelauncher::mainLoop, nullptr)); + + m_outputThread = new CThread(new TMethodJob( + this, &CMSWindowsRelauncher::outputLoop, nullptr)); +} + +void +CMSWindowsRelauncher::stop() +{ + m_running = false; + m_thread->wait(5); +} + +// this still gets the physical session (the one the keyboard and +// mouse is connected to), sometimes this returns -1 but not sure why +DWORD +CMSWindowsRelauncher::getSessionId() +{ + return WTSGetActiveConsoleSessionId(); +} + +BOOL +CMSWindowsRelauncher::winlogonInSession(DWORD sessionId, PHANDLE process) +{ + // first we need to take a snapshot of the running processes + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) { + LOG((CLOG_ERR "could not get process snapshot (error: %i)", + GetLastError())); + return 0; + } + + PROCESSENTRY32 entry; + entry.dwSize = sizeof(PROCESSENTRY32); + + // get the first process, and if we can't do that then it's + // unlikely we can go any further + BOOL gotEntry = Process32First(snapshot, &entry); + if (!gotEntry) { + LOG((CLOG_ERR "could not get first process entry (error: %i)", + GetLastError())); + return 0; + } + + // used to record process names for debug info + std::list nameList; + + // now just iterate until we can find winlogon.exe pid + DWORD pid = 0; + while(gotEntry) { + + // make sure we're not checking the system process + if (entry.th32ProcessID != 0) { + + DWORD processSessionId; + BOOL pidToSidRet = ProcessIdToSessionId( + entry.th32ProcessID, &processSessionId); + + if (!pidToSidRet) { + LOG((CLOG_ERR "could not get session id for process id %i (error: %i)", + entry.th32ProcessID, GetLastError())); + return 0; + } + + // only pay attention to processes in the active session + if (processSessionId == sessionId) { + + // store the names so we can record them for debug + nameList.push_back(entry.szExeFile); + + if (_stricmp(entry.szExeFile, "winlogon.exe") == 0) { + pid = entry.th32ProcessID; + break; + } + } + } + + // now move on to the next entry (if we're not at the end) + gotEntry = Process32Next(snapshot, &entry); + if (!gotEntry) { + + DWORD err = GetLastError(); + if (err != ERROR_NO_MORE_FILES) { + + // only worry about error if it's not the end of the snapshot + LOG((CLOG_ERR "could not get subsiquent process entry (error: %i)", + GetLastError())); + return 0; + } + } + } + + std::string nameListJoin; + for(std::list::iterator it = nameList.begin(); + it != nameList.end(); it++) { + nameListJoin.append(*it); + nameListJoin.append(", "); + } + + LOG((CLOG_DEBUG "checked processes while looking for winlogon.exe: %s", + nameListJoin.c_str())); + + CloseHandle(snapshot); + + if (pid) { + // now get the process so we can get the process, with which + // we'll use to get the process token. + *process = OpenProcess(MAXIMUM_ALLOWED, FALSE, pid); + return true; + } + else { + LOG((CLOG_DEBUG "could not find winlogon.exe in session %i", sessionId)); + return false; + } +} + +// gets the current user (so we can launch under their session) +HANDLE +CMSWindowsRelauncher::getCurrentUserToken(DWORD sessionId, LPSECURITY_ATTRIBUTES security) +{ + HANDLE currentToken; + HANDLE winlogonProcess; + + if (winlogonInSession(sessionId, &winlogonProcess)) { + + LOG((CLOG_DEBUG "session %i has winlogon.exe", sessionId)); + + // get the token, so we can re-launch with this token + // -- do we really need all these access bits? + BOOL tokenRet = OpenProcessToken( + winlogonProcess, + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY | + TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | + TOKEN_ADJUST_SESSIONID | TOKEN_READ | TOKEN_WRITE, + ¤tToken); + + if (!tokenRet) { + LOG((CLOG_ERR "could not open token (error: %i)", GetLastError())); + return 0; + } + } + else { + + LOG((CLOG_ERR "session %i does not have winlogon.exe " + "which is needed for re-launch", sessionId)); + return 0; + } + + HANDLE primaryToken; + BOOL duplicateRet = DuplicateTokenEx( + currentToken, MAXIMUM_ALLOWED, security, + SecurityImpersonation, TokenPrimary, &primaryToken); + + if (!duplicateRet) { + LOG((CLOG_ERR "could not duplicate token %i (error: %i)", + currentToken, GetLastError())); + return 0; + } + + return primaryToken; +} + +void +CMSWindowsRelauncher::mainLoop(void*) +{ + SendSas sendSasFunc = NULL; + HINSTANCE sasLib = LoadLibrary("sas.dll"); + if (sasLib) { + LOG((CLOG_DEBUG "found sas.dll")); + sendSasFunc = (SendSas)GetProcAddress(sasLib, "SendSAS"); + } + + DWORD sessionId = -1; + bool launched = false; + + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + if (!CreatePipe(&m_stdOutRead, &m_stdOutWrite, &saAttr, 0)) { + throw XArch(new XArchEvalWindows()); + } + + PROCESS_INFORMATION pi; + ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); + + while (m_running) { + + HANDLE sendSasEvent = 0; + if (sasLib && sendSasFunc) { + // can't we just create one event? seems weird creating a new + // event every second... + sendSasEvent = CreateEvent(NULL, FALSE, FALSE, "Global\\SendSAS"); + } + + DWORD newSessionId = getSessionId(); + + // only enter here when id changes, and the session isn't -1, which + // may mean that there is no active session. + if (((newSessionId != sessionId) && (newSessionId != -1)) || m_commandChanged) { + + m_commandChanged = false; + + if (launched) { + LOG((CLOG_DEBUG "closing existing process to make way for new one")); + shutdownProcess(pi, 10); + launched = false; + } + + // ok, this is now the active session (forget the old one if any) + sessionId = newSessionId; + + SECURITY_ATTRIBUTES sa; + ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES)); + + // get the token for the user in active session, which is the + // one receiving input from mouse and keyboard. + HANDLE userToken = getCurrentUserToken(sessionId, &sa); + + if (userToken != 0) { + LOG((CLOG_DEBUG "got user token to launch new process")); + + std::string cmd = command(); + if (cmd == "") { + LOG((CLOG_WARN "nothing to launch, no command specified.")); + continue; + } + + // in case reusing process info struct + ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); + + STARTUPINFO si; + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + si.lpDesktop = "winsta0\\default"; + si.hStdError = m_stdOutWrite; + si.hStdOutput = m_stdOutWrite; + si.dwFlags |= STARTF_USESTDHANDLES; + + LPVOID environment; + BOOL blockRet = CreateEnvironmentBlock(&environment, userToken, FALSE); + if (!blockRet) { + LOG((CLOG_ERR "could not create environment block (error: %i)", + GetLastError())); + continue; + } + else { + + DWORD creationFlags = + NORMAL_PRIORITY_CLASS | + CREATE_NO_WINDOW | + CREATE_UNICODE_ENVIRONMENT; + + // re-launch in current active user session + BOOL createRet = CreateProcessAsUser( + userToken, NULL, LPSTR(cmd.c_str()), + &sa, NULL, TRUE, creationFlags, + environment, NULL, &si, &pi); + + DestroyEnvironmentBlock(environment); + CloseHandle(userToken); + + if (!createRet) { + LOG((CLOG_ERR "could not launch (error: %i)", GetLastError())); + continue; + } + else { + LOG((CLOG_DEBUG "launched in session %i (cmd: %s)", + sessionId, cmd.c_str())); + launched = true; + } + } + } + } + + if (sendSasEvent) { + // use SendSAS event to wait for next session. + if (WaitForSingleObject(sendSasEvent, 1000) == WAIT_OBJECT_0 && sendSasFunc) { + LOG((CLOG_DEBUG "calling SendSAS")); + sendSasFunc(FALSE); + } + CloseHandle(sendSasEvent); + } + else { + // check for session change every second. + ARCH->sleep(1); + } + } + + if (launched) { + LOG((CLOG_DEBUG "terminated running process on exit")); + shutdownProcess(pi, 10); + } +} + +void +CMSWindowsRelauncher::command(const std::string& command) +{ + LOG((CLOG_INFO "service command updated")); + LOG((CLOG_DEBUG "new command: %s", command.c_str())); + m_command = command; + m_commandChanged = true; +} + +std::string +CMSWindowsRelauncher::command() const +{ + if (!m_autoDetectCommand) { + return m_command; + } + + // seems like a fairly convoluted way to get the process name + const char* launchName = CApp::instance().argsBase().m_pname; + std::string args = ARCH->commandLine(); + + // build up a full command line + std::stringstream cmdTemp; + cmdTemp << launchName << /*" --debug-data session-" << sessionId <<*/ args; + + std::string cmd = cmdTemp.str(); + + size_t i; + std::string find = "--relaunch"; + while((i = cmd.find(find)) != std::string::npos) { + cmd.replace(i, find.length(), ""); + } + + return cmd; +} + +void +CMSWindowsRelauncher::outputLoop(void*) +{ + CHAR buffer[kOutputBufferSize]; + + while (true) { + + DWORD bytesRead; + BOOL success = ReadFile(m_stdOutRead, buffer, kOutputBufferSize, &bytesRead, NULL); + + // assume the process has gone away? slow down + // the reads until another one turns up. + if (!success || bytesRead == 0) { + ARCH->sleep(1); + } + else { + // send process output over IPC to GUI. + buffer[bytesRead] = '\0'; + ARCH->ipcLog().writeLog(kINFO, buffer); + } + + } +} + +void +CMSWindowsRelauncher::shutdownProcess(const PROCESS_INFORMATION& pi, int timeout) +{ + DWORD exitCode; + GetExitCodeProcess(pi.hProcess, &exitCode); + if (exitCode != STILL_ACTIVE) + return; + + sendIpcMessage(kIpcShutdown, ""); + + // wait for process to exit gracefully. + double start = ARCH->time(); + while (true) + { + GetExitCodeProcess(pi.hProcess, &exitCode); + if (exitCode != STILL_ACTIVE) { + LOG((CLOG_INFO "process %d was shutdown successfully", pi.dwProcessId)); + break; + } + else { + + if ((ARCH->time() - start) > timeout) { + // if timeout reached, kill forcefully. + // calling TerminateProcess on synergy is very bad! + // it causes the hook DLL to stay loaded in some apps, + // making it impossible to start synergy again. + LOG((CLOG_WARN "shutdown timed out after %d secs, forcefully terminating", pi.dwProcessId)); + TerminateProcess(pi.hProcess, kExitSuccess); + break; + } + + ARCH->sleep(1); + } + } +} + +// TODO: put this in an IPC client class. +void +CMSWindowsRelauncher::sendIpcMessage(int type, const char* data) +{ + char message[1024]; + message[0] = type; + char* messagePtr = message; + messagePtr++; + strcpy(messagePtr, data); + + HANDLE pipe = CreateFile( + _T("\\\\.\\pipe\\SynergyNode"), + GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); + + if (pipe == INVALID_HANDLE_VALUE) + { + LOG((CLOG_ERR "could not connect to node, error: %d", GetLastError())); + return; + } + + DWORD dwMode = PIPE_READMODE_MESSAGE; + BOOL stateSuccess = SetNamedPipeHandleState(pipe, &dwMode, NULL, NULL); + + if (!stateSuccess) + { + LOG((CLOG_ERR "could not set node pipe state, error: %d", GetLastError())); + return; + } + + DWORD written; + BOOL writeSuccess = WriteFile( + pipe, message, (DWORD)strlen(message), &written, NULL); + + if (!writeSuccess) + { + LOG((CLOG_ERR "could not write to node pipe, error: %d", GetLastError())); + return; + } + + CloseHandle(pipe); +} diff --git a/src/lib/platform/CMSWindowsRelauncher.h b/src/lib/platform/CMSWindowsRelauncher.h new file mode 100644 index 00000000..7b05f31a --- /dev/null +++ b/src/lib/platform/CMSWindowsRelauncher.h @@ -0,0 +1,54 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +class CThread; + +class CMSWindowsRelauncher { +public: + CMSWindowsRelauncher(bool autoDetectCommand); + virtual ~CMSWindowsRelauncher(); + void startAsync(); + std::string command() const; + void command(const std::string& command); + void stop(); + +private: + void mainLoop(void*); + BOOL winlogonInSession(DWORD sessionId, PHANDLE process); + DWORD getSessionId(); + HANDLE getCurrentUserToken(DWORD sessionId, LPSECURITY_ATTRIBUTES security); + void outputLoop(void*); + void sendIpcMessage(int type, const char* data); + void shutdownProcess(const PROCESS_INFORMATION& pi, int timeout); + +private: + CThread* m_thread; + bool m_autoDetectCommand; + std::string m_command; + bool m_running; + bool m_commandChanged; + HANDLE m_stdOutWrite; + HANDLE m_stdOutRead; + CThread* m_outputThread; +}; diff --git a/src/lib/platform/CMSWindowsScreen.cpp b/src/lib/platform/CMSWindowsScreen.cpp new file mode 100644 index 00000000..a9cc314a --- /dev/null +++ b/src/lib/platform/CMSWindowsScreen.cpp @@ -0,0 +1,1860 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsScreen.h" +#include "CMSWindowsClipboard.h" +#include "CMSWindowsDesks.h" +#include "CMSWindowsEventQueueBuffer.h" +#include "CMSWindowsKeyState.h" +#include "CMSWindowsScreenSaver.h" +#include "CClipboard.h" +#include "CKeyMap.h" +#include "XScreen.h" +#include "CLock.h" +#include "CThread.h" +#include "CFunctionJob.h" +#include "CLog.h" +#include "CString.h" +#include "CStringUtil.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" +#include "TMethodJob.h" +#include "CArch.h" +#include "CArchMiscWindows.h" +#include +#include + +// +// add backwards compatible multihead support (and suppress bogus warning). +// this isn't supported on MinGW yet AFAICT. +// +#if defined(_MSC_VER) +#pragma warning(push) +#pragma warning(disable: 4706) // assignment within conditional +#define COMPILE_MULTIMON_STUBS +#include +#pragma warning(pop) +#endif + +// X button stuff +#if !defined(WM_XBUTTONDOWN) +#define WM_XBUTTONDOWN 0x020B +#define WM_XBUTTONUP 0x020C +#define WM_XBUTTONDBLCLK 0x020D +#define WM_NCXBUTTONDOWN 0x00AB +#define WM_NCXBUTTONUP 0x00AC +#define WM_NCXBUTTONDBLCLK 0x00AD +#define MOUSEEVENTF_XDOWN 0x0080 +#define MOUSEEVENTF_XUP 0x0100 +#define XBUTTON1 0x0001 +#define XBUTTON2 0x0002 +#endif +#if !defined(VK_XBUTTON1) +#define VK_XBUTTON1 0x05 +#define VK_XBUTTON2 0x06 +#endif + +// WM_POWERBROADCAST stuff +#if !defined(PBT_APMRESUMEAUTOMATIC) +#define PBT_APMRESUMEAUTOMATIC 0x0012 +#endif + +// +// CMSWindowsScreen +// + +HINSTANCE CMSWindowsScreen::s_windowInstance = NULL; +CMSWindowsScreen* CMSWindowsScreen::s_screen = NULL; + +CMSWindowsScreen::CMSWindowsScreen(bool isPrimary, bool noHooks, const CGameDeviceInfo& gameDeviceInfo) : + m_isPrimary(isPrimary), + m_noHooks(noHooks), + m_is95Family(CArchMiscWindows::isWindows95Family()), + m_isOnScreen(m_isPrimary), + m_class(0), + m_x(0), m_y(0), + m_w(0), m_h(0), + m_xCenter(0), m_yCenter(0), + m_multimon(false), + m_xCursor(0), m_yCursor(0), + m_sequenceNumber(0), + m_mark(0), + m_markReceived(0), + m_fixTimer(NULL), + m_keyLayout(NULL), + m_screensaver(NULL), + m_screensaverNotify(false), + m_screensaverActive(false), + m_window(NULL), + m_nextClipboardWindow(NULL), + m_ownClipboard(false), + m_desks(NULL), + m_hookLibrary(NULL), + m_keyState(NULL), + m_hasMouse(GetSystemMetrics(SM_MOUSEPRESENT) != 0), + m_showingMouse(false), + m_gameDeviceInfo(gameDeviceInfo), + m_gameDevice(NULL) +{ + assert(s_windowInstance != NULL); + assert(s_screen == NULL); + + s_screen = this; + try { + if (m_isPrimary && !m_noHooks) { + m_hookLibrary = openHookLibrary("synrgyhk"); + } + m_screensaver = new CMSWindowsScreenSaver(); + m_desks = new CMSWindowsDesks( + m_isPrimary, m_noHooks, + m_hookLibrary, m_screensaver, + new TMethodJob(this, + &CMSWindowsScreen::updateKeysCB)); + m_keyState = new CMSWindowsKeyState(m_desks, getEventTarget()); + updateScreenShape(); + m_class = createWindowClass(); + m_window = createWindow(m_class, "Synergy"); + forceShowCursor(); + 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)); + } + catch (...) { + delete m_keyState; + delete m_desks; + delete m_screensaver; + destroyWindow(m_window); + destroyClass(m_class); + + if (m_hookLibrary != NULL) + closeHookLibrary(m_hookLibrary); + + s_screen = NULL; + throw; + } + + // install event handlers + EVENTQUEUE->adoptHandler(CEvent::kSystem, IEventQueue::getSystemTarget(), + new TMethodEventJob(this, + &CMSWindowsScreen::handleSystemEvent)); + + // install the platform event queue + EVENTQUEUE->adoptBuffer(new CMSWindowsEventQueueBuffer); + + if ((gameDeviceInfo.m_mode == CGameDeviceInfo::kGameModeXInput) && + (gameDeviceInfo.m_poll != CGameDeviceInfo::kGamePollDynamic)) + LOG((CLOG_WARN "only dynamic polling is supported with xnput.")); + + if ((gameDeviceInfo.m_mode == CGameDeviceInfo::kGameModeJoyInfoEx) && + (gameDeviceInfo.m_poll != CGameDeviceInfo::kGamePollStatic)) + LOG((CLOG_WARN "only static polling is supported with joyinfoex.")); + + if (m_gameDeviceInfo.m_mode == CGameDeviceInfo::kGameModeXInput) { +#if GAME_DEVICE_SUPPORT + m_gameDevice = new CMSWindowsXInput(this, gameDeviceInfo); +#else if _AMD64_ + LOG((CLOG_WARN "xinput game device mode not supported for 64-bit.")); +#endif + } + else { + m_gameDevice = new CEventGameDevice(getEventTarget()); + } +} + +CMSWindowsScreen::~CMSWindowsScreen() +{ + assert(s_screen != NULL); + + disable(); + EVENTQUEUE->adoptBuffer(NULL); + EVENTQUEUE->removeHandler(CEvent::kSystem, IEventQueue::getSystemTarget()); + delete m_keyState; + delete m_desks; + delete m_screensaver; + destroyWindow(m_window); + destroyClass(m_class); + + if (m_gameDevice != NULL) + delete m_gameDevice; + + if (m_hookLibrary != NULL) + closeHookLibrary(m_hookLibrary); + + s_screen = NULL; +} + +void +CMSWindowsScreen::init(HINSTANCE windowInstance) +{ + assert(s_windowInstance == NULL); + assert(windowInstance != NULL); + + s_windowInstance = windowInstance; +} + +HINSTANCE +CMSWindowsScreen::getWindowInstance() +{ + return s_windowInstance; +} + +void +CMSWindowsScreen::enable() +{ + assert(m_isOnScreen == m_isPrimary); + + // we need to poll some things to fix them + m_fixTimer = EVENTQUEUE->newTimer(1.0, NULL); + EVENTQUEUE->adoptHandler(CEvent::kTimer, m_fixTimer, + new TMethodEventJob(this, + &CMSWindowsScreen::handleFixes)); + + // install our clipboard snooper + m_nextClipboardWindow = SetClipboardViewer(m_window); + + // track the active desk and (re)install the hooks + m_desks->enable(); + + if (m_isPrimary) { + if (m_hookLibrary != NULL) { + // set jump zones + m_hookLibraryLoader.m_setZone(m_x, m_y, m_w, m_h, getJumpZoneSize()); + + // watch jump zones + m_hookLibraryLoader.m_setMode(kHOOK_WATCH_JUMP_ZONE); + } + } + else { + // prevent the system from entering power saving modes. if + // it did we'd be forced to disconnect from the server and + // the server would not be able to wake us up. + CArchMiscWindows::addBusyState(CArchMiscWindows::kSYSTEM); + } +} + +void +CMSWindowsScreen::disable() +{ + // stop tracking the active desk + m_desks->disable(); + + if (m_isPrimary) { + if (m_hookLibrary != NULL) { + // disable hooks + m_hookLibraryLoader.m_setMode(kHOOK_DISABLE); + } + + // enable special key sequences on win95 family + enableSpecialKeys(true); + } + else { + // allow the system to enter power saving mode + CArchMiscWindows::removeBusyState(CArchMiscWindows::kSYSTEM | + CArchMiscWindows::kDISPLAY); + } + + // tell key state + m_keyState->disable(); + + // stop snooping the clipboard + ChangeClipboardChain(m_window, m_nextClipboardWindow); + m_nextClipboardWindow = NULL; + + // uninstall fix timer + if (m_fixTimer != NULL) { + EVENTQUEUE->removeHandler(CEvent::kTimer, m_fixTimer); + EVENTQUEUE->deleteTimer(m_fixTimer); + m_fixTimer = NULL; + } + + m_isOnScreen = m_isPrimary; + forceShowCursor(); +} + +void +CMSWindowsScreen::enter() +{ + m_desks->enter(); + if (m_isPrimary) { + // enable special key sequences on win95 family + enableSpecialKeys(true); + + if (m_hookLibrary != NULL) { + // watch jump zones + m_hookLibraryLoader.m_setMode(kHOOK_WATCH_JUMP_ZONE); + } + + // all messages prior to now are invalid + nextMark(); + } else { + // Entering a secondary screen. Ensure that no screensaver is active + // and that the screen is not in powersave mode. + CArchMiscWindows::wakeupDisplay(); + + if(m_screensaver != NULL && m_screensaverActive) + { + m_screensaver->deactivate(); + m_screensaverActive = 0; + } + } + + // now on screen + m_isOnScreen = true; + forceShowCursor(); +} + +bool +CMSWindowsScreen::leave() +{ + // get keyboard layout of foreground window. we'll use this + // keyboard layout for translating keys sent to clients. + HWND window = GetForegroundWindow(); + DWORD thread = GetWindowThreadProcessId(window, NULL); + m_keyLayout = GetKeyboardLayout(thread); + + // tell the key mapper about the keyboard layout + m_keyState->setKeyLayout(m_keyLayout); + + // tell desk that we're leaving and tell it the keyboard layout + m_desks->leave(m_keyLayout); + + if (m_isPrimary) { + + // warp to center + LOG((CLOG_DEBUG1 "warping cursor to center: %+d, %+d", m_xCenter, m_yCenter)); + warpCursor(m_xCenter, m_yCenter); + + // disable special key sequences on win95 family + enableSpecialKeys(false); + + // all messages prior to now are invalid + nextMark(); + + // remember the modifier state. this is the modifier state + // reflected in the internal keyboard state. + m_keyState->saveModifiers(); + + if (m_hookLibrary != NULL) { + // capture events + m_hookLibraryLoader.m_setMode(kHOOK_RELAY_EVENTS); + } + } + + // now off screen + m_isOnScreen = false; + forceShowCursor(); + + return true; +} + +bool +CMSWindowsScreen::setClipboard(ClipboardID, const IClipboard* src) +{ + CMSWindowsClipboard dst(m_window); + if (src != NULL) { + // save clipboard data + return CClipboard::copy(&dst, src); + } + else { + // assert clipboard ownership + if (!dst.open(0)) { + return false; + } + dst.empty(); + dst.close(); + return true; + } +} + +void +CMSWindowsScreen::checkClipboards() +{ + // if we think we own the clipboard but we don't then somebody + // grabbed the clipboard on this screen without us knowing. + // tell the server that this screen grabbed the clipboard. + // + // this works around bugs in the clipboard viewer chain. + // sometimes NT will simply never send WM_DRAWCLIPBOARD + // messages for no apparent reason and rebooting fixes the + // problem. since we don't want a broken clipboard until the + // next reboot we do this double check. clipboard ownership + // won't be reflected on other screens until we leave but at + // least the clipboard itself will work. + if (m_ownClipboard && !CMSWindowsClipboard::isOwnedBySynergy()) { + LOG((CLOG_DEBUG "clipboard changed: lost ownership and no notification received")); + m_ownClipboard = false; + sendClipboardEvent(getClipboardGrabbedEvent(), kClipboardClipboard); + sendClipboardEvent(getClipboardGrabbedEvent(), kClipboardSelection); + } +} + +void +CMSWindowsScreen::openScreensaver(bool notify) +{ + assert(m_screensaver != NULL); + + m_screensaverNotify = notify; + if (m_screensaverNotify) { + m_desks->installScreensaverHooks(true); + } + else if (m_screensaver) { + m_screensaver->disable(); + } +} + +void +CMSWindowsScreen::closeScreensaver() +{ + if (m_screensaver != NULL) { + if (m_screensaverNotify) { + m_desks->installScreensaverHooks(false); + } + else { + m_screensaver->enable(); + } + } + m_screensaverNotify = false; +} + +void +CMSWindowsScreen::screensaver(bool activate) +{ + assert(m_screensaver != NULL); + if (m_screensaver==NULL) return; + + if (activate) { + m_screensaver->activate(); + } + else { + m_screensaver->deactivate(); + } +} + +void +CMSWindowsScreen::resetOptions() +{ + m_desks->resetOptions(); +} + +void +CMSWindowsScreen::setOptions(const COptionsList& options) +{ + m_desks->setOptions(options); +} + +void +CMSWindowsScreen::setSequenceNumber(UInt32 seqNum) +{ + m_sequenceNumber = seqNum; +} + +bool +CMSWindowsScreen::isPrimary() const +{ + return m_isPrimary; +} + +void* +CMSWindowsScreen::getEventTarget() const +{ + return const_cast(this); +} + +bool +CMSWindowsScreen::getClipboard(ClipboardID, IClipboard* dst) const +{ + CMSWindowsClipboard src(m_window); + CClipboard::copy(dst, &src); + return true; +} + +void +CMSWindowsScreen::getShape(SInt32& x, SInt32& y, SInt32& w, SInt32& h) const +{ + assert(m_class != 0); + + x = m_x; + y = m_y; + w = m_w; + h = m_h; +} + +void +CMSWindowsScreen::getCursorPos(SInt32& x, SInt32& y) const +{ + m_desks->getCursorPos(x, y); +} + +void +CMSWindowsScreen::reconfigure(UInt32 activeSides) +{ + assert(m_isPrimary); + + LOG((CLOG_DEBUG "active sides: %x", activeSides)); + + if (m_hookLibrary != NULL) + m_hookLibraryLoader.m_setSides(activeSides); +} + +void +CMSWindowsScreen::warpCursor(SInt32 x, SInt32 y) +{ + // warp mouse + warpCursorNoFlush(x, y); + + // remove all input events before and including warp + MSG msg; + while (PeekMessage(&msg, NULL, SYNERGY_MSG_INPUT_FIRST, + SYNERGY_MSG_INPUT_LAST, PM_REMOVE)) { + // do nothing + } + + // save position to compute delta of next motion + saveMousePosition(x, y); +} + +void CMSWindowsScreen::saveMousePosition(SInt32 x, SInt32 y) { + + m_xCursor = x; + m_yCursor = y; + + LOG((CLOG_DEBUG5 "saved mouse position for next delta: %+d,%+d", x,y)); +} + +UInt32 +CMSWindowsScreen::registerHotKey(KeyID key, KeyModifierMask mask) +{ + // only allow certain modifiers + if ((mask & ~(KeyModifierShift | KeyModifierControl | + KeyModifierAlt | KeyModifierSuper)) != 0) { + // this should be a warning, but this can confuse users, + // as this warning happens almost always. + LOG((CLOG_DEBUG "could not map hotkey id=%04x mask=%04x", key, mask)); + return 0; + } + + // fail if no keys + if (key == kKeyNone && mask == 0) { + return 0; + } + + // convert to win32 + UINT modifiers = 0; + if ((mask & KeyModifierShift) != 0) { + modifiers |= MOD_SHIFT; + } + if ((mask & KeyModifierControl) != 0) { + modifiers |= MOD_CONTROL; + } + if ((mask & KeyModifierAlt) != 0) { + modifiers |= MOD_ALT; + } + if ((mask & KeyModifierSuper) != 0) { + modifiers |= MOD_WIN; + } + UINT vk = m_keyState->mapKeyToVirtualKey(key); + if (key != kKeyNone && vk == 0) { + // can't map key + // this should be a warning, but this can confuse users, + // as this warning happens almost always. + LOG((CLOG_DEBUG "could not map hotkey id=%04x mask=%04x", key, mask)); + return 0; + } + + // choose hotkey id + UInt32 id; + if (!m_oldHotKeyIDs.empty()) { + id = m_oldHotKeyIDs.back(); + m_oldHotKeyIDs.pop_back(); + } + else { + //id = m_hotKeys.size() + 1; + id = (UInt32)m_hotKeys.size() + 1; + } + + // if this hot key has modifiers only then we'll handle it specially + bool err; + if (key == kKeyNone) { + // check if already registered + err = (m_hotKeyToIDMap.count(CHotKeyItem(vk, modifiers)) > 0); + } + else { + // register with OS + err = (RegisterHotKey(NULL, id, modifiers, vk) == 0); + } + + if (!err) { + m_hotKeys.insert(std::make_pair(id, CHotKeyItem(vk, modifiers))); + m_hotKeyToIDMap[CHotKeyItem(vk, modifiers)] = id; + } + else { + m_oldHotKeyIDs.push_back(id); + m_hotKeys.erase(id); + LOG((CLOG_WARN "failed to register hotkey %s (id=%04x mask=%04x)", CKeyMap::formatKey(key, mask).c_str(), key, mask)); + return 0; + } + + LOG((CLOG_DEBUG "registered hotkey %s (id=%04x mask=%04x) as id=%d", CKeyMap::formatKey(key, mask).c_str(), key, mask, id)); + return id; +} + +void +CMSWindowsScreen::unregisterHotKey(UInt32 id) +{ + // look up hotkey + HotKeyMap::iterator i = m_hotKeys.find(id); + if (i == m_hotKeys.end()) { + return; + } + + // unregister with OS + bool err; + if (i->second.getVirtualKey() != 0) { + err = !UnregisterHotKey(NULL, id); + } + else { + err = false; + } + if (err) { + LOG((CLOG_WARN "failed to unregister hotkey id=%d", id)); + } + else { + LOG((CLOG_DEBUG "unregistered hotkey id=%d", id)); + } + + // discard hot key from map and record old id for reuse + m_hotKeyToIDMap.erase(i->second); + m_hotKeys.erase(i); + m_oldHotKeyIDs.push_back(id); +} + +void +CMSWindowsScreen::fakeInputBegin() +{ + assert(m_isPrimary); + + if (!m_isOnScreen) { + m_keyState->useSavedModifiers(true); + } + m_desks->fakeInputBegin(); +} + +void +CMSWindowsScreen::fakeInputEnd() +{ + assert(m_isPrimary); + + m_desks->fakeInputEnd(); + if (!m_isOnScreen) { + m_keyState->useSavedModifiers(false); + } +} + +SInt32 +CMSWindowsScreen::getJumpZoneSize() const +{ + return 1; +} + +bool +CMSWindowsScreen::isAnyMouseButtonDown() const +{ + static const char* buttonToName[] = { + "", + "Left Button", + "Middle Button", + "Right Button", + "X Button 1", + "X Button 2" + }; + + for (UInt32 i = 1; i < sizeof(m_buttons) / sizeof(m_buttons[0]); ++i) { + if (m_buttons[i]) { + LOG((CLOG_DEBUG "locked by \"%s\"", buttonToName[i])); + return true; + } + } + + return false; +} + +void +CMSWindowsScreen::getCursorCenter(SInt32& x, SInt32& y) const +{ + x = m_xCenter; + y = m_yCenter; +} + +void +CMSWindowsScreen::gameDeviceTimingResp(UInt16 freq) +{ + m_gameDevice->gameDeviceTimingResp(freq); +} + +void +CMSWindowsScreen::gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2) +{ + m_gameDevice->gameDeviceFeedback(id, m1, m2); +} + +void +CMSWindowsScreen::fakeMouseButton(ButtonID id, bool press) +{ + m_desks->fakeMouseButton(id, press); +} + +void +CMSWindowsScreen::fakeMouseMove(SInt32 x, SInt32 y) const +{ + m_desks->fakeMouseMove(x, y); +} + +void +CMSWindowsScreen::fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const +{ + m_desks->fakeMouseRelativeMove(dx, dy); +} + +void +CMSWindowsScreen::fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const +{ + m_desks->fakeMouseWheel(xDelta, yDelta); +} + +void +CMSWindowsScreen::fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const +{ + LOG((CLOG_DEBUG "fake game device buttons id=%d buttons=%d", id, buttons)); + m_gameDevice->fakeGameDeviceButtons(id, buttons); +} + +void +CMSWindowsScreen::fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const +{ + LOG((CLOG_DEBUG "fake game device sticks id=%d s1=%+d,%+d s2=%+d,%+d", id, x1, y1, x2, y2)); + m_gameDevice->fakeGameDeviceSticks(id, x1, y1, x2, y2); +} + +void +CMSWindowsScreen::fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const +{ + LOG((CLOG_DEBUG "fake game device triggers id=%d t1=%d t2=%d", id, t1, t2)); + m_gameDevice->fakeGameDeviceTriggers(id, t1, t2); +} + +void +CMSWindowsScreen::queueGameDeviceTimingReq() const +{ + LOG((CLOG_DEBUG "queue game device timing request")); + m_gameDevice->queueGameDeviceTimingReq(); +} + +void +CMSWindowsScreen::updateKeys() +{ + m_desks->updateKeys(); +} + +void +CMSWindowsScreen::fakeKeyDown(KeyID id, KeyModifierMask mask, + KeyButton button) +{ + CPlatformScreen::fakeKeyDown(id, mask, button); + updateForceShowCursor(); +} + +bool +CMSWindowsScreen::fakeKeyRepeat(KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton button) +{ + bool result = CPlatformScreen::fakeKeyRepeat(id, mask, count, button); + updateForceShowCursor(); + return result; +} + +bool +CMSWindowsScreen::fakeKeyUp(KeyButton button) +{ + bool result = CPlatformScreen::fakeKeyUp(button); + updateForceShowCursor(); + return result; +} + +void +CMSWindowsScreen::fakeAllKeysUp() +{ + CPlatformScreen::fakeAllKeysUp(); + updateForceShowCursor(); +} + +HINSTANCE +CMSWindowsScreen::openHookLibrary(const char* name) +{ + return m_hookLibraryLoader.openHookLibrary(name); +} + +void +CMSWindowsScreen::closeHookLibrary(HINSTANCE hookLibrary) const +{ + if (hookLibrary != NULL) { + m_hookLibraryLoader.m_cleanup(); + FreeLibrary(hookLibrary); + } +} + +HCURSOR +CMSWindowsScreen::createBlankCursor() const +{ + // create a transparent cursor + int cw = GetSystemMetrics(SM_CXCURSOR); + int ch = GetSystemMetrics(SM_CYCURSOR); + + UInt8* cursorAND = new UInt8[ch * ((cw + 31) >> 2)]; + UInt8* cursorXOR = new UInt8[ch * ((cw + 31) >> 2)]; + memset(cursorAND, 0xff, ch * ((cw + 31) >> 2)); + memset(cursorXOR, 0x00, ch * ((cw + 31) >> 2)); + HCURSOR c = CreateCursor(s_windowInstance, 0, 0, cw, ch, cursorAND, cursorXOR); + delete[] cursorXOR; + delete[] cursorAND; + return c; +} + +void +CMSWindowsScreen::destroyCursor(HCURSOR cursor) const +{ + if (cursor != NULL) { + DestroyCursor(cursor); + } +} + +ATOM +CMSWindowsScreen::createWindowClass() const +{ + WNDCLASSEX classInfo; + classInfo.cbSize = sizeof(classInfo); + classInfo.style = CS_DBLCLKS | CS_NOCLOSE; + classInfo.lpfnWndProc = &CMSWindowsScreen::wndProc; + classInfo.cbClsExtra = 0; + classInfo.cbWndExtra = 0; + classInfo.hInstance = s_windowInstance; + classInfo.hIcon = NULL; + classInfo.hCursor = NULL; + classInfo.hbrBackground = NULL; + classInfo.lpszMenuName = NULL; + classInfo.lpszClassName = "Synergy"; + classInfo.hIconSm = NULL; + return RegisterClassEx(&classInfo); +} + +void +CMSWindowsScreen::destroyClass(ATOM windowClass) const +{ + if (windowClass != 0) { + UnregisterClass(reinterpret_cast(windowClass), s_windowInstance); + } +} + +HWND +CMSWindowsScreen::createWindow(ATOM windowClass, const char* name) const +{ + HWND window = CreateWindowEx(WS_EX_TOPMOST | + WS_EX_TRANSPARENT | + WS_EX_TOOLWINDOW, + reinterpret_cast(windowClass), + name, + WS_POPUP, + 0, 0, 1, 1, + NULL, NULL, + s_windowInstance, + NULL); + if (window == NULL) { + LOG((CLOG_ERR "failed to create window: %d", GetLastError())); + throw XScreenOpenFailure(); + } + return window; +} + +void +CMSWindowsScreen::destroyWindow(HWND hwnd) const +{ + if (hwnd != NULL) { + DestroyWindow(hwnd); + } +} + +void +CMSWindowsScreen::sendEvent(CEvent::Type type, void* data) +{ + EVENTQUEUE->addEvent(CEvent(type, getEventTarget(), data)); +} + +void +CMSWindowsScreen::sendClipboardEvent(CEvent::Type type, ClipboardID id) +{ + CClipboardInfo* info = (CClipboardInfo*)malloc(sizeof(CClipboardInfo)); + if(info == NULL) { + LOG((CLOG_ERR "malloc failed on %s:%s", __FILE__, __LINE__ )); + return; + } + info->m_id = id; + info->m_sequenceNumber = m_sequenceNumber; + sendEvent(type, info); +} + +void +CMSWindowsScreen::handleSystemEvent(const CEvent& event, void*) +{ + MSG* msg = reinterpret_cast(event.getData()); + assert(msg != NULL); + + if (CArchMiscWindows::processDialog(msg)) { + return; + } + if (onPreDispatch(msg->hwnd, msg->message, msg->wParam, msg->lParam)) { + return; + } + TranslateMessage(msg); + DispatchMessage(msg); +} + +void +CMSWindowsScreen::updateButtons() +{ + int numButtons = GetSystemMetrics(SM_CMOUSEBUTTONS); + m_buttons[kButtonNone] = false; + m_buttons[kButtonLeft] = (GetKeyState(VK_LBUTTON) < 0); + m_buttons[kButtonRight] = (GetKeyState(VK_RBUTTON) < 0); + m_buttons[kButtonMiddle] = (GetKeyState(VK_MBUTTON) < 0); + m_buttons[kButtonExtra0 + 0] = (numButtons >= 4) && + (GetKeyState(VK_XBUTTON1) < 0); + m_buttons[kButtonExtra0 + 1] = (numButtons >= 5) && + (GetKeyState(VK_XBUTTON2) < 0); +} + +IKeyState* +CMSWindowsScreen::getKeyState() const +{ + return m_keyState; +} + +bool +CMSWindowsScreen::onPreDispatch(HWND hwnd, + UINT message, WPARAM wParam, LPARAM lParam) +{ + // handle event + switch (message) { + case SYNERGY_MSG_SCREEN_SAVER: + return onScreensaver(wParam != 0); + + case SYNERGY_MSG_DEBUG: + LOG((CLOG_DEBUG1 "hook: 0x%08x 0x%08x", wParam, lParam)); + return true; + } + + if (m_isPrimary) { + return onPreDispatchPrimary(hwnd, message, wParam, lParam); + } + + return false; +} + +bool +CMSWindowsScreen::onPreDispatchPrimary(HWND, + UINT message, WPARAM wParam, LPARAM lParam) +{ + LOG((CLOG_DEBUG5 "handling pre-dispatch primary")); + + // handle event + switch (message) { + case SYNERGY_MSG_MARK: + return onMark(static_cast(wParam)); + + case SYNERGY_MSG_KEY: + return onKey(wParam, lParam); + + case SYNERGY_MSG_MOUSE_BUTTON: + return onMouseButton(wParam, lParam); + + case SYNERGY_MSG_MOUSE_MOVE: + return onMouseMove(static_cast(wParam), + static_cast(lParam)); + + case SYNERGY_MSG_MOUSE_WHEEL: + // XXX -- support x-axis scrolling + return onMouseWheel(0, static_cast(wParam)); + + case SYNERGY_MSG_PRE_WARP: + { + // save position to compute delta of next motion + saveMousePosition(static_cast(wParam), static_cast(lParam)); + + // we warped the mouse. discard events until we find the + // matching post warp event. see warpCursorNoFlush() for + // where the events are sent. we discard the matching + // post warp event and can be sure we've skipped the warp + // event. + MSG msg; + do { + GetMessage(&msg, NULL, SYNERGY_MSG_MOUSE_MOVE, + SYNERGY_MSG_POST_WARP); + } while (msg.message != SYNERGY_MSG_POST_WARP); + } + return true; + + case SYNERGY_MSG_POST_WARP: + LOG((CLOG_WARN "unmatched post warp")); + return true; + + case WM_HOTKEY: + // we discard these messages. we'll catch the hot key in the + // regular key event handling, where we can detect both key + // press and release. we only register the hot key so no other + // app will act on the key combination. + break; + } + + return false; +} + +bool +CMSWindowsScreen::onEvent(HWND, UINT msg, + WPARAM wParam, LPARAM lParam, LRESULT* result) +{ + switch (msg) { + case WM_QUERYENDSESSION: + if (m_is95Family) { + *result = TRUE; + return true; + } + break; + + case WM_ENDSESSION: + if (m_is95Family) { + if (wParam == TRUE && lParam == 0) { + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); + } + return true; + } + break; + + case WM_DRAWCLIPBOARD: + // first pass on the message + if (m_nextClipboardWindow != NULL) { + SendMessage(m_nextClipboardWindow, msg, wParam, lParam); + } + + // now handle the message + return onClipboardChange(); + + case WM_CHANGECBCHAIN: + if (m_nextClipboardWindow == (HWND)wParam) { + m_nextClipboardWindow = (HWND)lParam; + LOG((CLOG_DEBUG "clipboard chain: new next: 0x%08x", m_nextClipboardWindow)); + } + else if (m_nextClipboardWindow != NULL) { + SendMessage(m_nextClipboardWindow, msg, wParam, lParam); + } + return true; + + case WM_DISPLAYCHANGE: + return onDisplayChange(); + + case WM_POWERBROADCAST: + switch (wParam) { + case PBT_APMRESUMEAUTOMATIC: + case PBT_APMRESUMECRITICAL: + case PBT_APMRESUMESUSPEND: + EVENTQUEUE->addEvent(CEvent(IScreen::getResumeEvent(), + getEventTarget(), NULL, + CEvent::kDeliverImmediately)); + break; + + case PBT_APMSUSPEND: + EVENTQUEUE->addEvent(CEvent(IScreen::getSuspendEvent(), + getEventTarget(), NULL, + CEvent::kDeliverImmediately)); + break; + } + *result = TRUE; + return true; + + case WM_DEVICECHANGE: + forceShowCursor(); + break; + + case WM_SETTINGCHANGE: + if (wParam == SPI_SETMOUSEKEYS) { + forceShowCursor(); + } + break; + } + + return false; +} + +bool +CMSWindowsScreen::onMark(UInt32 mark) +{ + m_markReceived = mark; + return true; +} + +bool +CMSWindowsScreen::onKey(WPARAM wParam, LPARAM lParam) +{ + static const KeyModifierMask s_ctrlAlt = + KeyModifierControl | KeyModifierAlt; + + LOG((CLOG_DEBUG1 "event: Key char=%d, vk=0x%02x, nagr=%d, lParam=0x%08x", (wParam & 0xff00u) >> 8, wParam & 0xffu, (wParam & 0x10000u) ? 1 : 0, lParam)); + + // get event info + KeyButton button = (KeyButton)((lParam & 0x01ff0000) >> 16); + bool down = ((lParam & 0x80000000u) == 0x00000000u); + bool wasDown = isKeyDown(button); + KeyModifierMask oldState = pollActiveModifiers(); + + // check for autorepeat + if (m_keyState->testAutoRepeat(down, (lParam & 0x40000000u) == 1, button)) { + lParam |= 0x40000000u; + } + + // if the button is zero then guess what the button should be. + // these are badly synthesized key events and logitech software + // that maps mouse buttons to keys is known to do this. + // alternatively, we could just throw these events out. + if (button == 0) { + button = m_keyState->virtualKeyToButton(wParam & 0xffu); + if (button == 0) { + return true; + } + wasDown = isKeyDown(button); + } + + // record keyboard state + m_keyState->onKey(button, down, oldState); + + // windows doesn't tell us the modifier key state on mouse or key + // events so we have to figure it out. most apps would use + // GetKeyState() or even GetAsyncKeyState() for that but we can't + // because our hook doesn't pass on key events for several modifiers. + // it can't otherwise the system would interpret them normally on + // the primary screen even when on a secondary screen. so tapping + // alt would activate menus and tapping the windows key would open + // the start menu. if you don't pass those events on in the hook + // then GetKeyState() understandably doesn't reflect the effect of + // the event. curiously, neither does GetAsyncKeyState(), which is + // surprising. + // + // so anyway, we have to track the modifier state ourselves for + // at least those modifiers we don't pass on. pollActiveModifiers() + // does that but we have to update the keyboard state before calling + // pollActiveModifiers() to get the right answer. but the only way + // to set the modifier state or to set the up/down state of a key + // is via onKey(). so we have to call onKey() twice. + KeyModifierMask state = pollActiveModifiers(); + m_keyState->onKey(button, down, state); + + // check for hot keys + if (oldState != state) { + // modifier key was pressed/released + if (onHotKey(0, lParam)) { + return true; + } + } + else { + // non-modifier was pressed/released + if (onHotKey(wParam, lParam)) { + return true; + } + } + + // ignore message if posted prior to last mark change + if (!ignore()) { + // check for ctrl+alt+del. we do not want to pass that to the + // client. the user can use ctrl+alt+pause to emulate it. + UINT virtKey = (wParam & 0xffu); + if (virtKey == VK_DELETE && (state & s_ctrlAlt) == s_ctrlAlt) { + LOG((CLOG_DEBUG "discard ctrl+alt+del")); + return true; + } + + // check for ctrl+alt+del emulation + if ((virtKey == VK_PAUSE || virtKey == VK_CANCEL) && + (state & s_ctrlAlt) == s_ctrlAlt) { + LOG((CLOG_DEBUG "emulate ctrl+alt+del")); + // switch wParam and lParam to be as if VK_DELETE was + // pressed or released. when mapping the key we require that + // we not use AltGr (the 0x10000 flag in wParam) and we not + // use the keypad delete key (the 0x01000000 flag in lParam). + wParam = VK_DELETE | 0x00010000u; + lParam &= 0xfe000000; + lParam |= m_keyState->virtualKeyToButton(wParam & 0xffu) << 16; + lParam |= 0x01000001; + } + + // process key + KeyModifierMask mask; + KeyID key = m_keyState->mapKeyFromEvent(wParam, lParam, &mask); + button = static_cast((lParam & 0x01ff0000u) >> 16); + if (key != kKeyNone) { + // fix key up. if the key isn't down according to + // our table then we never got the key press event + // for it. if it's not a modifier key then we'll + // synthesize the press first. only do this on + // the windows 95 family, which eats certain special + // keys like alt+tab, ctrl+esc, etc. + if (m_is95Family && !wasDown && !down) { + switch (virtKey) { + case VK_SHIFT: + case VK_LSHIFT: + case VK_RSHIFT: + case VK_CONTROL: + case VK_LCONTROL: + case VK_RCONTROL: + case VK_MENU: + case VK_LMENU: + case VK_RMENU: + case VK_LWIN: + case VK_RWIN: + case VK_CAPITAL: + case VK_NUMLOCK: + case VK_SCROLL: + break; + + default: + m_keyState->sendKeyEvent(getEventTarget(), + true, false, key, mask, 1, button); + break; + } + } + + // do it + m_keyState->sendKeyEvent(getEventTarget(), + ((lParam & 0x80000000u) == 0), + ((lParam & 0x40000000u) != 0), + key, mask, (SInt32)(lParam & 0xffff), button); + } + else { + LOG((CLOG_DEBUG1 "cannot map key")); + } + } + + return true; +} + +bool +CMSWindowsScreen::onHotKey(WPARAM wParam, LPARAM lParam) +{ + // get the key info + KeyModifierMask state = getActiveModifiers(); + UINT virtKey = (wParam & 0xffu); + UINT modifiers = 0; + if ((state & KeyModifierShift) != 0) { + modifiers |= MOD_SHIFT; + } + if ((state & KeyModifierControl) != 0) { + modifiers |= MOD_CONTROL; + } + if ((state & KeyModifierAlt) != 0) { + modifiers |= MOD_ALT; + } + if ((state & KeyModifierSuper) != 0) { + modifiers |= MOD_WIN; + } + + // find the hot key id + HotKeyToIDMap::const_iterator i = + m_hotKeyToIDMap.find(CHotKeyItem(virtKey, modifiers)); + if (i == m_hotKeyToIDMap.end()) { + return false; + } + + // find what kind of event + CEvent::Type type; + if ((lParam & 0x80000000u) == 0u) { + if ((lParam & 0x40000000u) != 0u) { + // ignore key repeats but it counts as a hot key + return true; + } + type = getHotKeyDownEvent(); + } + else { + type = getHotKeyUpEvent(); + } + + // generate event + EVENTQUEUE->addEvent(CEvent(type, getEventTarget(), + CHotKeyInfo::alloc(i->second))); + + return true; +} + +bool +CMSWindowsScreen::onMouseButton(WPARAM wParam, LPARAM lParam) +{ + // get which button + bool pressed = mapPressFromEvent(wParam, lParam); + ButtonID button = mapButtonFromEvent(wParam, lParam); + + // keep our shadow key state up to date + if (button >= kButtonLeft && button <= kButtonExtra0 + 1) { + if (pressed) { + m_buttons[button] = true; + } + else { + m_buttons[button] = false; + } + } + + // ignore message if posted prior to last mark change + if (!ignore()) { + KeyModifierMask mask = m_keyState->getActiveModifiers(); + if (pressed) { + LOG((CLOG_DEBUG1 "event: button press button=%d", button)); + if (button != kButtonNone) { + sendEvent(getButtonDownEvent(), + CButtonInfo::alloc(button, mask)); + } + } + else { + LOG((CLOG_DEBUG1 "event: button release button=%d", button)); + if (button != kButtonNone) { + sendEvent(getButtonUpEvent(), + CButtonInfo::alloc(button, mask)); + } + } + } + + return true; +} + +// here's how mouse movements are sent across the network to a client: +// 1. synergy checks the mouse position on server screen +// 2. records the delta (current x,y minus last x,y) +// 3. records the current x,y as "last" (so we can calc delta next time) +// 4. on the server, puts the cursor back to the center of the screen +// - remember the cursor is hidden on the server at this point +// - this actually records the current x,y as "last" a second time (it seems) +// 5. sends the delta movement to the client (could be +1,+1 or -1,+4 for example) +bool +CMSWindowsScreen::onMouseMove(SInt32 mx, SInt32 my) +{ + // compute motion delta (relative to the last known + // mouse position) + SInt32 x = mx - m_xCursor; + SInt32 y = my - m_yCursor; + + LOG((CLOG_DEBUG3 + "mouse move - motion delta: %+d=(%+d - %+d),%+d=(%+d - %+d)", + x, mx, m_xCursor, y, my, m_yCursor)); + + // ignore if the mouse didn't move or if message posted prior + // to last mark change. + if (ignore() || (x == 0 && y == 0)) { + return true; + } + + // save position to compute delta of next motion + saveMousePosition(mx, my); + + if (m_isOnScreen) { + + // motion on primary screen + sendEvent( + getMotionOnPrimaryEvent(), + CMotionInfo::alloc(m_xCursor, m_yCursor)); + } + else + { + // the motion is on the secondary screen, so we warp mouse back to + // center on the server screen. if we don't do this, then the mouse + // will always try to return to the original entry point on the + // secondary screen. + LOG((CLOG_DEBUG5 "warping server cursor to center: %+d,%+d", m_xCenter, m_yCenter)); + warpCursorNoFlush(m_xCenter, m_yCenter); + + // examine the motion. if it's about the distance + // from the center of the screen to an edge then + // it's probably a bogus motion that we want to + // ignore (see warpCursorNoFlush() for a further + // description). + static SInt32 bogusZoneSize = 10; + if (-x + bogusZoneSize > m_xCenter - m_x || + x + bogusZoneSize > m_x + m_w - m_xCenter || + -y + bogusZoneSize > m_yCenter - m_y || + y + bogusZoneSize > m_y + m_h - m_yCenter) { + + LOG((CLOG_DEBUG "dropped bogus delta motion: %+d,%+d", x, y)); + } + else { + // send motion + sendEvent(getMotionOnSecondaryEvent(), CMotionInfo::alloc(x, y)); + } + } + + return true; +} + +bool +CMSWindowsScreen::onMouseWheel(SInt32 xDelta, SInt32 yDelta) +{ + // ignore message if posted prior to last mark change + if (!ignore()) { + LOG((CLOG_DEBUG1 "event: button wheel delta=%+d,%+d", xDelta, yDelta)); + sendEvent(getWheelEvent(), CWheelInfo::alloc(xDelta, yDelta)); + } + return true; +} + +bool +CMSWindowsScreen::onScreensaver(bool activated) +{ + // ignore this message if there are any other screen saver + // messages already in the queue. this is important because + // our checkStarted() function has a deliberate delay, so it + // can't respond to events at full CPU speed and will fall + // behind if a lot of screen saver events are generated. + // that can easily happen because windows will continually + // send SC_SCREENSAVE until the screen saver starts, even if + // the screen saver is disabled! + MSG msg; + if (PeekMessage(&msg, NULL, SYNERGY_MSG_SCREEN_SAVER, + SYNERGY_MSG_SCREEN_SAVER, PM_NOREMOVE)) { + return true; + } + + if (activated) { + if (!m_screensaverActive && + m_screensaver->checkStarted(SYNERGY_MSG_SCREEN_SAVER, FALSE, 0)) { + m_screensaverActive = true; + sendEvent(getScreensaverActivatedEvent()); + + // enable display power down + CArchMiscWindows::removeBusyState(CArchMiscWindows::kDISPLAY); + } + } + else { + if (m_screensaverActive) { + m_screensaverActive = false; + sendEvent(getScreensaverDeactivatedEvent()); + + // disable display power down + CArchMiscWindows::addBusyState(CArchMiscWindows::kDISPLAY); + } + } + + return true; +} + +bool +CMSWindowsScreen::onDisplayChange() +{ + // screen resolution may have changed. save old shape. + SInt32 xOld = m_x, yOld = m_y, wOld = m_w, hOld = m_h; + + // update shape + updateScreenShape(); + + // do nothing if resolution hasn't changed + if (xOld != m_x || yOld != m_y || wOld != m_w || hOld != m_h) { + if (m_isPrimary) { + // warp mouse to center if off screen + if (!m_isOnScreen) { + + LOG((CLOG_DEBUG1 "warping cursor to center: %+d, %+d", m_xCenter, m_yCenter)); + warpCursor(m_xCenter, m_yCenter); + } + + // tell hook about resize if on screen + else { + m_hookLibraryLoader.m_setZone(m_x, m_y, m_w, m_h, getJumpZoneSize()); + } + } + + // send new screen info + sendEvent(getShapeChangedEvent()); + + LOG((CLOG_DEBUG "screen shape: %d,%d %dx%d %s", m_x, m_y, m_w, m_h, m_multimon ? "(multi-monitor)" : "")); + } + + return true; +} + +bool +CMSWindowsScreen::onClipboardChange() +{ + // now notify client that somebody changed the clipboard (unless + // we're the owner). + if (!CMSWindowsClipboard::isOwnedBySynergy()) { + if (m_ownClipboard) { + LOG((CLOG_DEBUG "clipboard changed: lost ownership")); + m_ownClipboard = false; + sendClipboardEvent(getClipboardGrabbedEvent(), kClipboardClipboard); + sendClipboardEvent(getClipboardGrabbedEvent(), kClipboardSelection); + } + } + else if (!m_ownClipboard) { + LOG((CLOG_DEBUG "clipboard changed: synergy owned")); + m_ownClipboard = true; + } + + return true; +} + +void +CMSWindowsScreen::warpCursorNoFlush(SInt32 x, SInt32 y) +{ + // send an event that we can recognize before the mouse warp + PostThreadMessage(GetCurrentThreadId(), SYNERGY_MSG_PRE_WARP, x, y); + + // warp mouse. hopefully this inserts a mouse motion event + // between the previous message and the following message. + SetCursorPos(x, y); + + // check to see if the mouse pos was set correctly + POINT cursorPos; + GetCursorPos(&cursorPos); + + if ((cursorPos.x != x) && (cursorPos.y != y)) { + LOG((CLOG_DEBUG "SetCursorPos did not work; using fakeMouseMove instead")); + + // when at Vista/7 login screen, SetCursorPos does not work (which could be + // an MS security feature). instead we can use fakeMouseMove, which calls + // mouse_event. + // IMPORTANT: as of implementing this function, it has an annoying side + // effect; instead of the mouse returning to the correct exit point, it + // returns to the center of the screen. this could have something to do with + // the center screen warping technique used (see comments for onMouseMove + // definition). + fakeMouseMove(x, y); + } + + // yield the CPU. there's a race condition when warping: + // a hardware mouse event occurs + // the mouse hook is not called because that process doesn't have the CPU + // we send PRE_WARP, SetCursorPos(), send POST_WARP + // we process all of those events and update m_x, m_y + // we finish our time slice + // the hook is called + // the hook sends us a mouse event from the pre-warp position + // we get the CPU + // we compute a bogus warp + // we need the hook to process all mouse events that occur + // before we warp before we do the warp but i'm not sure how + // to guarantee that. yielding the CPU here may reduce the + // chance of undesired behavior. we'll also check for very + // large motions that look suspiciously like about half width + // or height of the screen. + ARCH->sleep(0.0); + + // send an event that we can recognize after the mouse warp + PostThreadMessage(GetCurrentThreadId(), SYNERGY_MSG_POST_WARP, 0, 0); +} + +void +CMSWindowsScreen::nextMark() +{ + // next mark + ++m_mark; + + // mark point in message queue where the mark was changed + PostThreadMessage(GetCurrentThreadId(), SYNERGY_MSG_MARK, m_mark, 0); +} + +bool +CMSWindowsScreen::ignore() const +{ + return (m_mark != m_markReceived); +} + +void +CMSWindowsScreen::updateScreenShape() +{ + // get shape + m_x = GetSystemMetrics(SM_XVIRTUALSCREEN); + m_y = GetSystemMetrics(SM_YVIRTUALSCREEN); + m_w = GetSystemMetrics(SM_CXVIRTUALSCREEN); + m_h = GetSystemMetrics(SM_CYVIRTUALSCREEN); + + // get center for cursor + m_xCenter = GetSystemMetrics(SM_CXSCREEN) >> 1; + m_yCenter = GetSystemMetrics(SM_CYSCREEN) >> 1; + + // check for multiple monitors + m_multimon = (m_w != GetSystemMetrics(SM_CXSCREEN) || + m_h != GetSystemMetrics(SM_CYSCREEN)); + + // tell the desks + m_desks->setShape(m_x, m_y, m_w, m_h, m_xCenter, m_yCenter, m_multimon); +} + +void +CMSWindowsScreen::handleFixes(const CEvent&, void*) +{ + // fix clipboard chain + fixClipboardViewer(); + + // update keys if keyboard layouts have changed + if (m_keyState->didGroupsChange()) { + updateKeys(); + } +} + +void +CMSWindowsScreen::fixClipboardViewer() +{ + // XXX -- disable this code for now. somehow it can cause an infinite + // recursion in the WM_DRAWCLIPBOARD handler. either we're sending + // the message to our own window or some window farther down the chain + // forwards the message to our window or a window farther up the chain. + // i'm not sure how that could happen. the m_nextClipboardWindow = NULL + // was not in the code that infinite loops and may fix the bug but i + // doubt it. +/* + ChangeClipboardChain(m_window, m_nextClipboardWindow); + m_nextClipboardWindow = NULL; + m_nextClipboardWindow = SetClipboardViewer(m_window); +*/ +} + +void +CMSWindowsScreen::enableSpecialKeys(bool enable) const +{ + // enable/disable ctrl+alt+del, alt+tab, etc on win95 family. + // since the win95 family doesn't support low-level hooks, we + // use this undocumented feature to suppress normal handling + // of certain key combinations. + if (m_is95Family) { + DWORD dummy = 0; + SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, + enable ? FALSE : TRUE, &dummy, 0); + } +} + +ButtonID +CMSWindowsScreen::mapButtonFromEvent(WPARAM msg, LPARAM button) const +{ + switch (msg) { + case WM_LBUTTONDOWN: + case WM_LBUTTONDBLCLK: + case WM_LBUTTONUP: + case WM_NCLBUTTONDOWN: + case WM_NCLBUTTONDBLCLK: + case WM_NCLBUTTONUP: + return kButtonLeft; + + case WM_MBUTTONDOWN: + case WM_MBUTTONDBLCLK: + case WM_MBUTTONUP: + case WM_NCMBUTTONDOWN: + case WM_NCMBUTTONDBLCLK: + case WM_NCMBUTTONUP: + return kButtonMiddle; + + case WM_RBUTTONDOWN: + case WM_RBUTTONDBLCLK: + case WM_RBUTTONUP: + case WM_NCRBUTTONDOWN: + case WM_NCRBUTTONDBLCLK: + case WM_NCRBUTTONUP: + return kButtonRight; + + case WM_XBUTTONDOWN: + case WM_XBUTTONDBLCLK: + case WM_XBUTTONUP: + case WM_NCXBUTTONDOWN: + case WM_NCXBUTTONDBLCLK: + case WM_NCXBUTTONUP: + switch (button) { + case XBUTTON1: + if (GetSystemMetrics(SM_CMOUSEBUTTONS) >= 4) { + return kButtonExtra0 + 0; + } + break; + + case XBUTTON2: + if (GetSystemMetrics(SM_CMOUSEBUTTONS) >= 5) { + return kButtonExtra0 + 1; + } + break; + } + return kButtonNone; + + default: + return kButtonNone; + } +} + +bool +CMSWindowsScreen::mapPressFromEvent(WPARAM msg, LPARAM) const +{ + switch (msg) { + case WM_LBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_XBUTTONDOWN: + case WM_LBUTTONDBLCLK: + case WM_MBUTTONDBLCLK: + case WM_RBUTTONDBLCLK: + case WM_XBUTTONDBLCLK: + case WM_NCLBUTTONDOWN: + case WM_NCMBUTTONDOWN: + case WM_NCRBUTTONDOWN: + case WM_NCXBUTTONDOWN: + case WM_NCLBUTTONDBLCLK: + case WM_NCMBUTTONDBLCLK: + case WM_NCRBUTTONDBLCLK: + case WM_NCXBUTTONDBLCLK: + return true; + + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + case WM_XBUTTONUP: + case WM_NCLBUTTONUP: + case WM_NCMBUTTONUP: + case WM_NCRBUTTONUP: + case WM_NCXBUTTONUP: + return false; + + default: + return false; + } +} + +void +CMSWindowsScreen::updateKeysCB(void*) +{ + // record which keys we think are down + bool down[IKeyState::kNumButtons]; + bool sendFixes = (isPrimary() && !m_isOnScreen); + if (sendFixes) { + for (KeyButton i = 0; i < IKeyState::kNumButtons; ++i) { + down[i] = m_keyState->isKeyDown(i); + } + } + + // update layouts if necessary + if (m_keyState->didGroupsChange()) { + CPlatformScreen::updateKeyMap(); + } + + // now update the keyboard state + CPlatformScreen::updateKeyState(); + + // now see which keys we thought were down but now think are up. + // send key releases for these keys to the active client. + if (sendFixes) { + KeyModifierMask mask = pollActiveModifiers(); + for (KeyButton i = 0; i < IKeyState::kNumButtons; ++i) { + if (down[i] && !m_keyState->isKeyDown(i)) { + m_keyState->sendKeyEvent(getEventTarget(), + false, false, kKeyNone, mask, 1, i); + } + } + } +} + +void +CMSWindowsScreen::forceShowCursor() +{ + // check for mouse + m_hasMouse = (GetSystemMetrics(SM_MOUSEPRESENT) != 0); + + // decide if we should show the mouse + bool showMouse = (!m_hasMouse && !m_isPrimary && m_isOnScreen); + + // show/hide the mouse + if (showMouse != m_showingMouse) { + if (showMouse) { + m_oldMouseKeys.cbSize = sizeof(m_oldMouseKeys); + m_gotOldMouseKeys = + (SystemParametersInfo(SPI_GETMOUSEKEYS, + m_oldMouseKeys.cbSize, &m_oldMouseKeys, 0) != 0); + if (m_gotOldMouseKeys) { + m_mouseKeys = m_oldMouseKeys; + m_showingMouse = true; + updateForceShowCursor(); + } + } + else { + if (m_gotOldMouseKeys) { + SystemParametersInfo(SPI_SETMOUSEKEYS, + m_oldMouseKeys.cbSize, + &m_oldMouseKeys, SPIF_SENDCHANGE); + m_showingMouse = false; + } + } + } +} + +void +CMSWindowsScreen::updateForceShowCursor() +{ + DWORD oldFlags = m_mouseKeys.dwFlags; + + // turn on MouseKeys + m_mouseKeys.dwFlags = MKF_AVAILABLE | MKF_MOUSEKEYSON; + + // make sure MouseKeys is active in whatever state the NumLock is + // not currently in. + if ((m_keyState->getActiveModifiers() & KeyModifierNumLock) != 0) { + m_mouseKeys.dwFlags |= MKF_REPLACENUMBERS; + } + + // update MouseKeys + if (oldFlags != m_mouseKeys.dwFlags) { + SystemParametersInfo(SPI_SETMOUSEKEYS, + m_mouseKeys.cbSize, &m_mouseKeys, SPIF_SENDCHANGE); + } +} + +LRESULT CALLBACK +CMSWindowsScreen::wndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + assert(s_screen != NULL); + + LRESULT result = 0; + if (!s_screen->onEvent(hwnd, msg, wParam, lParam, &result)) { + result = DefWindowProc(hwnd, msg, wParam, lParam); + } + + return result; +} + +// +// CMSWindowsScreen::CHotKeyItem +// + +CMSWindowsScreen::CHotKeyItem::CHotKeyItem(UINT keycode, UINT mask) : + m_keycode(keycode), + m_mask(mask) +{ + // do nothing +} + +UINT +CMSWindowsScreen::CHotKeyItem::getVirtualKey() const +{ + return m_keycode; +} + +bool +CMSWindowsScreen::CHotKeyItem::operator<(const CHotKeyItem& x) const +{ + return (m_keycode < x.m_keycode || + (m_keycode == x.m_keycode && m_mask < x.m_mask)); +} diff --git a/src/lib/platform/CMSWindowsScreen.h b/src/lib/platform/CMSWindowsScreen.h new file mode 100644 index 00000000..edfd89f7 --- /dev/null +++ b/src/lib/platform/CMSWindowsScreen.h @@ -0,0 +1,332 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSSCREEN_H +#define CMSWINDOWSSCREEN_H + +#include "CPlatformScreen.h" +#include "CSynergyHook.h" +#include "CCondVar.h" +#include "CMutex.h" +#include "CString.h" +#include "CMSWindowsHookLibraryLoader.h" +#include "CGameDevice.h" +#include "CMSWindowsXInput.h" +#include "CEventGameDevice.h" + +#define WIN32_LEAN_AND_MEAN +#include + +class CEventQueueTimer; +class CMSWindowsDesks; +class CMSWindowsKeyState; +class CMSWindowsScreenSaver; +class CThread; + +//! Implementation of IPlatformScreen for Microsoft Windows +class CMSWindowsScreen : public CPlatformScreen { +public: + CMSWindowsScreen(bool isPrimary, bool noHooks, const CGameDeviceInfo &gameDevice); + virtual ~CMSWindowsScreen(); + + //! @name manipulators + //@{ + + //! Initialize + /*! + Saves the application's HINSTANCE. This \b must be called by + WinMain with the HINSTANCE it was passed. + */ + static void init(HINSTANCE); + + //@} + //! @name accessors + //@{ + + //! Get instance + /*! + Returns the application instance handle passed to init(). + */ + static HINSTANCE getWindowInstance(); + + //@} + + // IScreen overrides + virtual void* getEventTarget() const; + virtual bool getClipboard(ClipboardID id, IClipboard*) const; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const; + virtual void getCursorPos(SInt32& x, SInt32& y) const; + + // IPrimaryScreen overrides + virtual void reconfigure(UInt32 activeSides); + virtual void warpCursor(SInt32 x, SInt32 y); + virtual UInt32 registerHotKey(KeyID key, + KeyModifierMask mask); + virtual void unregisterHotKey(UInt32 id); + virtual void fakeInputBegin(); + virtual void fakeInputEnd(); + virtual SInt32 getJumpZoneSize() const; + virtual bool isAnyMouseButtonDown() const; + virtual void getCursorCenter(SInt32& x, SInt32& y) const; + virtual void gameDeviceTimingResp(UInt16 freq); + virtual void gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2); + + // ISecondaryScreen overrides + virtual void fakeMouseButton(ButtonID id, bool press); + virtual void fakeMouseMove(SInt32 x, SInt32 y) const; + virtual void fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const; + virtual void fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const; + virtual void fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const; + virtual void fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const; + virtual void fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const; + virtual void queueGameDeviceTimingReq() const; + + // IKeyState overrides + virtual void updateKeys(); + virtual void fakeKeyDown(KeyID id, KeyModifierMask mask, + KeyButton button); + virtual bool fakeKeyRepeat(KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton button); + virtual bool fakeKeyUp(KeyButton button); + virtual void fakeAllKeysUp(); + + // IPlatformScreen overrides + virtual void enable(); + virtual void disable(); + virtual void enter(); + virtual bool leave(); + virtual bool setClipboard(ClipboardID, const IClipboard*); + virtual void checkClipboards(); + virtual void openScreensaver(bool notify); + virtual void closeScreensaver(); + virtual void screensaver(bool activate); + virtual void resetOptions(); + virtual void setOptions(const COptionsList& options); + virtual void setSequenceNumber(UInt32); + virtual bool isPrimary() const; + +protected: + // IPlatformScreen overrides + virtual void handleSystemEvent(const CEvent&, void*); + virtual void updateButtons(); + virtual IKeyState* getKeyState() const; + +private: + // initialization and shutdown operations + HINSTANCE openHookLibrary(const char* name); + void closeHookLibrary(HINSTANCE hookLibrary) const; + HCURSOR createBlankCursor() const; + void destroyCursor(HCURSOR cursor) const; + ATOM createWindowClass() const; + ATOM createDeskWindowClass(bool isPrimary) const; + void destroyClass(ATOM windowClass) const; + HWND createWindow(ATOM windowClass, const char* name) const; + void destroyWindow(HWND) const; + + // convenience function to send events +public: // HACK + void sendEvent(CEvent::Type type, void* = NULL); +private: // HACK + void sendClipboardEvent(CEvent::Type type, ClipboardID id); + + // handle message before it gets dispatched. returns true iff + // the message should not be dispatched. + bool onPreDispatch(HWND, UINT, WPARAM, LPARAM); + + // handle message before it gets dispatched. returns true iff + // the message should not be dispatched. + bool onPreDispatchPrimary(HWND, UINT, WPARAM, LPARAM); + + // handle message. returns true iff handled and optionally sets + // \c *result (which defaults to 0). + bool onEvent(HWND, UINT, WPARAM, LPARAM, LRESULT* result); + + // message handlers + bool onMark(UInt32 mark); + bool onKey(WPARAM, LPARAM); + bool onHotKey(WPARAM, LPARAM); + bool onMouseButton(WPARAM, LPARAM); + bool onMouseMove(SInt32 x, SInt32 y); + bool onMouseWheel(SInt32 xDelta, SInt32 yDelta); + bool onScreensaver(bool activated); + bool onDisplayChange(); + bool onClipboardChange(); + + // warp cursor without discarding queued events + void warpCursorNoFlush(SInt32 x, SInt32 y); + + // discard posted messages + void nextMark(); + + // test if event should be ignored + bool ignore() const; + + // update screen size cache + void updateScreenShape(); + + // fix timer callback + void handleFixes(const CEvent&, void*); + + // fix the clipboard viewer chain + void fixClipboardViewer(); + + // enable/disable special key combinations so we can catch/pass them + void enableSpecialKeys(bool) const; + + // map a button event to a button ID + ButtonID mapButtonFromEvent(WPARAM msg, LPARAM button) const; + + // map a button event to a press (true) or release (false) + bool mapPressFromEvent(WPARAM msg, LPARAM button) const; + + // job to update the key state + void updateKeysCB(void*); + + // determine whether the mouse is hidden by the system and force + // it to be displayed if user has entered this secondary screen. + void forceShowCursor(); + + // forceShowCursor uses MouseKeys to show the cursor. since we + // don't actually want MouseKeys behavior we have to make sure + // it applies when NumLock is in whatever state it's not in now. + // this method does that. + void updateForceShowCursor(); + + // our window proc + static LRESULT CALLBACK wndProc(HWND, UINT, WPARAM, LPARAM); + +private: + struct CHotKeyItem { + public: + CHotKeyItem(UINT vk, UINT modifiers); + + UINT getVirtualKey() const; + + bool operator<(const CHotKeyItem&) const; + + private: + UINT m_keycode; + UINT m_mask; + }; + typedef std::map HotKeyMap; + typedef std::vector HotKeyIDList; + typedef std::map HotKeyToIDMap; + + static HINSTANCE s_windowInstance; + + // true if screen is being used as a primary screen, false otherwise + bool m_isPrimary; + + // true if hooks are not to be installed (useful for debugging) + bool m_noHooks; + + // true if windows 95/98/me + bool m_is95Family; + + // true if mouse has entered the screen + bool m_isOnScreen; + + // our resources + ATOM m_class; + + // screen shape stuff + SInt32 m_x, m_y; + SInt32 m_w, m_h; + SInt32 m_xCenter, m_yCenter; + + // true if system appears to have multiple monitors + bool m_multimon; + + // last mouse position + SInt32 m_xCursor, m_yCursor; + + // last clipboard + UInt32 m_sequenceNumber; + + // used to discard queued messages that are no longer needed + UInt32 m_mark; + UInt32 m_markReceived; + + // the main loop's thread id + DWORD m_threadID; + + // timer for periodically checking stuff that requires polling + CEventQueueTimer* m_fixTimer; + + // the keyboard layout to use when off primary screen + HKL m_keyLayout; + + // screen saver stuff + CMSWindowsScreenSaver* m_screensaver; + bool m_screensaverNotify; + bool m_screensaverActive; + + // clipboard stuff. our window is used mainly as a clipboard + // owner and as a link in the clipboard viewer chain. + HWND m_window; + HWND m_nextClipboardWindow; + bool m_ownClipboard; + + // one desk per desktop and a cond var to communicate with it + CMSWindowsDesks* m_desks; + + // hook library stuff + HINSTANCE m_hookLibrary; + + // keyboard stuff + CMSWindowsKeyState* m_keyState; + + // hot key stuff + HotKeyMap m_hotKeys; + HotKeyIDList m_oldHotKeyIDs; + HotKeyToIDMap m_hotKeyToIDMap; + + // map of button state + bool m_buttons[1 + kButtonExtra0 + 1]; + + // the system shows the mouse cursor when an internal display count + // is >= 0. this count is maintained per application but there's + // apparently a system wide count added to the application's count. + // this system count is 0 if there's a mouse attached to the system + // and -1 otherwise. the MouseKeys accessibility feature can modify + // this system count by making the system appear to have a mouse. + // + // m_hasMouse is true iff there's a mouse attached to the system or + // MouseKeys is simulating one. we track this so we can force the + // cursor to be displayed when the user has entered this screen. + // m_showingMouse is true when we're doing that. + bool m_hasMouse; + bool m_showingMouse; + bool m_gotOldMouseKeys; + MOUSEKEYS m_mouseKeys; + MOUSEKEYS m_oldMouseKeys; + + // loads synrgyhk.dll + CMSWindowsHookLibraryLoader + m_hookLibraryLoader; + + const CGameDeviceInfo& m_gameDeviceInfo; + CGameDevice* m_gameDevice; + + static CMSWindowsScreen* s_screen; + + // save last position of mouse to compute next delta movement + void saveMousePosition(SInt32 x, SInt32 y); +}; + +#endif diff --git a/src/lib/platform/CMSWindowsScreenSaver.cpp b/src/lib/platform/CMSWindowsScreenSaver.cpp new file mode 100644 index 00000000..4f2a488f --- /dev/null +++ b/src/lib/platform/CMSWindowsScreenSaver.cpp @@ -0,0 +1,501 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsScreenSaver.h" +#include "CMSWindowsScreen.h" +#include "CThread.h" +#include "CLog.h" +#include "TMethodJob.h" +#include "CArch.h" +#include "CArchMiscWindows.h" +#include +#include + +#if !defined(SPI_GETSCREENSAVERRUNNING) +#define SPI_GETSCREENSAVERRUNNING 114 +#endif + +static const TCHAR* g_isSecureNT = "ScreenSaverIsSecure"; +static const TCHAR* g_isSecure9x = "ScreenSaveUsePassword"; +static const TCHAR* const g_pathScreenSaverIsSecure[] = { + "Control Panel", + "Desktop", + NULL +}; + +// +// CMSWindowsScreenSaver +// + +CMSWindowsScreenSaver::CMSWindowsScreenSaver() : + m_wasSecure(false), + m_wasSecureAnInt(false), + m_process(NULL), + m_watch(NULL), + m_threadID(0), + m_active(false) +{ + // detect OS + m_is95Family = false; + m_is95 = false; + m_isNT = false; + OSVERSIONINFO info; + info.dwOSVersionInfoSize = sizeof(info); + if (GetVersionEx(&info)) { + m_is95Family = (info.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS); + if (info.dwPlatformId == VER_PLATFORM_WIN32_NT && + info.dwMajorVersion <= 4) { + m_isNT = true; + } + else if (info.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS && + info.dwMajorVersion == 4 && + info.dwMinorVersion == 0) { + m_is95 = true; + } + } + + // check if screen saver is enabled + SystemParametersInfo(SPI_GETSCREENSAVEACTIVE, 0, &m_wasEnabled, 0); +} + +CMSWindowsScreenSaver::~CMSWindowsScreenSaver() +{ + unwatchProcess(); +} + +bool +CMSWindowsScreenSaver::checkStarted(UINT msg, WPARAM wParam, LPARAM lParam) +{ + // if already started then say it didn't just start + if (m_active) { + return false; + } + + // screen saver may have started. look for it and get + // the process. if we can't find it then assume it + // didn't really start. we wait a moment before + // looking to give the screen saver a chance to start. + // this shouldn't be a problem since we only get here + // if the screen saver wants to kick in, meaning that + // the system is idle or the user deliberately started + // the screen saver. + Sleep(250); + + // set parameters common to all screen saver handling + m_threadID = GetCurrentThreadId(); + m_msg = msg; + m_wParam = wParam; + m_lParam = lParam; + + // we handle the screen saver differently for the windows + // 95 and nt families. + if (m_is95Family) { + // on windows 95 we wait for the screen saver process + // to terminate. get the process. + DWORD processID = findScreenSaver(); + HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, processID); + if (process == NULL) { + // didn't start + LOG((CLOG_DEBUG2 "can't open screen saver process")); + return false; + } + + // watch for the process to exit + watchProcess(process); + } + else { + // on the windows nt family we wait for the desktop to + // change until it's neither the Screen-Saver desktop + // nor a desktop we can't open (the login desktop). + // since windows will send the request-to-start-screen- + // saver message even when the screen saver is disabled + // we first check that the screen saver is indeed active + // before watching for it to stop. + if (!isActive()) { + LOG((CLOG_DEBUG2 "can't open screen saver desktop")); + return false; + } + + watchDesktop(); + } + + return true; +} + +void +CMSWindowsScreenSaver::enable() +{ + SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, m_wasEnabled, 0, 0); + + // restore password protection + if (m_wasSecure) { + setSecure(true, m_wasSecureAnInt); + } + + // restore display power down + CArchMiscWindows::removeBusyState(CArchMiscWindows::kDISPLAY); +} + +void +CMSWindowsScreenSaver::disable() +{ + SystemParametersInfo(SPI_GETSCREENSAVEACTIVE, 0, &m_wasEnabled, 0); + SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, FALSE, 0, 0); + + // disable password protected screensaver + m_wasSecure = isSecure(&m_wasSecureAnInt); + if (m_wasSecure) { + setSecure(false, m_wasSecureAnInt); + } + + // disable display power down + CArchMiscWindows::addBusyState(CArchMiscWindows::kDISPLAY); +} + +void +CMSWindowsScreenSaver::activate() +{ + // don't activate if already active + if (!isActive()) { + // activate + HWND hwnd = GetForegroundWindow(); + if (hwnd != NULL) { + PostMessage(hwnd, WM_SYSCOMMAND, SC_SCREENSAVE, 0); + } + else { + // no foreground window. pretend we got the event instead. + DefWindowProc(NULL, WM_SYSCOMMAND, SC_SCREENSAVE, 0); + } + + // restore power save when screen saver activates + CArchMiscWindows::removeBusyState(CArchMiscWindows::kDISPLAY); + } +} + +void +CMSWindowsScreenSaver::deactivate() +{ + bool killed = false; + if (!m_is95Family) { + // NT runs screen saver in another desktop + HDESK desktop = OpenDesktop("Screen-saver", 0, FALSE, + DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS); + if (desktop != NULL) { + EnumDesktopWindows(desktop, + &CMSWindowsScreenSaver::killScreenSaverFunc, + reinterpret_cast(&killed)); + CloseDesktop(desktop); + } + } + + // if above failed or wasn't tried, try the windows 95 way + if (!killed) { + // find screen saver window and close it + HWND hwnd = FindWindow("WindowsScreenSaverClass", NULL); + if (hwnd == NULL) { + // win2k may use a different class + hwnd = FindWindow("Default Screen Saver", NULL); + } + if (hwnd != NULL) { + PostMessage(hwnd, WM_CLOSE, 0, 0); + } + } + + // force timer to restart + SystemParametersInfo(SPI_GETSCREENSAVEACTIVE, 0, &m_wasEnabled, 0); + SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, + !m_wasEnabled, 0, SPIF_SENDWININICHANGE); + SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, + m_wasEnabled, 0, SPIF_SENDWININICHANGE); + + // disable display power down + CArchMiscWindows::removeBusyState(CArchMiscWindows::kDISPLAY); +} + +bool +CMSWindowsScreenSaver::isActive() const +{ + if (m_is95) { + return (FindWindow("WindowsScreenSaverClass", NULL) != NULL); + } + else if (m_isNT) { + // screen saver runs on a separate desktop + HDESK desktop = OpenDesktop("Screen-saver", 0, FALSE, MAXIMUM_ALLOWED); + if (desktop == NULL && GetLastError() != ERROR_ACCESS_DENIED) { + // desktop doesn't exist so screen saver is not running + return false; + } + + // desktop exists. this should indicate that the screen saver + // is running but an OS bug can cause a valid handle to be + // returned even if the screen saver isn't running (Q230117). + // we'll try to enumerate the windows on the desktop and, if + // there are any, we assume the screen saver is running. (note + // that if we don't have permission to enumerate then we'll + // assume that the screen saver is not running.) that'd be + // easy enough except there's another OS bug (Q198590) that can + // cause EnumDesktopWindows() to enumerate the windows of + // another desktop if the requested desktop has no windows. to + // work around that we have to verify that the enumerated + // windows are, in fact, on the expected desktop. + CFindScreenSaverInfo info; + info.m_desktop = desktop; + info.m_window = NULL; + EnumDesktopWindows(desktop, + &CMSWindowsScreenSaver::findScreenSaverFunc, + reinterpret_cast(&info)); + + // done with desktop + CloseDesktop(desktop); + + // screen saver is running if a window was found + return (info.m_window != NULL); + } + else { + BOOL running; + SystemParametersInfo(SPI_GETSCREENSAVERRUNNING, 0, &running, 0); + return (running != FALSE); + } +} + +BOOL CALLBACK +CMSWindowsScreenSaver::findScreenSaverFunc(HWND hwnd, LPARAM arg) +{ + CFindScreenSaverInfo* info = reinterpret_cast(arg); + + if (info->m_desktop != NULL) { + DWORD threadID = GetWindowThreadProcessId(hwnd, NULL); + HDESK desktop = GetThreadDesktop(threadID); + if (desktop != NULL && desktop != info->m_desktop) { + // stop enumerating -- wrong desktop + return FALSE; + } + } + + // found a window + info->m_window = hwnd; + + // don't need to enumerate further + return FALSE; +} + +BOOL CALLBACK +CMSWindowsScreenSaver::killScreenSaverFunc(HWND hwnd, LPARAM arg) +{ + if (IsWindowVisible(hwnd)) { + HINSTANCE instance = (HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE); + if (instance != CMSWindowsScreen::getWindowInstance()) { + PostMessage(hwnd, WM_CLOSE, 0, 0); + *reinterpret_cast(arg) = true; + } + } + return TRUE; +} + +DWORD +CMSWindowsScreenSaver::findScreenSaver() +{ + // try windows 95 way + HWND hwnd = FindWindow("WindowsScreenSaverClass", NULL); + + // get process ID of process that owns the window, if found + if (hwnd != NULL) { + DWORD processID; + GetWindowThreadProcessId(hwnd, &processID); + return processID; + } + + // not found + return 0; +} + +void +CMSWindowsScreenSaver::watchDesktop() +{ + // stop watching previous process/desktop + unwatchProcess(); + + // watch desktop in another thread + LOG((CLOG_DEBUG "watching screen saver desktop")); + m_active = true; + m_watch = new CThread(new TMethodJob(this, + &CMSWindowsScreenSaver::watchDesktopThread)); +} + +void +CMSWindowsScreenSaver::watchProcess(HANDLE process) +{ + // stop watching previous process/desktop + unwatchProcess(); + + // watch new process in another thread + if (process != NULL) { + LOG((CLOG_DEBUG "watching screen saver process")); + m_process = process; + m_active = true; + m_watch = new CThread(new TMethodJob(this, + &CMSWindowsScreenSaver::watchProcessThread)); + } +} + +void +CMSWindowsScreenSaver::unwatchProcess() +{ + if (m_watch != NULL) { + LOG((CLOG_DEBUG "stopped watching screen saver process/desktop")); + m_watch->cancel(); + m_watch->wait(); + delete m_watch; + m_watch = NULL; + m_active = false; + } + if (m_process != NULL) { + CloseHandle(m_process); + m_process = NULL; + } +} + +void +CMSWindowsScreenSaver::watchDesktopThread(void*) +{ + DWORD reserved = 0; + TCHAR* name = NULL; + + for (;;) { + // wait a bit + ARCH->sleep(0.2); + + if (m_isNT) { + // get current desktop + HDESK desk = OpenInputDesktop(0, FALSE, GENERIC_READ); + if (desk == NULL) { + // can't open desktop so keep waiting + continue; + } + + // get current desktop name length + DWORD size; + GetUserObjectInformation(desk, UOI_NAME, NULL, 0, &size); + + // allocate more space for the name, if necessary + if (size > reserved) { + reserved = size; + name = (TCHAR*)alloca(reserved + sizeof(TCHAR)); + } + + // get current desktop name + GetUserObjectInformation(desk, UOI_NAME, name, size, &size); + CloseDesktop(desk); + + // compare name to screen saver desktop name + if (_tcsicmp(name, TEXT("Screen-saver")) == 0) { + // still the screen saver desktop so keep waiting + continue; + } + } + else { + // 2000/XP have a sane way to detect a runnin screensaver. + BOOL running; + SystemParametersInfo(SPI_GETSCREENSAVERRUNNING, 0, &running, 0); + if (running) { + continue; + } + } + + // send screen saver deactivation message + m_active = false; + PostThreadMessage(m_threadID, m_msg, m_wParam, m_lParam); + return; + } +} + +void +CMSWindowsScreenSaver::watchProcessThread(void*) +{ + for (;;) { + CThread::testCancel(); + if (WaitForSingleObject(m_process, 50) == WAIT_OBJECT_0) { + // process terminated + LOG((CLOG_DEBUG "screen saver died")); + + // send screen saver deactivation message + m_active = false; + PostThreadMessage(m_threadID, m_msg, m_wParam, m_lParam); + return; + } + } +} + +void +CMSWindowsScreenSaver::setSecure(bool secure, bool saveSecureAsInt) +{ + HKEY hkey = + CArchMiscWindows::addKey(HKEY_CURRENT_USER, g_pathScreenSaverIsSecure); + if (hkey == NULL) { + return; + } + + const TCHAR* isSecure = m_is95Family ? g_isSecure9x : g_isSecureNT; + if (saveSecureAsInt) { + CArchMiscWindows::setValue(hkey, isSecure, secure ? 1 : 0); + } + else { + CArchMiscWindows::setValue(hkey, isSecure, secure ? "1" : "0"); + } + + CArchMiscWindows::closeKey(hkey); +} + +bool +CMSWindowsScreenSaver::isSecure(bool* wasSecureFlagAnInt) const +{ + // get the password protection setting key + HKEY hkey = + CArchMiscWindows::openKey(HKEY_CURRENT_USER, g_pathScreenSaverIsSecure); + if (hkey == NULL) { + return false; + } + + // get the value. the value may be an int or a string, depending + // on the version of windows. + bool result; + const TCHAR* isSecure = m_is95Family ? g_isSecure9x : g_isSecureNT; + switch (CArchMiscWindows::typeOfValue(hkey, isSecure)) { + default: + result = false; + break; + + case CArchMiscWindows::kUINT: { + DWORD value = + CArchMiscWindows::readValueInt(hkey, isSecure); + *wasSecureFlagAnInt = true; + result = (value != 0); + break; + } + + case CArchMiscWindows::kSTRING: { + std::string value = + CArchMiscWindows::readValueString(hkey, isSecure); + *wasSecureFlagAnInt = false; + result = (value != "0"); + break; + } + } + + CArchMiscWindows::closeKey(hkey); + return result; +} diff --git a/src/lib/platform/CMSWindowsScreenSaver.h b/src/lib/platform/CMSWindowsScreenSaver.h new file mode 100644 index 00000000..741108d6 --- /dev/null +++ b/src/lib/platform/CMSWindowsScreenSaver.h @@ -0,0 +1,95 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSSCREENSAVER_H +#define CMSWINDOWSSCREENSAVER_H + +#include "IScreenSaver.h" +#include "CString.h" +#define WIN32_LEAN_AND_MEAN +#include + +class CThread; + +//! Microsoft windows screen saver implementation +class CMSWindowsScreenSaver : public IScreenSaver { +public: + CMSWindowsScreenSaver(); + virtual ~CMSWindowsScreenSaver(); + + //! @name manipulators + //@{ + + //! Check if screen saver started + /*! + Check if the screen saver really started. Returns false if it + hasn't, true otherwise. When the screen saver stops, \c msg will + be posted to the current thread's message queue with the given + parameters. + */ + bool checkStarted(UINT msg, WPARAM, LPARAM); + + //@} + + // IScreenSaver overrides + virtual void enable(); + virtual void disable(); + virtual void activate(); + virtual void deactivate(); + virtual bool isActive() const; + +private: + class CFindScreenSaverInfo { + public: + HDESK m_desktop; + HWND m_window; + }; + + static BOOL CALLBACK findScreenSaverFunc(HWND hwnd, LPARAM lParam); + static BOOL CALLBACK killScreenSaverFunc(HWND hwnd, LPARAM lParam); + + DWORD findScreenSaver(); + void watchDesktop(); + void watchProcess(HANDLE process); + void unwatchProcess(); + void watchDesktopThread(void*); + void watchProcessThread(void*); + + void setSecure(bool secure, bool saveSecureAsInt); + bool isSecure(bool* wasSecureAnInt) const; + +private: + bool m_is95Family; + bool m_is95; + bool m_isNT; + BOOL m_wasEnabled; + bool m_wasSecure; + bool m_wasSecureAnInt; + + HANDLE m_process; + CThread* m_watch; + DWORD m_threadID; + UINT m_msg; + WPARAM m_wParam; + LPARAM m_lParam; + + // checkActive state. true if the screen saver is being watched + // for deactivation (and is therefore active). + bool m_active; +}; + +#endif diff --git a/src/lib/platform/CMSWindowsUtil.cpp b/src/lib/platform/CMSWindowsUtil.cpp new file mode 100644 index 00000000..2471c13c --- /dev/null +++ b/src/lib/platform/CMSWindowsUtil.cpp @@ -0,0 +1,78 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CMSWindowsUtil.h" +#include "CStringUtil.h" +#include + +// +// CMSWindowsUtil +// + +CString +CMSWindowsUtil::getString(HINSTANCE instance, DWORD id) +{ + char buffer[1024]; + int size = static_cast(sizeof(buffer) / sizeof(buffer[0])); + char* msg = buffer; + + // load string + int n = LoadString(instance, id, msg, size); + msg[n] = '\0'; + if (n < size) { + return msg; + } + + // not enough buffer space. keep trying larger buffers until + // we get the whole string. + msg = NULL; + do { + size <<= 1; + delete[] msg; + char* msg = new char[size]; + n = LoadString(instance, id, msg, size); + } while (n == size); + msg[n] = '\0'; + + CString result(msg); + delete[] msg; + return result; +} + +CString +CMSWindowsUtil::getErrorString(HINSTANCE hinstance, DWORD error, DWORD id) +{ + char* buffer; + if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_IGNORE_INSERTS | + FORMAT_MESSAGE_FROM_SYSTEM, + 0, + error, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR)&buffer, + 0, + NULL) == 0) { + CString errorString = CStringUtil::print("%d", error); + return CStringUtil::format(getString(hinstance, id).c_str(), + errorString.c_str()); + } + else { + CString result(buffer); + LocalFree(buffer); + return result; + } +} diff --git a/src/lib/platform/CMSWindowsUtil.h b/src/lib/platform/CMSWindowsUtil.h new file mode 100644 index 00000000..7f4e9291 --- /dev/null +++ b/src/lib/platform/CMSWindowsUtil.h @@ -0,0 +1,41 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMSWINDOWSUTIL_H +#define CMSWINDOWSUTIL_H + +#include "CString.h" +#define WINDOWS_LEAN_AND_MEAN +#include + +class CMSWindowsUtil { +public: + //! Get message string + /*! + Gets a string for \p id from the string table of \p instance. + */ + static CString getString(HINSTANCE instance, DWORD id); + + //! Get error string + /*! + Gets a system error message for \p error. If the error cannot be + found return the string for \p id, replacing ${1} with \p error. + */ + static CString getErrorString(HINSTANCE, DWORD error, DWORD id); +}; + +#endif diff --git a/src/lib/platform/CMSWindowsXInput.cpp b/src/lib/platform/CMSWindowsXInput.cpp new file mode 100644 index 00000000..b2d2051c --- /dev/null +++ b/src/lib/platform/CMSWindowsXInput.cpp @@ -0,0 +1,362 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 notsee . + */ + +#include "CMSWindowsXInput.h" +#include "XScreen.h" +#include "CThread.h" +#include "TMethodJob.h" +#include "CLog.h" +#include "XInputHook.h" +#include "CMSWindowsScreen.h" + +#include "XInput.h" + +typedef DWORD (WINAPI *XInputGetStateFunc)(DWORD, XINPUT_STATE*); +typedef DWORD (WINAPI *XInputSetStateFunc)(DWORD, XINPUT_VIBRATION*); + +CMSWindowsXInput::CMSWindowsXInput(CMSWindowsScreen* screen, const CGameDeviceInfo& gameDeviceInfo) : +m_screen(screen), +m_gameDeviceInfo(gameDeviceInfo), +m_xInputPollThread(NULL), +m_xInputTimingThread(NULL), +m_xInputFeedbackThread(NULL), +m_gameButtonsLast(0), +m_gameLeftTriggerLast(0), +m_gameRightTriggerLast(0), +m_gameLeftStickXLast(0), +m_gameLeftStickYLast(0), +m_gameRightStickXLast(0), +m_gameRightStickYLast(0), +m_gameLastTimingSent(0), +m_gameTimingWaiting(false), +m_gameFakeLag(0), +m_gamePollFreq(kGamePollFreqDefault), +m_gamePollFreqAdjust(0), +m_gameFakeLagMin(kGamePollFreqDefault), +m_gameTimingStarted(false), +m_gameTimingFirst(0), +m_gameFakeLagLast(0), +m_gameTimingCalibrated(false), +m_xinputModule(NULL) +{ + m_xinputModule = LoadLibrary("xinput1_3.dll"); + if (m_xinputModule == NULL) + { + throw XScreenXInputFailure("could not load xinput library"); + } + + if (screen->isPrimary()) + { + // only capture xinput on the server. + m_xInputPollThread = new CThread(new TMethodJob( + this, &CMSWindowsXInput::xInputPollThread)); + } + else + { + // check for queued timing requests on client. + m_xInputTimingThread = new CThread(new TMethodJob( + this, &CMSWindowsXInput::xInputTimingThread)); + + // check for waiting feedback state on client. + m_xInputFeedbackThread = new CThread(new TMethodJob( + this, &CMSWindowsXInput::xInputFeedbackThread)); + } +} + +CMSWindowsXInput::~CMSWindowsXInput() +{ +} + +void +CMSWindowsXInput::fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const +{ + SetXInputButtons(id, buttons); +} + +void +CMSWindowsXInput::fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const +{ + SetXInputSticks(id, x1, y1, x2, y2); +} + +void +CMSWindowsXInput::fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const +{ + SetXInputTriggers(id, t1, t2); +} + +void +CMSWindowsXInput::queueGameDeviceTimingReq() const +{ + QueueXInputTimingReq(); +} + +void +CMSWindowsXInput::gameDeviceTimingResp(UInt16 freq) +{ + if (!m_gameTimingStarted) + { + // record when timing started for calibration period. + m_gameTimingFirst = (UInt16)(ARCH->time() * 1000); + m_gameTimingStarted = true; + } + + m_gameTimingWaiting = false; + m_gameFakeLagLast = m_gameFakeLag; + m_gameFakeLag = (UInt16)((ARCH->time() - m_gameLastTimingSent) * 1000); + m_gameFakeLagRecord.push_back(m_gameFakeLag); + + if (m_gameFakeLag < m_gameFakeLagMin) + { + // record the lowest value so that the poll frequency + // is adjusted to track. + m_gameFakeLagMin = m_gameFakeLag; + } + else if (m_gameFakeLag > (m_gameFakeLagLast * 2)) + { + // if fake lag has increased significantly since the last + // timing, then we must have reached the safe minimum. + m_gameFakeLagMin = m_gameFakeLagLast; + } + + // only change poll frequency if it's a sensible value. + if (freq > kGamePollFreqMin && freq < kGamePollFreqMax) + { + m_gamePollFreq = freq; + } + + UInt16 timeSinceStart = ((UInt16)(ARCH->time() * 1000) - m_gameTimingFirst); + if (!m_gameTimingCalibrated && (timeSinceStart < kGameCalibrationPeriod)) + { + // during the calibration period, increase polling speed + // to try and find the lowest lag value. + m_gamePollFreqAdjust = 1; + LOG((CLOG_DEBUG2 "calibrating game device poll frequency, start=%d, now=%d, since=%d", + m_gameTimingFirst, (int)(ARCH->time() * 1000), timeSinceStart)); + } + else + { + // @bug - calibration seems to re-occur after a period of time, + // though, this would actually be a nice feature (but could be + // a bit risky -- could cause poor game play)... setting this + // stops calibration from happening again. + m_gameTimingCalibrated = true; + + // only adjust poll frequency if outside desired limits. + m_gamePollFreqAdjust = 0; + if (m_gameFakeLag > m_gameFakeLagMin * 3) + { + m_gamePollFreqAdjust = 1; + } + else if (m_gameFakeLag < m_gameFakeLagMin) + { + m_gamePollFreqAdjust = -1; + } + } + + LOG((CLOG_DEBUG3 "game device timing, lag=%dms, freq=%dms, adjust=%dms, min=%dms", + m_gameFakeLag, m_gamePollFreq, m_gamePollFreqAdjust, m_gameFakeLagMin)); + + if (m_gameFakeLagRecord.size() >= kGameLagRecordMax) + { + UInt16 v, min = 65535, max = 0, total = 0; + std::vector::iterator it; + for (it = m_gameFakeLagRecord.begin(); it < m_gameFakeLagRecord.end(); ++it) + { + v = *it; + if (v < min) + { + min = v; + } + if (v > max) + { + max = v; + } + total += v; + } + + LOG((CLOG_INFO "game device timing, min=%dms, max=%dms, avg=%dms", + min, max, (UInt16)(total / m_gameFakeLagRecord.size()))); + m_gameFakeLagRecord.clear(); + } +} + +void +CMSWindowsXInput::gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2) +{ + XInputSetStateFunc xInputSetStateFunc = + (XInputSetStateFunc)GetProcAddress(m_xinputModule, "XInputSetState"); + + if (xInputSetStateFunc == NULL) + { + throw XScreenXInputFailure("could not get function address: XInputSetState"); + } + + XINPUT_VIBRATION vibration; + ZeroMemory(&vibration, sizeof(XINPUT_VIBRATION)); + vibration.wLeftMotorSpeed = m1; + vibration.wRightMotorSpeed = m2; + xInputSetStateFunc(id, &vibration); +} + +void +CMSWindowsXInput::xInputPollThread(void*) +{ + LOG((CLOG_DEBUG "xinput poll thread started")); + + XInputGetStateFunc xInputGetStateFunc = + (XInputGetStateFunc)GetProcAddress(m_xinputModule, "XInputGetState"); + + if (xInputGetStateFunc == NULL) + { + throw XScreenXInputFailure("could not get function address: XInputGetState"); + } + + int index = 0; + XINPUT_STATE state; + bool end = false; + while (!end) + { + ZeroMemory(&state, sizeof(XINPUT_STATE)); + DWORD result = xInputGetStateFunc(index, &state); + + // timeout the timing request after 10 seconds + if (m_gameTimingWaiting && (ARCH->time() - m_gameLastTimingSent > 2)) + { + m_gameTimingWaiting = false; + LOG((CLOG_DEBUG "game device timing request timed out")); + } + + // xinput controller is connected + if (result == ERROR_SUCCESS) + { + // @todo game device state class + bool buttonsChanged = state.Gamepad.wButtons != m_gameButtonsLast; + bool leftTriggerChanged = state.Gamepad.bLeftTrigger != m_gameLeftTriggerLast; + bool rightTriggerChanged = state.Gamepad.bRightTrigger != m_gameRightTriggerLast; + bool leftStickXChanged = state.Gamepad.sThumbLX != m_gameLeftStickXLast; + bool leftStickYChanged = state.Gamepad.sThumbLY != m_gameLeftStickYLast; + bool rightStickXChanged = state.Gamepad.sThumbRX != m_gameRightStickXLast; + bool rightStickYChanged = state.Gamepad.sThumbRY != m_gameRightStickYLast; + + m_gameButtonsLast = state.Gamepad.wButtons; + m_gameLeftTriggerLast = state.Gamepad.bLeftTrigger; + m_gameRightTriggerLast = state.Gamepad.bRightTrigger; + m_gameLeftStickXLast = state.Gamepad.sThumbLX; + m_gameLeftStickYLast = state.Gamepad.sThumbLY; + m_gameRightStickXLast = state.Gamepad.sThumbRX; + m_gameRightStickYLast = state.Gamepad.sThumbRY; + + bool eventSent = false; + + if (buttonsChanged) + { + LOG((CLOG_DEBUG "xinput buttons changed")); + + // xinput buttons convert exactly to synergy buttons + m_screen->sendEvent(m_screen->getGameDeviceButtonsEvent(), + new IPrimaryScreen::CGameDeviceButtonInfo(index, state.Gamepad.wButtons)); + + eventSent = true; + } + + if (leftStickXChanged || leftStickYChanged || rightStickXChanged || rightStickYChanged) + { + LOG((CLOG_DEBUG "xinput sticks changed")); + + m_screen->sendEvent(m_screen->getGameDeviceSticksEvent(), + new IPrimaryScreen::CGameDeviceStickInfo( + index, + m_gameLeftStickXLast, m_gameLeftStickYLast, + m_gameRightStickXLast, m_gameRightStickYLast)); + + eventSent = true; + } + + if (leftTriggerChanged || rightTriggerChanged) + { + LOG((CLOG_DEBUG "xinput triggers changed")); + + // @todo seems wrong re-using x/y for a single value... + m_screen->sendEvent(m_screen->getGameDeviceTriggersEvent(), + new IPrimaryScreen::CGameDeviceTriggerInfo( + index, + state.Gamepad.bLeftTrigger, + state.Gamepad.bRightTrigger)); + + eventSent = true; + } + + if (/*eventSent && */!m_gameTimingWaiting && (ARCH->time() - m_gameLastTimingSent > .5)) + { + m_screen->sendEvent(m_screen->getGameDeviceTimingReqEvent(), NULL); + m_gameLastTimingSent = ARCH->time(); + m_gameTimingWaiting = true; + + LOG((CLOG_DEBUG "game device timing request at %.4f", m_gameLastTimingSent)); + } + } + + UInt16 sleep = m_gamePollFreq + m_gamePollFreqAdjust; + LOG((CLOG_DEBUG5 "xinput poll sleeping for %dms", sleep)); + Sleep(sleep); + } +} + +void +CMSWindowsXInput::xInputTimingThread(void*) +{ + LOG((CLOG_DEBUG "xinput timing thread started")); + + bool end = false; + while (!end) + { + // if timing request was queued, a timing response is queued + // when the xinput status is faked; if it was faked, go tell + // the server when this happened. + if (DequeueXInputTimingResp()) + { + LOG((CLOG_DEBUG "dequeued game device timing response")); + m_screen->sendEvent(m_screen->getGameDeviceTimingRespEvent(), + new IPrimaryScreen::CGameDeviceTimingRespInfo(GetXInputFakeFreqMillis())); + } + + // give the cpu a break. + Sleep(1); + } +} + +void +CMSWindowsXInput::xInputFeedbackThread(void*) +{ + LOG((CLOG_DEBUG "xinput feedback thread started")); + + int index = 0; + bool end = false; + while (!end) + { + WORD leftMotor, rightMotor; + if (DequeueXInputFeedback(&leftMotor, &rightMotor)) + { + LOG((CLOG_DEBUG "dequeued game device feedback")); + m_screen->sendEvent(m_screen->getGameDeviceFeedbackEvent(), + new IPrimaryScreen::CGameDeviceFeedbackInfo(index, leftMotor, rightMotor)); + } + + Sleep(50); + } +} diff --git a/src/lib/platform/CMSWindowsXInput.h b/src/lib/platform/CMSWindowsXInput.h new file mode 100644 index 00000000..92f0138d --- /dev/null +++ b/src/lib/platform/CMSWindowsXInput.h @@ -0,0 +1,91 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CGameDevice.h" +#include "CThread.h" +#include "CGameDevice.h" +#include "BasicTypes.h" +#include "GameDeviceTypes.h" +#include "IPrimaryScreen.h" + +#define WIN32_LEAN_AND_MEAN +#include +#include + +class CMSWindowsScreen; + +enum +{ + kGamePollFreqDefault = 100, + kGamePollFreqMin = 50, + kGamePollFreqMax = 200, + kGameCalibrationPeriod = 10000, // 10 seconds + kGameLagRecordMax = 10, +}; + +class CMSWindowsXInput : public CGameDevice +{ +public: + CMSWindowsXInput(CMSWindowsScreen* screen, const CGameDeviceInfo& gameDevice); + virtual ~CMSWindowsXInput(); + + void init(CMSWindowsScreen* screen); + + // thread for polling xinput state. + void xInputPollThread(void*); + + // thread for checking queued timing requests. + void xInputTimingThread(void*); + + // thread for checking pending feedback state. + void xInputFeedbackThread(void*); + + virtual void gameDeviceTimingResp(UInt16 freq); + virtual void gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2); + virtual void fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const; + virtual void fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const; + virtual void fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const; + virtual void queueGameDeviceTimingReq() const; + +private: + CMSWindowsScreen* m_screen; + const CGameDeviceInfo& m_gameDeviceInfo; + CThread* m_xInputPollThread; + CThread* m_xInputTimingThread; + CThread* m_xInputFeedbackThread; + WORD m_gameButtonsLast; + BYTE m_gameLeftTriggerLast; + BYTE m_gameRightTriggerLast; + SHORT m_gameLeftStickXLast; + SHORT m_gameLeftStickYLast; + SHORT m_gameRightStickXLast; + SHORT m_gameRightStickYLast; + double m_gameLastTimingSent; + bool m_gameTimingWaiting; + UInt16 m_gameFakeLag; + UInt16 m_gameFakeLagMin; + UInt16 m_gamePollFreq; + SInt8 m_gamePollFreqAdjust; + UInt16 m_gameTimingStarted; + UInt16 m_gameTimingFirst; + UInt16 m_gameFakeLagLast; + bool m_gameTimingCalibrated; + std::vector m_gameFakeLagRecord; + HMODULE m_xinputModule; +}; diff --git a/src/lib/platform/CMakeLists.txt b/src/lib/platform/CMakeLists.txt new file mode 100644 index 00000000..1fe7e17a --- /dev/null +++ b/src/lib/platform/CMakeLists.txt @@ -0,0 +1,195 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +if (WIN32) + + set(inc + CMSWindowsClipboard.h + CMSWindowsClipboardAnyTextConverter.h + CMSWindowsClipboardBitmapConverter.h + CMSWindowsClipboardHTMLConverter.h + CMSWindowsClipboardTextConverter.h + CMSWindowsClipboardUTF16Converter.h + CMSWindowsDesks.h + CMSWindowsEventQueueBuffer.h + CMSWindowsKeyState.h + CMSWindowsScreen.h + CMSWindowsScreenSaver.h + CMSWindowsUtil.h + CMSWindowsRelauncher.h + CMSWindowsHookLibraryLoader.h + IMSWindowsClipboardFacade.h + CMSWindowsDebugOutputter.h + CMSWindowsXInput.h + ) + + set(src + CMSWindowsClipboard.cpp + CMSWindowsClipboardFacade.cpp + CMSWindowsClipboardAnyTextConverter.cpp + CMSWindowsClipboardBitmapConverter.cpp + CMSWindowsClipboardHTMLConverter.cpp + CMSWindowsClipboardTextConverter.cpp + CMSWindowsClipboardUTF16Converter.cpp + CMSWindowsDesks.cpp + CMSWindowsEventQueueBuffer.cpp + CMSWindowsKeyState.cpp + CMSWindowsScreen.cpp + CMSWindowsScreenSaver.cpp + CMSWindowsUtil.cpp + CMSWindowsRelauncher.cpp + CMSWindowsHookLibraryLoader.cpp + CMSWindowsDebugOutputter.cpp + ) + + set(inc_hook + CSynergyHook.h + ) + + set(src_hook + CSynergyHook.cpp + ) + + if (GAME_DEVICE_SUPPORT) + + list(APPEND inc + CMSWindowsXInput.h + ) + + list(APPEND src + CMSWindowsXInput.cpp + ) + + + set(inc_xinhook + XInputHook.h + HookDLL.h + ) + + set(src_xinhook + XInputHook.cpp + HookDLL.cpp + ) + + set(inc_xinproxy13 + XInput13.h + XInputProxy13.h + ) + + set(src_xinproxy13 + XInputProxy13.cpp + ) + + endif() + + list(APPEND src + ${inc} + ) + +elseif (APPLE) + + set(src + COSXClipboard.cpp + COSXClipboardAnyTextConverter.cpp + COSXClipboardTextConverter.cpp + COSXClipboardUTF16Converter.cpp + COSXEventQueueBuffer.cpp + COSXKeyState.cpp + COSXScreen.cpp + COSXScreenSaver.cpp + COSXScreenSaverUtil.m + ) + +elseif (UNIX) + + set(src + CXWindowsClipboard.cpp + CXWindowsClipboardAnyBitmapConverter.cpp + CXWindowsClipboardBMPConverter.cpp + CXWindowsClipboardHTMLConverter.cpp + CXWindowsClipboardTextConverter.cpp + CXWindowsClipboardUCS2Converter.cpp + CXWindowsClipboardUTF8Converter.cpp + CXWindowsEventQueueBuffer.cpp + CXWindowsKeyState.cpp + CXWindowsScreen.cpp + CXWindowsScreenSaver.cpp + CXWindowsUtil.cpp + ) + +endif() + +set(inc + ../arch + ../base + ../common + ../mt + ../synergy +) + +if (UNIX) + list(APPEND inc + ../../.. + ) +endif() + +include_directories(${inc}) +add_library(platform STATIC ${src}) + +if (WIN32) + add_library(synrgyhk SHARED ${inc_hook} ${src_hook}) + + # copy the dlls (and supporting files) from the lib dir to + # the bin dir, so that synergyc and synergys can easily find them. + # we should leave the other libraries compiling to the lib dir, + # so that the bin dir remains tidy. the path is relative to the + # build dir (in this case, that's: build\src\lib\platform). + add_custom_command( + TARGET synrgyhk + POST_BUILD + COMMAND xcopy /Y /Q + ..\\..\\..\\..\\lib\\${CMAKE_CFG_INTDIR}\\synrgyhk.* + ..\\..\\..\\..\\bin\\${CMAKE_CFG_INTDIR}\\ + ) + + if (GAME_DEVICE_SUPPORT) + include_directories($ENV{DXSDK_DIR}/Include) + add_library(synxinhk SHARED ${inc_xinhook} ${src_xinhook}) + + add_custom_command( + TARGET synxinhk + POST_BUILD + COMMAND xcopy /Y /Q + ..\\..\\..\\..\\lib\\${CMAKE_CFG_INTDIR}\\synxinhk.* + ..\\..\\..\\..\\bin\\${CMAKE_CFG_INTDIR}\\ + ) + + # synergy xinput1_3.dll proxy + include_directories($ENV{DXSDK_DIR}/Include) + add_library(sxinpx13 SHARED ${inc_xinproxy13} ${src_xinproxy13}) + target_link_libraries(sxinpx13 synxinhk) + add_custom_command( + TARGET sxinpx13 + POST_BUILD + COMMAND xcopy /Y /Q + ..\\..\\..\\..\\lib\\${CMAKE_CFG_INTDIR}\\sxinpx13.* + ..\\..\\..\\..\\bin\\${CMAKE_CFG_INTDIR}\\ + ) + endif() +endif() + +if (UNIX) + target_link_libraries(platform synergy ${libs}) +endif() diff --git a/src/lib/platform/COSXClipboard.cpp b/src/lib/platform/COSXClipboard.cpp new file mode 100644 index 00000000..fc04bac0 --- /dev/null +++ b/src/lib/platform/COSXClipboard.cpp @@ -0,0 +1,250 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "COSXClipboard.h" +#include "CClipboard.h" +#include "COSXClipboardUTF16Converter.h" +#include "COSXClipboardTextConverter.h" +#include "CLog.h" +#include "XArch.h" + +// +// COSXClipboard +// + +COSXClipboard::COSXClipboard() : + m_time(0), + m_pboard(NULL) +{ + m_converters.push_back(new COSXClipboardUTF16Converter); + m_converters.push_back(new COSXClipboardTextConverter); + + OSStatus createErr = PasteboardCreate(kPasteboardClipboard, &m_pboard); + if (createErr != noErr) { + LOG((CLOG_DEBUG "failed to create clipboard reference: error %i", createErr)); + LOG((CLOG_ERR "unable to connect to pasteboard, clipboard sharing disabled", createErr)); + m_pboard = NULL; + return; + + } + + OSStatus syncErr = PasteboardSynchronize(m_pboard); + if (syncErr != noErr) { + LOG((CLOG_DEBUG "failed to syncronize clipboard: error %i", syncErr)); + } +} + +COSXClipboard::~COSXClipboard() +{ + clearConverters(); +} + + bool +COSXClipboard::empty() +{ + LOG((CLOG_DEBUG "emptying clipboard")); + if (m_pboard == NULL) + return false; + + OSStatus err = PasteboardClear(m_pboard); + if (err != noErr) { + LOG((CLOG_DEBUG "failed to clear clipboard: error %i", err)); + return false; + } + + return true; +} + + bool +COSXClipboard::synchronize() +{ + if (m_pboard == NULL) + return false; + + PasteboardSyncFlags flags = PasteboardSynchronize(m_pboard); + LOG((CLOG_DEBUG2 "flags: %x", flags)); + + if (flags & kPasteboardModified) { + return true; + } + return false; +} + + void +COSXClipboard::add(EFormat format, const CString & data) +{ + bool emptied = false; + if (m_pboard == NULL) + return; + + LOG((CLOG_DEBUG "add %d bytes to clipboard format: %d", data.size(), format)); + + for (ConverterList::const_iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + + IOSXClipboardConverter* converter = *index; + + // skip converters for other formats + if (converter->getFormat() == format) { + CString osXData = converter->fromIClipboard(data); + CFStringRef flavorType = converter->getOSXFormat(); + CFDataRef dataRef = CFDataCreate(kCFAllocatorDefault, (UInt8 *)osXData.data(), osXData.size()); + + // integ tests showed that if you call add(...) twice, then the + // second call will actually fail to set clipboard data. calling + // empty() seems to solve this problem. but, only clear the clipboard + // for the first converter, otherwise further converters will wipe out + // what we just added. + if (!emptied) { + empty(); + emptied = true; + } + + PasteboardPutItemFlavor( + m_pboard, + (PasteboardItemID) 0, + flavorType, + dataRef, + kPasteboardFlavorNoFlags); + LOG((CLOG_DEBUG "added %d bytes to clipboard format: %d", data.size(), format)); + } + } +} + +bool +COSXClipboard::open(Time time) const +{ + if (m_pboard == NULL) + return false; + + LOG((CLOG_DEBUG "opening clipboard")); + m_time = time; + return true; +} + +void +COSXClipboard::close() const +{ + LOG((CLOG_DEBUG "closing clipboard")); + /* not needed */ +} + +IClipboard::Time +COSXClipboard::getTime() const +{ + return m_time; +} + +bool +COSXClipboard::has(EFormat format) const +{ + if (m_pboard == NULL) + return false; + + PasteboardItemID item; + PasteboardGetItemIdentifier(m_pboard, (CFIndex) 1, &item); + + for (ConverterList::const_iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + IOSXClipboardConverter* converter = *index; + if (converter->getFormat() == format) { + PasteboardFlavorFlags flags; + CFStringRef type = converter->getOSXFormat(); + + OSStatus res; + + if ((res = PasteboardGetItemFlavorFlags(m_pboard, item, type, &flags)) == noErr) { + return true; + } + } + } + + return false; +} + +CString +COSXClipboard::get(EFormat format) const +{ + CFStringRef type; + PasteboardItemID item; + CString result; + + if (m_pboard == NULL) + return result; + + PasteboardGetItemIdentifier(m_pboard, (CFIndex) 1, &item); + + + // find the converter for the first clipboard format we can handle + IOSXClipboardConverter* converter = NULL; + for (ConverterList::const_iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + converter = *index; + + PasteboardFlavorFlags flags; + type = converter->getOSXFormat(); + + if (converter->getFormat() == format && + PasteboardGetItemFlavorFlags(m_pboard, item, type, &flags) == noErr) { + break; + } + converter = NULL; + } + + // if no converter then we don't recognize any formats + if (converter == NULL) { + LOG((CLOG_DEBUG "Unable to find converter for data")); + return result; + } + + // get the clipboard data. + CFDataRef buffer = NULL; + try { + OSStatus err = PasteboardCopyItemFlavorData(m_pboard, item, type, &buffer); + + if (err != noErr) { + throw err; + } + + result = CString((char *) CFDataGetBytePtr(buffer), CFDataGetLength(buffer)); + } + catch (OSStatus err) { + LOG((CLOG_DEBUG "exception thrown in COSXClipboard::get MacError (%d)", err)); + } + catch (...) { + LOG((CLOG_DEBUG "unknown exception in COSXClipboard::get")); + RETHROW_XTHREAD + } + + if (buffer != NULL) + CFRelease(buffer); + + return converter->toIClipboard(result); +} + + void +COSXClipboard::clearConverters() +{ + if (m_pboard == NULL) + return; + + for (ConverterList::iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + delete *index; + } + m_converters.clear(); +} diff --git a/src/lib/platform/COSXClipboard.h b/src/lib/platform/COSXClipboard.h new file mode 100644 index 00000000..1f4aeb33 --- /dev/null +++ b/src/lib/platform/COSXClipboard.h @@ -0,0 +1,96 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef COSXCLIPBOARD_H +#define COSXCLIPBOARD_H + +#include +#include "IClipboard.h" +#include + +class IOSXClipboardConverter; + +//! OS X clipboard implementation +class COSXClipboard : public IClipboard { +public: + COSXClipboard(); + virtual ~COSXClipboard(); + + //! Test if clipboard is owned by synergy + static bool isOwnedBySynergy(); + + // IClipboard overrides + virtual bool empty(); + virtual void add(EFormat, const CString& data); + virtual bool open(Time) const; + virtual void close() const; + virtual Time getTime() const; + virtual bool has(EFormat) const; + virtual CString get(EFormat) const; + + bool synchronize(); +private: + void clearConverters(); + +private: + typedef std::vector ConverterList; + + mutable Time m_time; + ConverterList m_converters; + PasteboardRef m_pboard; +}; + +//! Clipboard format converter interface +/*! +This interface defines the methods common to all Scrap book format +*/ +class IOSXClipboardConverter : public IInterface { +public: + //! @name accessors + //@{ + + //! Get clipboard format + /*! + Return the clipboard format this object converts from/to. + */ + virtual IClipboard::EFormat + getFormat() const = 0; + + //! returns the scrap flavor type that this object converts from/to + virtual CFStringRef + getOSXFormat() const = 0; + + //! Convert from IClipboard format + /*! + Convert from the IClipboard format to the Carbon scrap format. + The input data must be in the IClipboard format returned by + getFormat(). The return data will be in the scrap + format returned by getOSXFormat(). + */ + virtual CString fromIClipboard(const CString&) const = 0; + + //! Convert to IClipboard format + /*! + Convert from the carbon scrap format to the IClipboard format + (i.e., the reverse of fromIClipboard()). + */ + virtual CString toIClipboard(const CString&) const = 0; + + //@} +}; + +#endif diff --git a/src/lib/platform/COSXClipboardAnyTextConverter.cpp b/src/lib/platform/COSXClipboardAnyTextConverter.cpp new file mode 100644 index 00000000..fcf55ea5 --- /dev/null +++ b/src/lib/platform/COSXClipboardAnyTextConverter.cpp @@ -0,0 +1,88 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "COSXClipboardAnyTextConverter.h" +#include + +// +// COSXClipboardAnyTextConverter +// + +COSXClipboardAnyTextConverter::COSXClipboardAnyTextConverter() +{ + // do nothing +} + +COSXClipboardAnyTextConverter::~COSXClipboardAnyTextConverter() +{ + // do nothing +} + +IClipboard::EFormat +COSXClipboardAnyTextConverter::getFormat() const +{ + return IClipboard::kText; +} + +CString +COSXClipboardAnyTextConverter::fromIClipboard(const CString& data) const +{ + // convert linefeeds and then convert to desired encoding + return doFromIClipboard(convertLinefeedToMacOS(data)); +} + +CString +COSXClipboardAnyTextConverter::toIClipboard(const CString& data) const +{ + // convert text then newlines + return convertLinefeedToUnix(doToIClipboard(data)); +} + +static +bool +isLF(char ch) +{ + return (ch == '\n'); +} + +static +bool +isCR(char ch) +{ + return (ch == '\r'); +} + +CString +COSXClipboardAnyTextConverter::convertLinefeedToMacOS(const CString& src) +{ + // note -- we assume src is a valid UTF-8 string + CString copy = src; + + std::replace_if(copy.begin(), copy.end(), isLF, '\r'); + + return copy; +} + +CString +COSXClipboardAnyTextConverter::convertLinefeedToUnix(const CString& src) +{ + CString copy = src; + + std::replace_if(copy.begin(), copy.end(), isCR, '\n'); + + return copy; +} diff --git a/src/lib/platform/COSXClipboardAnyTextConverter.h b/src/lib/platform/COSXClipboardAnyTextConverter.h new file mode 100644 index 00000000..487801c7 --- /dev/null +++ b/src/lib/platform/COSXClipboardAnyTextConverter.h @@ -0,0 +1,55 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef COSXCLIPBOARDANYTEXTCONVERTER_H +#define COSXCLIPBOARDANYTEXTCONVERTER_H + +#include "COSXClipboard.h" + +//! Convert to/from some text encoding +class COSXClipboardAnyTextConverter : public IOSXClipboardConverter { +public: + COSXClipboardAnyTextConverter(); + virtual ~COSXClipboardAnyTextConverter(); + + // IOSXClipboardConverter overrides + virtual IClipboard::EFormat + getFormat() const; + virtual CFStringRef + getOSXFormat() const = 0; + virtual CString fromIClipboard(const CString &) const; + virtual CString toIClipboard(const CString &) const; + +protected: + //! Convert from IClipboard format + /*! + Do UTF-8 conversion and linefeed conversion. + */ + virtual CString doFromIClipboard(const CString&) const = 0; + + //! Convert to IClipboard format + /*! + Do UTF-8 conversion and Linefeed conversion. + */ + virtual CString doToIClipboard(const CString&) const = 0; + +private: + static CString convertLinefeedToMacOS(const CString&); + static CString convertLinefeedToUnix(const CString&); +}; + +#endif diff --git a/src/lib/platform/COSXClipboardTextConverter.cpp b/src/lib/platform/COSXClipboardTextConverter.cpp new file mode 100644 index 00000000..bd9956b3 --- /dev/null +++ b/src/lib/platform/COSXClipboardTextConverter.cpp @@ -0,0 +1,91 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "COSXClipboardTextConverter.h" +#include "CUnicode.h" + +// +// COSXClipboardTextConverter +// + +COSXClipboardTextConverter::COSXClipboardTextConverter() +{ + // do nothing +} + +COSXClipboardTextConverter::~COSXClipboardTextConverter() +{ + // do nothing +} + +CFStringRef +COSXClipboardTextConverter::getOSXFormat() const +{ + return CFSTR("public.plain-text"); +} + +CString +COSXClipboardTextConverter::convertString( + const CString& data, + CFStringEncoding fromEncoding, + CFStringEncoding toEncoding) +{ + CFStringRef stringRef = + CFStringCreateWithCString(kCFAllocatorDefault, + data.c_str(), fromEncoding); + + if (stringRef == NULL) { + return CString(); + } + + CFIndex buffSize; + CFRange entireString = CFRangeMake(0, CFStringGetLength(stringRef)); + + CFStringGetBytes(stringRef, entireString, toEncoding, + 0, false, NULL, 0, &buffSize); + + char* buffer = new char[buffSize]; + + if (buffer == NULL) { + CFRelease(stringRef); + return CString(); + } + + CFStringGetBytes(stringRef, entireString, toEncoding, + 0, false, (UInt8*)buffer, buffSize, NULL); + + CString result(buffer, buffSize); + + delete[] buffer; + CFRelease(stringRef); + + return result; +} + +CString +COSXClipboardTextConverter::doFromIClipboard(const CString& data) const +{ + return convertString(data, kCFStringEncodingUTF8, + CFStringGetSystemEncoding()); +} + +CString +COSXClipboardTextConverter::doToIClipboard(const CString& data) const +{ + return convertString(data, CFStringGetSystemEncoding(), + kCFStringEncodingUTF8); +} diff --git a/src/lib/platform/COSXClipboardTextConverter.h b/src/lib/platform/COSXClipboardTextConverter.h new file mode 100644 index 00000000..aedf743d --- /dev/null +++ b/src/lib/platform/COSXClipboardTextConverter.h @@ -0,0 +1,44 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef COSXCLIPBOARDTEXTCONVERTER_H +#define COSXCLIPBOARDTEXTCONVERTER_H + +#include "COSXClipboardAnyTextConverter.h" + +//! Convert to/from locale text encoding +class COSXClipboardTextConverter : public COSXClipboardAnyTextConverter { +public: + COSXClipboardTextConverter(); + virtual ~COSXClipboardTextConverter(); + + // IOSXClipboardAnyTextConverter overrides + virtual CFStringRef + getOSXFormat() const; + +protected: + // COSXClipboardAnyTextConverter overrides + virtual CString doFromIClipboard(const CString&) const; + virtual CString doToIClipboard(const CString&) const; + + // generic encoding converter + static CString convertString(const CString& data, + CFStringEncoding fromEncoding, + CFStringEncoding toEncoding); +}; + +#endif diff --git a/src/lib/platform/COSXClipboardUTF16Converter.cpp b/src/lib/platform/COSXClipboardUTF16Converter.cpp new file mode 100644 index 00000000..06e688a5 --- /dev/null +++ b/src/lib/platform/COSXClipboardUTF16Converter.cpp @@ -0,0 +1,53 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "COSXClipboardUTF16Converter.h" +#include "CUnicode.h" + +// +// COSXClipboardUTF16Converter +// + +COSXClipboardUTF16Converter::COSXClipboardUTF16Converter() +{ + // do nothing +} + +COSXClipboardUTF16Converter::~COSXClipboardUTF16Converter() +{ + // do nothing +} + +CFStringRef +COSXClipboardUTF16Converter::getOSXFormat() const +{ + return CFSTR("public.utf16-plain-text"); +} + +CString +COSXClipboardUTF16Converter::doFromIClipboard(const CString& data) const +{ + // convert and add nul terminator + return CUnicode::UTF8ToUTF16(data); +} + +CString +COSXClipboardUTF16Converter::doToIClipboard(const CString& data) const +{ + // convert and strip nul terminator + return CUnicode::UTF16ToUTF8(data); +} diff --git a/src/lib/platform/COSXClipboardUTF16Converter.h b/src/lib/platform/COSXClipboardUTF16Converter.h new file mode 100644 index 00000000..833a90da --- /dev/null +++ b/src/lib/platform/COSXClipboardUTF16Converter.h @@ -0,0 +1,39 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef COSXCLIPBOARDUTF16CONVERTER_H +#define COSXCLIPBOARDUTF16CONVERTER_H + +#include "COSXClipboardAnyTextConverter.h" + +//! Convert to/from UTF-16 encoding +class COSXClipboardUTF16Converter : public COSXClipboardAnyTextConverter { +public: + COSXClipboardUTF16Converter(); + virtual ~COSXClipboardUTF16Converter(); + + // IOSXClipboardAnyTextConverter overrides + virtual CFStringRef + getOSXFormat() const; + +protected: + // COSXClipboardAnyTextConverter overrides + virtual CString doFromIClipboard(const CString&) const; + virtual CString doToIClipboard(const CString&) const; +}; + +#endif diff --git a/src/lib/platform/COSXEventQueueBuffer.cpp b/src/lib/platform/COSXEventQueueBuffer.cpp new file mode 100644 index 00000000..64b4e226 --- /dev/null +++ b/src/lib/platform/COSXEventQueueBuffer.cpp @@ -0,0 +1,127 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "COSXEventQueueBuffer.h" +#include "CEvent.h" +#include "IEventQueue.h" + +// +// CEventQueueTimer +// + +class CEventQueueTimer { }; + +// +// COSXEventQueueBuffer +// + +COSXEventQueueBuffer::COSXEventQueueBuffer() : + m_event(NULL) +{ + // do nothing +} + +COSXEventQueueBuffer::~COSXEventQueueBuffer() +{ + // release the last event + if (m_event != NULL) { + ReleaseEvent(m_event); + } +} + +void +COSXEventQueueBuffer::waitForEvent(double timeout) +{ + EventRef event; + ReceiveNextEvent(0, NULL, timeout, false, &event); +} + +IEventQueueBuffer::Type +COSXEventQueueBuffer::getEvent(CEvent& event, UInt32& dataID) +{ + // release the previous event + if (m_event != NULL) { + ReleaseEvent(m_event); + m_event = NULL; + } + + // get the next event + OSStatus error = ReceiveNextEvent(0, NULL, 0.0, true, &m_event); + + // handle the event + if (error == eventLoopQuitErr) { + event = CEvent(CEvent::kQuit); + return kSystem; + } + else if (error != noErr) { + return kNone; + } + else { + UInt32 eventClass = GetEventClass(m_event); + switch (eventClass) { + case 'Syne': + dataID = GetEventKind(m_event); + return kUser; + + default: + event = CEvent(CEvent::kSystem, + IEventQueue::getSystemTarget(), &m_event); + return kSystem; + } + } +} + +bool +COSXEventQueueBuffer::addEvent(UInt32 dataID) +{ + EventRef event; + OSStatus error = CreateEvent( + kCFAllocatorDefault, + 'Syne', + dataID, + 0, + kEventAttributeNone, + &event); + + if (error == noErr) { + error = PostEventToQueue(GetMainEventQueue(), event, + kEventPriorityStandard); + ReleaseEvent(event); + } + + return (error == noErr); +} + +bool +COSXEventQueueBuffer::isEmpty() const +{ + EventRef event; + OSStatus status = ReceiveNextEvent(0, NULL, 0.0, false, &event); + return (status == eventLoopTimedOutErr); +} + +CEventQueueTimer* +COSXEventQueueBuffer::newTimer(double, bool) const +{ + return new CEventQueueTimer; +} + +void +COSXEventQueueBuffer::deleteTimer(CEventQueueTimer* timer) const +{ + delete timer; +} diff --git a/src/lib/platform/COSXEventQueueBuffer.h b/src/lib/platform/COSXEventQueueBuffer.h new file mode 100644 index 00000000..b03afb32 --- /dev/null +++ b/src/lib/platform/COSXEventQueueBuffer.h @@ -0,0 +1,43 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef COSXEVENTQUEUEBUFFER_H +#define COSXEVENTQUEUEBUFFER_H + +#include +#include "IEventQueueBuffer.h" + +//! Event queue buffer for OS X +class COSXEventQueueBuffer : public IEventQueueBuffer { +public: + COSXEventQueueBuffer(); + virtual ~COSXEventQueueBuffer(); + + // IEventQueueBuffer overrides + virtual void waitForEvent(double timeout); + virtual Type getEvent(CEvent& event, UInt32& dataID); + virtual bool addEvent(UInt32 dataID); + virtual bool isEmpty() const; + virtual CEventQueueTimer* + newTimer(double duration, bool oneShot) const; + virtual void deleteTimer(CEventQueueTimer*) const; + +private: + EventRef m_event; +}; + +#endif diff --git a/src/lib/platform/COSXKeyState.cpp b/src/lib/platform/COSXKeyState.cpp new file mode 100644 index 00000000..0f1841b3 --- /dev/null +++ b/src/lib/platform/COSXKeyState.cpp @@ -0,0 +1,1317 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "COSXKeyState.h" +#include "CLog.h" +#include "CArch.h" + +#if defined(MAC_OS_X_VERSION_10_5) +#include +#endif + +// Note that some virtual keys codes appear more than once. The +// first instance of a virtual key code maps to the KeyID that we +// want to generate for that code. The others are for mapping +// different KeyIDs to a single key code. + +#if defined(MAC_OS_X_VERSION_10_5) +static const UInt32 s_shiftVK = kVK_Shift; +static const UInt32 s_controlVK = kVK_Control; +static const UInt32 s_altVK = kVK_Option; +static const UInt32 s_superVK = kVK_Command; +static const UInt32 s_capsLockVK = kVK_CapsLock; +static const UInt32 s_numLockVK = kVK_ANSI_KeypadClear; // 71 +#else +// Hardcoded virtual key table on 10.4 and below. +static const UInt32 s_shiftVK = 56; +static const UInt32 s_controlVK = 59; +static const UInt32 s_altVK = 58; +static const UInt32 s_superVK = 55; +static const UInt32 s_capsLockVK = 57; +static const UInt32 s_numLockVK = 71; +#endif + +static const UInt32 s_osxNumLock = 1 << 16; + +struct CKeyEntry { +public: + KeyID m_keyID; + UInt32 m_virtualKey; +}; +static const CKeyEntry s_controlKeys[] = { +#if defined(MAC_OS_X_VERSION_10_5) + // cursor keys. if we don't do this we'll may still get these from + // the keyboard resource but they may not correspond to the arrow + // keys. + { kKeyLeft, kVK_LeftArrow }, + { kKeyRight, kVK_RightArrow }, + { kKeyUp, kVK_UpArrow }, + { kKeyDown, kVK_DownArrow }, + { kKeyHome, kVK_Home }, + { kKeyEnd, kVK_End }, + { kKeyPageUp, kVK_PageUp }, + { kKeyPageDown, kVK_PageDown }, + { kKeyInsert, kVK_Help }, // Mac Keyboards have 'Help' on 'Insert' + + // function keys + { kKeyF1, kVK_F1 }, + { kKeyF2, kVK_F2 }, + { kKeyF3, kVK_F3 }, + { kKeyF4, kVK_F4 }, + { kKeyF5, kVK_F5 }, + { kKeyF6, kVK_F6 }, + { kKeyF7, kVK_F7 }, + { kKeyF8, kVK_F8 }, + { kKeyF9, kVK_F9 }, + { kKeyF10, kVK_F10 }, + { kKeyF11, kVK_F11 }, + { kKeyF12, kVK_F12 }, + { kKeyF13, kVK_F13 }, + { kKeyF14, kVK_F14 }, + { kKeyF15, kVK_F15 }, + { kKeyF16, kVK_F16 }, + + { kKeyKP_0, kVK_ANSI_Keypad0 }, + { kKeyKP_1, kVK_ANSI_Keypad1 }, + { kKeyKP_2, kVK_ANSI_Keypad2 }, + { kKeyKP_3, kVK_ANSI_Keypad3 }, + { kKeyKP_4, kVK_ANSI_Keypad4 }, + { kKeyKP_5, kVK_ANSI_Keypad5 }, + { kKeyKP_6, kVK_ANSI_Keypad6 }, + { kKeyKP_7, kVK_ANSI_Keypad7 }, + { kKeyKP_8, kVK_ANSI_Keypad8 }, + { kKeyKP_9, kVK_ANSI_Keypad9 }, + { kKeyKP_Decimal, kVK_ANSI_KeypadDecimal }, + { kKeyKP_Equal, kVK_ANSI_KeypadEquals }, + { kKeyKP_Multiply, kVK_ANSI_KeypadMultiply }, + { kKeyKP_Add, kVK_ANSI_KeypadPlus }, + { kKeyKP_Divide, kVK_ANSI_KeypadDivide }, + { kKeyKP_Subtract, kVK_ANSI_KeypadMinus }, + { kKeyKP_Enter, kVK_ANSI_KeypadEnter }, +#else + // Hardcoded virtual key table on 10.4 and below. + // cursor keys. + { kKeyLeft, 123 }, + { kKeyRight, 124 }, + { kKeyUp, 126 }, + { kKeyDown, 125 }, + { kKeyHome, 115 }, + { kKeyEnd, 119 }, + { kKeyPageUp, 116 }, + { kKeyPageDown, 121 }, + { kKeyInsert, 114 }, + + // function keys + { kKeyF1, 122 }, + { kKeyF2, 120 }, + { kKeyF3, 99 }, + { kKeyF4, 118 }, + { kKeyF5, 96 }, + { kKeyF6, 97 }, + { kKeyF7, 98 }, + { kKeyF8, 100 }, + { kKeyF9, 101 }, + { kKeyF10, 109 }, + { kKeyF11, 103 }, + { kKeyF12, 111 }, + { kKeyF13, 105 }, + { kKeyF14, 107 }, + { kKeyF15, 113 }, + { kKeyF16, 106 }, + + { kKeyKP_0, 82 }, + { kKeyKP_1, 83 }, + { kKeyKP_2, 84 }, + { kKeyKP_3, 85 }, + { kKeyKP_4, 86 }, + { kKeyKP_5, 87 }, + { kKeyKP_6, 88 }, + { kKeyKP_7, 89 }, + { kKeyKP_8, 91 }, + { kKeyKP_9, 92 }, + { kKeyKP_Decimal, 65 }, + { kKeyKP_Equal, 81 }, + { kKeyKP_Multiply, 67 }, + { kKeyKP_Add, 69 }, + { kKeyKP_Divide, 75 }, + { kKeyKP_Subtract, 78 }, + { kKeyKP_Enter, 76 }, +#endif + + // virtual key 110 is fn+enter and i have no idea what that's supposed + // to map to. also the enter key with numlock on is a modifier but i + // don't know which. + + // modifier keys. OS X doesn't seem to support right handed versions + // of modifier keys so we map them to the left handed versions. + { kKeyShift_L, s_shiftVK }, + { kKeyShift_R, s_shiftVK }, // 60 + { kKeyControl_L, s_controlVK }, + { kKeyControl_R, s_controlVK }, // 62 + { kKeyAlt_L, s_altVK }, + { kKeyAlt_R, s_altVK }, + { kKeySuper_L, s_superVK }, + { kKeySuper_R, s_superVK }, // 61 + { kKeyMeta_L, s_superVK }, + { kKeyMeta_R, s_superVK }, // 61 + + // toggle modifiers + { kKeyNumLock, s_numLockVK }, + { kKeyCapsLock, s_capsLockVK } +}; + + +// +// COSXKeyState +// + +COSXKeyState::COSXKeyState() : + m_deadKeyState(0) +{ + init(); +} + +COSXKeyState::COSXKeyState(IEventQueue& eventQueue, CKeyMap& keyMap) : + CKeyState(eventQueue, keyMap), + m_deadKeyState(0) +{ + init(); +} + +COSXKeyState::~COSXKeyState() +{ +} + +void +COSXKeyState::init() +{ + // initialize modifier key values + shiftPressed = false; + controlPressed = false; + altPressed = false; + superPressed = false; + capsPressed = false; + + // build virtual key map + for (size_t i = 0; i < sizeof(s_controlKeys) / + sizeof(s_controlKeys[0]); ++i) { + m_virtualKeyMap[s_controlKeys[i].m_virtualKey] = + s_controlKeys[i].m_keyID; + } +} + +KeyModifierMask +COSXKeyState::mapModifiersFromOSX(UInt32 mask) const +{ + LOG((CLOG_DEBUG1 "mask: %04x", mask)); + + KeyModifierMask outMask = 0; + if ((mask & kCGEventFlagMaskShift) != 0) { + outMask |= KeyModifierShift; + } + if ((mask & kCGEventFlagMaskControl) != 0) { + outMask |= KeyModifierControl; + } + if ((mask & kCGEventFlagMaskAlternate) != 0) { + outMask |= KeyModifierAlt; + } + if ((mask & kCGEventFlagMaskCommand) != 0) { + outMask |= KeyModifierSuper; + } + if ((mask & kCGEventFlagMaskAlphaShift) != 0) { + outMask |= KeyModifierCapsLock; + } + if ((mask & kCGEventFlagMaskNumericPad) != 0) { + outMask |= KeyModifierNumLock; + } + + return outMask; +} + +KeyModifierMask +COSXKeyState::mapModifiersToCarbon(UInt32 mask) const +{ + KeyModifierMask outMask = 0; + if ((mask & kCGEventFlagMaskShift) != 0) { + outMask |= shiftKey; + } + if ((mask & kCGEventFlagMaskControl) != 0) { + outMask |= controlKey; + } + if ((mask & kCGEventFlagMaskCommand) != 0) { + outMask |= cmdKey; + } + if ((mask & kCGEventFlagMaskAlternate) != 0) { + outMask |= optionKey; + } + if ((mask & kCGEventFlagMaskAlphaShift) != 0) { + outMask |= alphaLock; + } + if ((mask & kCGEventFlagMaskNumericPad) != 0) { + outMask |= s_osxNumLock; + } + + return outMask; +} + +KeyButton +COSXKeyState::mapKeyFromEvent(CKeyIDs& ids, + KeyModifierMask* maskOut, CGEventRef event) const +{ + ids.clear(); + + // map modifier key + if (maskOut != NULL) { + KeyModifierMask activeMask = getActiveModifiers(); + activeMask &= ~KeyModifierAltGr; + *maskOut = activeMask; + } + + // get virtual key + UInt32 vkCode = CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode); + + // handle up events + UInt32 eventKind = CGEventGetType(event); + if (eventKind == kCGEventKeyUp) { + // the id isn't used. we just need the same button we used on + // the key press. note that we don't use or reset the dead key + // state; up events should not affect the dead key state. + ids.push_back(kKeyNone); + return mapVirtualKeyToKeyButton(vkCode); + } + + // check for special keys + CVirtualKeyMap::const_iterator i = m_virtualKeyMap.find(vkCode); + if (i != m_virtualKeyMap.end()) { + m_deadKeyState = 0; + ids.push_back(i->second); + return mapVirtualKeyToKeyButton(vkCode); + } + + // get keyboard info + +#if defined(MAC_OS_X_VERSION_10_5) + TISInputSourceRef currentKeyboardLayout = TISCopyCurrentKeyboardLayoutInputSource(); +#else + KeyboardLayoutRef currentKeyboardLayout; + OSStatus status = KLGetCurrentKeyboardLayout(¤tKeyboardLayout); +#endif + if (currentKeyboardLayout == NULL) { + return kKeyNone; + } + + // get the event modifiers and remove the command and control + // keys. note if we used them though. + // UCKeyTranslate expects old-style Carbon modifiers, so convert. + UInt32 modifiers; + modifiers = mapModifiersToCarbon(CGEventGetFlags(event)); + static const UInt32 s_commandModifiers = + cmdKey | controlKey | rightControlKey; + bool isCommand = ((modifiers & s_commandModifiers) != 0); + modifiers &= ~s_commandModifiers; + + // if we've used a command key then we want the glyph produced without + // the option key (i.e. the base glyph). + //if (isCommand) { + modifiers &= ~optionKey; + //} + + // choose action + UInt16 action; + if(eventKind==kCGEventKeyDown) { + action = kUCKeyActionDown; + } + else if(CGEventGetIntegerValueField(event, kCGKeyboardEventAutorepeat)==1) { + action = kUCKeyActionAutoKey; + } + else { + return 0; + } + + // translate via uchr resource +#if defined(MAC_OS_X_VERSION_10_5) + CFDataRef ref = (CFDataRef) TISGetInputSourceProperty(currentKeyboardLayout, + kTISPropertyUnicodeKeyLayoutData); + const UCKeyboardLayout* layout = (const UCKeyboardLayout*) CFDataGetBytePtr(ref); + const bool layoutValid = (layout != NULL); +#else + const void* resource; + int err = KLGetKeyboardLayoutProperty(currentKeyboardLayout, kKLuchrData, &resource); + const bool layoutValid = (err == noErr); + const UCKeyboardLayout* layout = (const UCKeyboardLayout*)resource; +#endif + + if (layoutValid) { + // translate key + UniCharCount count; + UniChar chars[2]; + LOG((CLOG_DEBUG2 "modifiers: %08x", modifiers & 0xffu)); + OSStatus status = UCKeyTranslate(layout, + vkCode & 0xffu, action, + (modifiers >> 8) & 0xffu, + LMGetKbdType(), 0, &m_deadKeyState, + sizeof(chars) / sizeof(chars[0]), &count, chars); + + // get the characters + if (status == 0) { + if (count != 0 || m_deadKeyState == 0) { + m_deadKeyState = 0; + for (UniCharCount i = 0; i < count; ++i) { + ids.push_back(CKeyResource::unicharToKeyID(chars[i])); + } + adjustAltGrModifier(ids, maskOut, isCommand); + return mapVirtualKeyToKeyButton(vkCode); + } + return 0; + } + } + + return 0; +} + +bool +COSXKeyState::fakeCtrlAltDel() +{ + // pass keys through unchanged + return false; +} + +KeyModifierMask +COSXKeyState::pollActiveModifiers() const +{ + return mapModifiersFromOSX(GetCurrentKeyModifiers()); +} + +SInt32 +COSXKeyState::pollActiveGroup() const +{ + bool layoutValid = true; +#if defined(MAC_OS_X_VERSION_10_5) + TISInputSourceRef keyboardLayout = TISCopyCurrentKeyboardLayoutInputSource(); +#else + KeyboardLayoutRef keyboardLayout; + OSStatus status = KLGetCurrentKeyboardLayout(&keyboardLayout); + layoutValid = (status == noErr); +#endif + + if (layoutValid) { + GroupMap::const_iterator i = m_groupMap.find(keyboardLayout); + if (i != m_groupMap.end()) { + return i->second; + } + } + return 0; +} + +void +COSXKeyState::pollPressedKeys(KeyButtonSet& pressedKeys) const +{ + KeyMap km; + GetKeys(km); + const UInt8* m = reinterpret_cast(km); + for (UInt32 i = 0; i < 16; ++i) { + for (UInt32 j = 0; j < 8; ++j) { + if ((m[i] & (1u << j)) != 0) { + pressedKeys.insert(mapVirtualKeyToKeyButton(8 * i + j)); + } + } + } +} + +void +COSXKeyState::getKeyMap(CKeyMap& keyMap) +{ + // update keyboard groups + if (getGroups(m_groups)) { + m_groupMap.clear(); + SInt32 numGroups = (SInt32)m_groups.size(); + for (SInt32 g = 0; g < numGroups; ++g) { + m_groupMap[m_groups[g]] = g; + } + } + + UInt32 keyboardType = LMGetKbdType(); + for (SInt32 g = 0, n = (SInt32)m_groups.size(); g < n; ++g) { + // add special keys + getKeyMapForSpecialKeys(keyMap, g); + + const void* resource; + bool layoutValid = false; + + // add regular keys + // try uchr resource first + #if defined(MAC_OS_X_VERSION_10_5) + CFDataRef resourceRef = (CFDataRef)TISGetInputSourceProperty( + m_groups[g], kTISPropertyUnicodeKeyLayoutData); + layoutValid = resourceRef != NULL; + if (layoutValid) + resource = CFDataGetBytePtr(resourceRef); + #else + layoutValid = KLGetKeyboardLayoutProperty( + m_groups[g], kKLuchrData, &resource); + #endif + + if (layoutValid) { + CUCHRKeyResource uchr(resource, keyboardType); + if (uchr.isValid()) { + LOG((CLOG_DEBUG1 "using uchr resource for group %d", g)); + getKeyMap(keyMap, g, uchr); + continue; + } + } + + LOG((CLOG_DEBUG1 "no keyboard resource for group %d", g)); + } +} + +void +COSXKeyState::fakeKey(const Keystroke& keystroke) +{ + CGEventRef ref; + + switch (keystroke.m_type) { + case Keystroke::kButton: + { + LOG((CLOG_DEBUG1 " %03x (%08x) %s", keystroke.m_data.m_button.m_button, keystroke.m_data.m_button.m_client, keystroke.m_data.m_button.m_press ? "down" : "up")); + + // let system figure out character for us + ref = CGEventCreateKeyboardEvent(0, mapKeyButtonToVirtualKey( + keystroke.m_data.m_button.m_button), + keystroke.m_data.m_button.m_press); + if (ref == NULL) { + LOG((CLOG_CRIT "unable to create keyboard event for keystroke")); + } + + UInt32 vk = mapKeyButtonToVirtualKey(keystroke.m_data.m_button.m_button); + UInt32 modifierDown = keystroke.m_data.m_button.m_press; + + // check the key for specials and store the value (persistent until changed) + if (vk == s_shiftVK) shiftPressed=modifierDown; + if (vk == s_controlVK) controlPressed=modifierDown; + if (vk == s_altVK) altPressed=modifierDown; + if (vk == s_superVK) superPressed=modifierDown; + if (vk == s_capsLockVK) capsPressed=modifierDown; + + //Set the event flags for special keys - see following link: + //http://stackoverflow.com/questions/2008126/cgeventpost-possible-bug-when-simulating-keyboard-events + CGEventFlags modifiers = 0; + if (shiftPressed) modifiers |= kCGEventFlagMaskShift; + if (controlPressed) modifiers |= kCGEventFlagMaskControl; + if (altPressed) modifiers |= kCGEventFlagMaskAlternate; + if (superPressed) modifiers |= kCGEventFlagMaskCommand; + if (capsPressed) modifiers |= kCGEventFlagMaskAlphaShift; + + CGEventSetFlags(ref, modifiers); + + CGEventPost(kCGHIDEventTap, ref); + + // add a delay if client data isn't zero + if (keystroke.m_data.m_button.m_client) { + ARCH->sleep(0.01); + } + } + break; + + case Keystroke::kGroup: + if (keystroke.m_data.m_group.m_absolute) { + LOG((CLOG_DEBUG1 " group %d", keystroke.m_data.m_group.m_group)); + setGroup(keystroke.m_data.m_group.m_group); + } + else { + LOG((CLOG_DEBUG1 " group %+d", keystroke.m_data.m_group.m_group)); + setGroup(getEffectiveGroup(pollActiveGroup(), + keystroke.m_data.m_group.m_group)); + } + break; + } +} + +void +COSXKeyState::getKeyMapForSpecialKeys(CKeyMap& keyMap, SInt32 group) const +{ + // special keys are insensitive to modifers and none are dead keys + CKeyMap::KeyItem item; + for (size_t i = 0; i < sizeof(s_controlKeys) / + sizeof(s_controlKeys[0]); ++i) { + const CKeyEntry& entry = s_controlKeys[i]; + item.m_id = entry.m_keyID; + item.m_group = group; + item.m_button = mapVirtualKeyToKeyButton(entry.m_virtualKey); + item.m_required = 0; + item.m_sensitive = 0; + item.m_dead = false; + item.m_client = 0; + CKeyMap::initModifierKey(item); + keyMap.addKeyEntry(item); + + if (item.m_lock) { + // all locking keys are half duplex on OS X + keyMap.addHalfDuplexButton(item.m_button); + } + } + + // note: we don't special case the number pad keys. querying the + // mac keyboard returns the non-keypad version of those keys but + // a CKeyState always provides a mapping from keypad keys to + // non-keypad keys so we'll be able to generate the characters + // anyway. +} + +bool +COSXKeyState::getKeyMap(CKeyMap& keyMap, + SInt32 group, const CKeyResource& r) const +{ + if (!r.isValid()) { + return false; + } + + // space for all possible modifier combinations + std::vector modifiers(r.getNumModifierCombinations()); + + // make space for the keys that any single button can synthesize + std::vector > buttonKeys(r.getNumTables()); + + // iterate over each button + CKeyMap::KeyItem item; + for (UInt32 i = 0; i < r.getNumButtons(); ++i) { + item.m_button = mapVirtualKeyToKeyButton(i); + + // the KeyIDs we've already handled + std::set keys; + + // convert the entry in each table for this button to a KeyID + for (UInt32 j = 0; j < r.getNumTables(); ++j) { + buttonKeys[j].first = r.getKey(j, i); + buttonKeys[j].second = CKeyMap::isDeadKey(buttonKeys[j].first); + } + + // iterate over each character table + for (UInt32 j = 0; j < r.getNumTables(); ++j) { + // get the KeyID for the button/table + KeyID id = buttonKeys[j].first; + if (id == kKeyNone) { + continue; + } + + // if we've already handled the KeyID in the table then + // move on to the next table + if (keys.count(id) > 0) { + continue; + } + keys.insert(id); + + // prepare item. the client state is 1 for dead keys. + item.m_id = id; + item.m_group = group; + item.m_dead = buttonKeys[j].second; + item.m_client = buttonKeys[j].second ? 1 : 0; + CKeyMap::initModifierKey(item); + if (item.m_lock) { + // all locking keys are half duplex on OS X + keyMap.addHalfDuplexButton(i); + } + + // collect the tables that map to the same KeyID. we know it + // can't be any earlier tables because of the check above. + std::set tables; + tables.insert(static_cast(j)); + for (UInt32 k = j + 1; k < r.getNumTables(); ++k) { + if (buttonKeys[k].first == id) { + tables.insert(static_cast(k)); + } + } + + // collect the modifier combinations that map to any of the + // tables we just collected + for (UInt32 k = 0; k < r.getNumModifierCombinations(); ++k) { + modifiers[k] = (tables.count(r.getTableForModifier(k)) > 0); + } + + // figure out which modifiers the key is sensitive to. the + // key is insensitive to a modifier if for every modifier mask + // with the modifier bit unset in the modifiers we also find + // the same mask with the bit set. + // + // we ignore a few modifiers that we know aren't important + // for generating characters. in fact, we want to ignore any + // characters generated by the control key. we don't map + // those and instead expect the control modifier plus a key. + UInt32 sensitive = 0; + for (UInt32 k = 0; (1u << k) < + r.getNumModifierCombinations(); ++k) { + UInt32 bit = (1u << k); + if ((bit << 8) == cmdKey || + (bit << 8) == controlKey || + (bit << 8) == rightControlKey) { + continue; + } + for (UInt32 m = 0; m < r.getNumModifierCombinations(); ++m) { + if (modifiers[m] != modifiers[m ^ bit]) { + sensitive |= bit; + break; + } + } + } + + // find each required modifier mask. the key can be synthesized + // using any of the masks. + std::set required; + for (UInt32 k = 0; k < r.getNumModifierCombinations(); ++k) { + if ((k & sensitive) == k && modifiers[k & sensitive]) { + required.insert(k); + } + } + + // now add a key entry for each key/required modifier pair. + item.m_sensitive = mapModifiersFromOSX(sensitive << 8); + for (std::set::iterator k = required.begin(); + k != required.end(); ++k) { + item.m_required = mapModifiersFromOSX(*k << 8); + keyMap.addKeyEntry(item); + } + } + } + + return true; +} + +bool +COSXKeyState::mapSynergyHotKeyToMac(KeyID key, KeyModifierMask mask, + UInt32 &macVirtualKey, UInt32 &macModifierMask) const +{ + // look up button for key + KeyButton button = getButton(key, pollActiveGroup()); + if (button == 0 && key != kKeyNone) { + return false; + } + macVirtualKey = mapKeyButtonToVirtualKey(button); + + // calculate modifier mask + macModifierMask = 0; + if ((mask & KeyModifierShift) != 0) { + macModifierMask |= shiftKey; + } + if ((mask & KeyModifierControl) != 0) { + macModifierMask |= controlKey; + } + if ((mask & KeyModifierAlt) != 0) { + macModifierMask |= cmdKey; + } + if ((mask & KeyModifierSuper) != 0) { + macModifierMask |= optionKey; + } + if ((mask & KeyModifierCapsLock) != 0) { + macModifierMask |= alphaLock; + } + if ((mask & KeyModifierNumLock) != 0) { + macModifierMask |= s_osxNumLock; + } + + return true; +} + +void +COSXKeyState::handleModifierKeys(void* target, + KeyModifierMask oldMask, KeyModifierMask newMask) +{ + // compute changed modifiers + KeyModifierMask changed = (oldMask ^ newMask); + + // synthesize changed modifier keys + if ((changed & KeyModifierShift) != 0) { + handleModifierKey(target, s_shiftVK, kKeyShift_L, + (newMask & KeyModifierShift) != 0, newMask); + } + if ((changed & KeyModifierControl) != 0) { + handleModifierKey(target, s_controlVK, kKeyControl_L, + (newMask & KeyModifierControl) != 0, newMask); + } + if ((changed & KeyModifierAlt) != 0) { + handleModifierKey(target, s_altVK, kKeyAlt_L, + (newMask & KeyModifierAlt) != 0, newMask); + } + if ((changed & KeyModifierSuper) != 0) { + handleModifierKey(target, s_superVK, kKeySuper_L, + (newMask & KeyModifierSuper) != 0, newMask); + } + if ((changed & KeyModifierCapsLock) != 0) { + handleModifierKey(target, s_capsLockVK, kKeyCapsLock, + (newMask & KeyModifierCapsLock) != 0, newMask); + } + if ((changed & KeyModifierNumLock) != 0) { + handleModifierKey(target, s_numLockVK, kKeyNumLock, + (newMask & KeyModifierNumLock) != 0, newMask); + } +} + +void +COSXKeyState::handleModifierKey(void* target, + UInt32 virtualKey, KeyID id, + bool down, KeyModifierMask newMask) +{ + KeyButton button = mapVirtualKeyToKeyButton(virtualKey); + onKey(button, down, newMask); + sendKeyEvent(target, down, false, id, newMask, 0, button); +} + +bool +COSXKeyState::getGroups(GroupList& groups) const +{ + CFIndex n; + bool gotLayouts = false; + +#if defined(MAC_OS_X_VERSION_10_5) + // get number of layouts + CFStringRef keys[] = { kTISPropertyInputSourceCategory }; + CFStringRef values[] = { kTISCategoryKeyboardInputSource }; + CFDictionaryRef dict = CFDictionaryCreate(NULL, (const void **)keys, (const void **)values, 1, NULL, NULL); + CFArrayRef kbds = TISCreateInputSourceList(dict, false); + n = CFArrayGetCount(kbds); + gotLayouts = (n != 0); +#else + OSStatus status = KLGetKeyboardLayoutCount(&n); + gotLayouts = (status == noErr); +#endif + + if (!gotLayouts) { + LOG((CLOG_DEBUG1 "can't get keyboard layouts")); + return false; + } + + // get each layout + groups.clear(); + for (CFIndex i = 0; i < n; ++i) { + bool addToGroups = true; +#if defined(MAC_OS_X_VERSION_10_5) + TISInputSourceRef keyboardLayout = + (TISInputSourceRef)CFArrayGetValueAtIndex(kbds, i); +#else + KeyboardLayoutRef keyboardLayout; + status = KLGetKeyboardLayoutAtIndex(i, &keyboardLayout); + addToGroups == (status == noErr); +#endif + if (addToGroups) + groups.push_back(keyboardLayout); + } + return true; +} + +void +COSXKeyState::setGroup(SInt32 group) +{ +#if defined(MAC_OS_X_VERSION_10_5) + TISSetInputMethodKeyboardLayoutOverride(m_groups[group]); +#else + KLSetCurrentKeyboardLayout(m_groups[group]); +#endif +} + +void +COSXKeyState::checkKeyboardLayout() +{ + // XXX -- should call this when notified that groups have changed. + // if no notification for that then we should poll. + GroupList groups; + if (getGroups(groups) && groups != m_groups) { + updateKeyMap(); + updateKeyState(); + } +} + +void +COSXKeyState::adjustAltGrModifier(const CKeyIDs& ids, + KeyModifierMask* mask, bool isCommand) const +{ + if (!isCommand) { + for (CKeyIDs::const_iterator i = ids.begin(); i != ids.end(); ++i) { + KeyID id = *i; + if (id != kKeyNone && + ((id < 0xe000u || id > 0xefffu) || + (id >= kKeyKP_Equal && id <= kKeyKP_9))) { + *mask |= KeyModifierAltGr; + return; + } + } + } +} + +KeyButton +COSXKeyState::mapVirtualKeyToKeyButton(UInt32 keyCode) +{ + // 'A' maps to 0 so shift every id + return static_cast(keyCode + KeyButtonOffset); +} + +UInt32 +COSXKeyState::mapKeyButtonToVirtualKey(KeyButton keyButton) +{ + return static_cast(keyButton - KeyButtonOffset); +} + + +// +// COSXKeyState::CKeyResource +// + +KeyID +COSXKeyState::CKeyResource::getKeyID(UInt8 c) +{ + if (c == 0) { + return kKeyNone; + } + else if (c >= 32 && c < 127) { + // ASCII + return static_cast(c); + } + else { + // handle special keys + switch (c) { + case 0x01: + return kKeyHome; + + case 0x02: + return kKeyKP_Enter; + + case 0x03: + return kKeyKP_Enter; + + case 0x04: + return kKeyEnd; + + case 0x05: + return kKeyHelp; + + case 0x08: + return kKeyBackSpace; + + case 0x09: + return kKeyTab; + + case 0x0b: + return kKeyPageUp; + + case 0x0c: + return kKeyPageDown; + + case 0x0d: + return kKeyReturn; + + case 0x10: + // OS X maps all the function keys (F1, etc) to this one key. + // we can't determine the right key here so we have to do it + // some other way. + return kKeyNone; + + case 0x1b: + return kKeyEscape; + + case 0x1c: + return kKeyLeft; + + case 0x1d: + return kKeyRight; + + case 0x1e: + return kKeyUp; + + case 0x1f: + return kKeyDown; + + case 0x7f: + return kKeyDelete; + + case 0x06: + case 0x07: + case 0x0a: + case 0x0e: + case 0x0f: + case 0x11: + case 0x12: + case 0x13: + case 0x14: + case 0x15: + case 0x16: + case 0x17: + case 0x18: + case 0x19: + case 0x1a: + // discard other control characters + return kKeyNone; + + default: + // not special or unknown + break; + } + + // create string with character + char str[2]; + str[0] = static_cast(c); + str[1] = 0; + +#if defined(MAC_OS_X_VERSION_10_5) + // get current keyboard script + TISInputSourceRef isref = TISCopyCurrentKeyboardInputSource(); + CFArrayRef langs = (CFArrayRef) TISGetInputSourceProperty(isref, kTISPropertyInputSourceLanguages); + CFStringEncoding encoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)CFArrayGetValueAtIndex(langs, 0)); +#else + CFStringEncoding encoding = GetScriptManagerVariable(smKeyScript); +#endif + // convert to unicode + CFStringRef cfString = + CFStringCreateWithCStringNoCopy( + kCFAllocatorDefault, str, encoding, kCFAllocatorNull); + + // sometimes CFStringCreate...() returns NULL (e.g. Apple Korean + // encoding with char value 214). if it did then make no key, + // otherwise CFStringCreateMutableCopy() will crash. + if (cfString == NULL) { + return kKeyNone; + } + + // convert to precomposed + CFMutableStringRef mcfString = + CFStringCreateMutableCopy(kCFAllocatorDefault, 0, cfString); + CFRelease(cfString); + CFStringNormalize(mcfString, kCFStringNormalizationFormC); + + // check result + int unicodeLength = CFStringGetLength(mcfString); + if (unicodeLength == 0) { + CFRelease(mcfString); + return kKeyNone; + } + if (unicodeLength > 1) { + // FIXME -- more than one character, we should handle this + CFRelease(mcfString); + return kKeyNone; + } + + // get unicode character + UniChar uc = CFStringGetCharacterAtIndex(mcfString, 0); + CFRelease(mcfString); + + // convert to KeyID + return static_cast(uc); + } +} + +KeyID +COSXKeyState::CKeyResource::unicharToKeyID(UniChar c) +{ + switch (c) { + case 3: + return kKeyKP_Enter; + + case 8: + return kKeyBackSpace; + + case 9: + return kKeyTab; + + case 13: + return kKeyReturn; + + case 27: + return kKeyEscape; + + case 127: + return kKeyDelete; + + default: + if (c < 32) { + return kKeyNone; + } + return static_cast(c); + } +} + + +// +// COSXKeyState::CUCHRKeyResource +// + +COSXKeyState::CUCHRKeyResource::CUCHRKeyResource(const void* resource, + UInt32 keyboardType) : + m_m(NULL), + m_cti(NULL), + m_sdi(NULL), + m_sri(NULL), + m_st(NULL) +{ + m_resource = reinterpret_cast(resource); + if (m_resource == NULL) { + return; + } + + // find the keyboard info for the current keyboard type + const UCKeyboardTypeHeader* th = NULL; + const UCKeyboardLayout* r = m_resource; + for (ItemCount i = 0; i < r->keyboardTypeCount; ++i) { + if (keyboardType >= r->keyboardTypeList[i].keyboardTypeFirst && + keyboardType <= r->keyboardTypeList[i].keyboardTypeLast) { + th = r->keyboardTypeList + i; + break; + } + if (r->keyboardTypeList[i].keyboardTypeFirst == 0) { + // found the default. use it unless we find a match. + th = r->keyboardTypeList + i; + } + } + if (th == NULL) { + // cannot find a suitable keyboard type + return; + } + + // get tables for keyboard type + const UInt8* base = reinterpret_cast(m_resource); + m_m = reinterpret_cast(base + + th->keyModifiersToTableNumOffset); + m_cti = reinterpret_cast(base + + th->keyToCharTableIndexOffset); + m_sdi = reinterpret_cast(base + + th->keySequenceDataIndexOffset); + if (th->keyStateRecordsIndexOffset != 0) { + m_sri = reinterpret_cast(base + + th->keyStateRecordsIndexOffset); + } + if (th->keyStateTerminatorsOffset != 0) { + m_st = reinterpret_cast(base + + th->keyStateTerminatorsOffset); + } + + // find the space key, but only if it can combine with dead keys. + // a dead key followed by a space yields the non-dead version of + // the dead key. + m_spaceOutput = 0xffffu; + UInt32 table = getTableForModifier(0); + for (UInt32 button = 0, n = getNumButtons(); button < n; ++button) { + KeyID id = getKey(table, button); + if (id == 0x20) { + UCKeyOutput c = + reinterpret_cast(base + + m_cti->keyToCharTableOffsets[table])[button]; + if ((c & kUCKeyOutputTestForIndexMask) == + kUCKeyOutputStateIndexMask) { + m_spaceOutput = (c & kUCKeyOutputGetIndexMask); + break; + } + } + } +} + +bool +COSXKeyState::CUCHRKeyResource::isValid() const +{ + return (m_m != NULL); +} + +UInt32 +COSXKeyState::CUCHRKeyResource::getNumModifierCombinations() const +{ + // only 32 (not 256) because the righthanded modifier bits are ignored + return 32; +} + +UInt32 +COSXKeyState::CUCHRKeyResource::getNumTables() const +{ + return m_cti->keyToCharTableCount; +} + +UInt32 +COSXKeyState::CUCHRKeyResource::getNumButtons() const +{ + return m_cti->keyToCharTableSize; +} + +UInt32 +COSXKeyState::CUCHRKeyResource::getTableForModifier(UInt32 mask) const +{ + if (mask >= m_m->modifiersCount) { + return m_m->defaultTableNum; + } + else { + return m_m->tableNum[mask]; + } +} + +KeyID +COSXKeyState::CUCHRKeyResource::getKey(UInt32 table, UInt32 button) const +{ + assert(table < getNumTables()); + assert(button < getNumButtons()); + + const UInt8* base = reinterpret_cast(m_resource); + const UCKeyOutput* cPtr = reinterpret_cast(base + + m_cti->keyToCharTableOffsets[table]); + + const UCKeyOutput c = cPtr[button]; + + KeySequence keys; + switch (c & kUCKeyOutputTestForIndexMask) { + case kUCKeyOutputStateIndexMask: + if (!getDeadKey(keys, c & kUCKeyOutputGetIndexMask)) { + return kKeyNone; + } + break; + + case kUCKeyOutputSequenceIndexMask: + default: + if (!addSequence(keys, c)) { + return kKeyNone; + } + break; + } + + // XXX -- no support for multiple characters + if (keys.size() != 1) { + return kKeyNone; + } + + return keys.front(); +} + +bool +COSXKeyState::CUCHRKeyResource::getDeadKey( + KeySequence& keys, UInt16 index) const +{ + if (m_sri == NULL || index >= m_sri->keyStateRecordCount) { + // XXX -- should we be using some other fallback? + return false; + } + + UInt16 state = 0; + if (!getKeyRecord(keys, index, state)) { + return false; + } + if (state == 0) { + // not a dead key + return true; + } + + // no dead keys if we couldn't find the space key + if (m_spaceOutput == 0xffffu) { + return false; + } + + // the dead key should not have put anything in the key list + if (!keys.empty()) { + return false; + } + + // get the character generated by pressing the space key after the + // dead key. if we're still in a compose state afterwards then we're + // confused so we bail. + if (!getKeyRecord(keys, m_spaceOutput, state) || state != 0) { + return false; + } + + // convert keys to their dead counterparts + for (KeySequence::iterator i = keys.begin(); i != keys.end(); ++i) { + *i = CKeyMap::getDeadKey(*i); + } + + return true; +} + +bool +COSXKeyState::CUCHRKeyResource::getKeyRecord( + KeySequence& keys, UInt16 index, UInt16& state) const +{ + const UInt8* base = reinterpret_cast(m_resource); + const UCKeyStateRecord* sr = + reinterpret_cast(base + + m_sri->keyStateRecordOffsets[index]); + const UCKeyStateEntryTerminal* kset = + reinterpret_cast(sr->stateEntryData); + + UInt16 nextState = 0; + bool found = false; + if (state == 0) { + found = true; + nextState = sr->stateZeroNextState; + if (!addSequence(keys, sr->stateZeroCharData)) { + return false; + } + } + else { + // we have a next entry + switch (sr->stateEntryFormat) { + case kUCKeyStateEntryTerminalFormat: + for (UInt16 j = 0; j < sr->stateEntryCount; ++j) { + if (kset[j].curState == state) { + if (!addSequence(keys, kset[j].charData)) { + return false; + } + nextState = 0; + found = true; + break; + } + } + break; + + case kUCKeyStateEntryRangeFormat: + // XXX -- not supported yet + break; + + default: + // XXX -- unknown format + return false; + } + } + if (!found) { + // use a terminator + if (m_st != NULL && state < m_st->keyStateTerminatorCount) { + if (!addSequence(keys, m_st->keyStateTerminators[state - 1])) { + return false; + } + } + nextState = sr->stateZeroNextState; + if (!addSequence(keys, sr->stateZeroCharData)) { + return false; + } + } + + // next + state = nextState; + + return true; +} + +bool +COSXKeyState::CUCHRKeyResource::addSequence( + KeySequence& keys, UCKeyCharSeq c) const +{ + if ((c & kUCKeyOutputTestForIndexMask) == kUCKeyOutputSequenceIndexMask) { + UInt16 index = (c & kUCKeyOutputGetIndexMask); + if (index < m_sdi->charSequenceCount && + m_sdi->charSequenceOffsets[index] != + m_sdi->charSequenceOffsets[index + 1]) { + // XXX -- sequences not supported yet + return false; + } + } + + if (c != 0xfffe && c != 0xffff) { + KeyID id = unicharToKeyID(c); + if (id != kKeyNone) { + keys.push_back(id); + } + } + + return true; +} diff --git a/src/lib/platform/COSXKeyState.h b/src/lib/platform/COSXKeyState.h new file mode 100644 index 00000000..0d90b1a0 --- /dev/null +++ b/src/lib/platform/COSXKeyState.h @@ -0,0 +1,225 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef COSXKEYSTATE_H +#define COSXKEYSTATE_H + +#include +#include "CKeyState.h" +#include "stdmap.h" +#include "stdset.h" +#include "stdvector.h" + +#if defined(MAC_OS_X_VERSION_10_5) + typedef TISInputSourceRef KeyLayout; +#else + typedef KeyboardLayoutRef KeyLayout; +#endif + +//! OS X key state +/*! +A key state for OS X. +*/ +class COSXKeyState : public CKeyState { +public: + typedef std::vector CKeyIDs; + + COSXKeyState(); + COSXKeyState(IEventQueue& eventQueue, CKeyMap& keyMap); + virtual ~COSXKeyState(); + + //! @name modifiers + //@{ + + //! Handle modifier key change + /*! + Determines which modifier keys have changed and updates the modifier + state and sends key events as appropriate. + */ + void handleModifierKeys(void* target, + KeyModifierMask oldMask, KeyModifierMask newMask); + + //@} + //! @name accessors + //@{ + + //! Convert OS X modifier mask to synergy mask + /*! + Returns the synergy modifier mask corresponding to the OS X modifier + mask in \p mask. + */ + KeyModifierMask mapModifiersFromOSX(UInt32 mask) const; + + //! Convert CG flags-style modifier mask to old-style Carbon + /*! + Still required in a few places for translation calls. + */ + KeyModifierMask mapModifiersToCarbon(UInt32 mask) const; + + //! Map key event to keys + /*! + Converts a key event into a sequence of KeyIDs and the shadow modifier + state to a modifier mask. The KeyIDs list, in order, the characters + generated by the key press/release. It returns the id of the button + that was pressed or released, or 0 if the button doesn't map to a known + KeyID. + */ + KeyButton mapKeyFromEvent(CKeyIDs& ids, + KeyModifierMask* maskOut, CGEventRef event) const; + + //! Map key and mask to native values + /*! + Calculates mac virtual key and mask for a key \p key and modifiers + \p mask. Returns \c true if the key can be mapped, \c false otherwise. + */ + bool mapSynergyHotKeyToMac(KeyID key, KeyModifierMask mask, + UInt32& macVirtualKey, + UInt32& macModifierMask) const; + + //@} + + // IKeyState overrides + virtual bool fakeCtrlAltDel(); + virtual KeyModifierMask + pollActiveModifiers() const; + virtual SInt32 pollActiveGroup() const; + virtual void pollPressedKeys(KeyButtonSet& pressedKeys) const; + +protected: + // CKeyState overrides + virtual void getKeyMap(CKeyMap& keyMap); + virtual void fakeKey(const Keystroke& keystroke); + +private: + class CKeyResource; + typedef std::vector GroupList; + + // Add hard coded special keys to a CKeyMap. + void getKeyMapForSpecialKeys( + CKeyMap& keyMap, SInt32 group) const; + + // Convert keyboard resource to a key map + bool getKeyMap(CKeyMap& keyMap, + SInt32 group, const CKeyResource& r) const; + + // Get the available keyboard groups + bool getGroups(GroupList&) const; + + // Change active keyboard group to group + void setGroup(SInt32 group); + + // Check if the keyboard layout has changed and update keyboard state + // if so. + void checkKeyboardLayout(); + + // Send an event for the given modifier key + void handleModifierKey(void* target, + UInt32 virtualKey, KeyID id, + bool down, KeyModifierMask newMask); + + // Checks if any in \p ids is a glyph key and if \p isCommand is false. + // If so it adds the AltGr modifier to \p mask. This allows OS X + // servers to use the option key both as AltGr and as a modifier. If + // option is acting as AltGr (i.e. it generates a glyph and there are + // no command modifiers active) then we don't send the super modifier + // to clients because they'd try to match it as a command modifier. + void adjustAltGrModifier(const CKeyIDs& ids, + KeyModifierMask* mask, bool isCommand) const; + + // Maps an OS X virtual key id to a KeyButton. This simply remaps + // the ids so we don't use KeyButton 0. + static KeyButton mapVirtualKeyToKeyButton(UInt32 keyCode); + + // Maps a KeyButton to an OS X key code. This is the inverse of + // mapVirtualKeyToKeyButton. + static UInt32 mapKeyButtonToVirtualKey(KeyButton keyButton); + + void init(); + +private: + class CKeyResource : public IInterface { + public: + virtual bool isValid() const = 0; + virtual UInt32 getNumModifierCombinations() const = 0; + virtual UInt32 getNumTables() const = 0; + virtual UInt32 getNumButtons() const = 0; + virtual UInt32 getTableForModifier(UInt32 mask) const = 0; + virtual KeyID getKey(UInt32 table, UInt32 button) const = 0; + + // Convert a character in the current script to the equivalent KeyID + static KeyID getKeyID(UInt8); + + // Convert a unicode character to the equivalent KeyID. + static KeyID unicharToKeyID(UniChar); + }; + + + class CUCHRKeyResource : public CKeyResource { + public: + CUCHRKeyResource(const void*, UInt32 keyboardType); + + // CKeyResource overrides + virtual bool isValid() const; + virtual UInt32 getNumModifierCombinations() const; + virtual UInt32 getNumTables() const; + virtual UInt32 getNumButtons() const; + virtual UInt32 getTableForModifier(UInt32 mask) const; + virtual KeyID getKey(UInt32 table, UInt32 button) const; + + private: + typedef std::vector KeySequence; + + bool getDeadKey(KeySequence& keys, UInt16 index) const; + bool getKeyRecord(KeySequence& keys, + UInt16 index, UInt16& state) const; + bool addSequence(KeySequence& keys, UCKeyCharSeq c) const; + + private: + const UCKeyboardLayout* m_resource; + const UCKeyModifiersToTableNum* m_m; + const UCKeyToCharTableIndex* m_cti; + const UCKeySequenceDataIndex* m_sdi; + const UCKeyStateRecordsIndex* m_sri; + const UCKeyStateTerminators* m_st; + UInt16 m_spaceOutput; + }; + + // OS X uses a physical key if 0 for the 'A' key. synergy reserves + // KeyButton 0 so we offset all OS X physical key ids by this much + // when used as a KeyButton and by minus this much to map a KeyButton + // to a physical button. + enum { + KeyButtonOffset = 1 + }; + + typedef std::map GroupMap; + typedef std::map CVirtualKeyMap; + + CVirtualKeyMap m_virtualKeyMap; + mutable UInt32 m_deadKeyState; + GroupList m_groups; + GroupMap m_groupMap; + + // Hold the current state of modifier keys + bool shiftPressed; + bool controlPressed; + bool altPressed; + bool superPressed; + bool capsPressed; +}; + +#endif diff --git a/src/lib/platform/COSXScreen.cpp b/src/lib/platform/COSXScreen.cpp new file mode 100644 index 00000000..7a734eb7 --- /dev/null +++ b/src/lib/platform/COSXScreen.cpp @@ -0,0 +1,1913 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "COSXScreen.h" +#include "COSXClipboard.h" +#include "COSXEventQueueBuffer.h" +#include "COSXKeyState.h" +#include "COSXScreenSaver.h" +#include "CClipboard.h" +#include "CKeyMap.h" +#include "CCondVar.h" +#include "CLock.h" +#include "CMutex.h" +#include "CThread.h" +#include "CLog.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" +#include "TMethodJob.h" +#include "XArch.h" + +#include + +#include +#include +#include + +// Set some enums for fast user switching if we're building with an SDK +// from before such support was added. +#if !defined(MAC_OS_X_VERSION_10_3) || \ + (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_3) +enum { + kEventClassSystem = 'macs', + kEventSystemUserSessionActivated = 10, + kEventSystemUserSessionDeactivated = 11 +}; +#endif + +// This isn't in any Apple SDK that I know of as of yet. +enum { + kSynergyEventMouseScroll = 11, + kSynergyMouseScrollAxisX = 'saxx', + kSynergyMouseScrollAxisY = 'saxy' +}; + +// +// COSXScreen +// + + + +bool COSXScreen::s_testedForGHOM = false; +bool COSXScreen::s_hasGHOM = false; +CEvent::Type COSXScreen::s_confirmSleepEvent = CEvent::kUnknown; + +COSXScreen::COSXScreen(bool isPrimary) : + MouseButtonEventMap(NumButtonIDs), + m_isPrimary(isPrimary), + m_isOnScreen(m_isPrimary), + m_cursorPosValid(false), + m_cursorHidden(false), + m_dragNumButtonsDown(0), + m_dragTimer(NULL), + m_keyState(NULL), + m_sequenceNumber(0), + m_screensaver(NULL), + m_screensaverNotify(false), + m_ownClipboard(false), + m_clipboardTimer(NULL), + m_hiddenWindow(NULL), + m_userInputWindow(NULL), + m_switchEventHandlerRef(0), + m_pmMutex(new CMutex), + m_pmWatchThread(NULL), + m_pmThreadReady(new CCondVar(m_pmMutex, false)), + m_activeModifierHotKey(0), + m_activeModifierHotKeyMask(0), + m_lastSingleClick(0), + m_lastDoubleClick(0), + m_lastSingleClickXCursor(0), + m_lastSingleClickYCursor(0) +{ + try { + m_displayID = CGMainDisplayID(); + updateScreenShape(m_displayID, 0); + m_screensaver = new COSXScreenSaver(getEventTarget()); + m_keyState = new COSXKeyState(); + + // TODO: http://stackoverflow.com/questions/2950124/enable-access-for-assistive-device-programmatically + if (m_isPrimary && !AXAPIEnabled()) + throw XArch("system setting not enabled: \"Enable access for assistive devices\""); + + // install display manager notification handler +#if defined(MAC_OS_X_VERSION_10_5) + CGDisplayRegisterReconfigurationCallback(displayReconfigurationCallback, this); +#else + m_displayManagerNotificationUPP = + NewDMExtendedNotificationUPP(displayManagerCallback); + OSStatus err = GetCurrentProcess(&m_PSN); + err = DMRegisterExtendedNotifyProc(m_displayManagerNotificationUPP, + this, 0, &m_PSN); +#endif + + // install fast user switching event handler + EventTypeSpec switchEventTypes[2]; + switchEventTypes[0].eventClass = kEventClassSystem; + switchEventTypes[0].eventKind = kEventSystemUserSessionDeactivated; + switchEventTypes[1].eventClass = kEventClassSystem; + switchEventTypes[1].eventKind = kEventSystemUserSessionActivated; + EventHandlerUPP switchEventHandler = + NewEventHandlerUPP(userSwitchCallback); + InstallApplicationEventHandler(switchEventHandler, 2, switchEventTypes, + this, &m_switchEventHandlerRef); + DisposeEventHandlerUPP(switchEventHandler); + + constructMouseButtonEventMap(); + + // watch for requests to sleep + EVENTQUEUE->adoptHandler(COSXScreen::getConfirmSleepEvent(), + getEventTarget(), + new TMethodEventJob(this, + &COSXScreen::handleConfirmSleep)); + + // create thread for monitoring system power state. + LOG((CLOG_DEBUG "starting watchSystemPowerThread")); + m_pmWatchThread = new CThread(new TMethodJob + (this, &COSXScreen::watchSystemPowerThread)); + } + catch (...) { + EVENTQUEUE->removeHandler(COSXScreen::getConfirmSleepEvent(), + getEventTarget()); + if (m_switchEventHandlerRef != 0) { + RemoveEventHandler(m_switchEventHandlerRef); + } +#if defined(MAC_OS_X_VERSION_10_5) + CGDisplayRemoveReconfigurationCallback(displayReconfigurationCallback, this); +#else + if (m_displayManagerNotificationUPP != NULL) { + DMRemoveExtendedNotifyProc(m_displayManagerNotificationUPP, + NULL, &m_PSN, 0); + } + + if (m_hiddenWindow) { + ReleaseWindow(m_hiddenWindow); + m_hiddenWindow = NULL; + } + + if (m_userInputWindow) { + ReleaseWindow(m_userInputWindow); + m_userInputWindow = NULL; + } +#endif + + delete m_keyState; + delete m_screensaver; + throw; + } + + // install event handlers + EVENTQUEUE->adoptHandler(CEvent::kSystem, IEventQueue::getSystemTarget(), + new TMethodEventJob(this, + &COSXScreen::handleSystemEvent)); + + // install the platform event queue + EVENTQUEUE->adoptBuffer(new COSXEventQueueBuffer); +} + +COSXScreen::~COSXScreen() +{ + disable(); + EVENTQUEUE->adoptBuffer(NULL); + EVENTQUEUE->removeHandler(CEvent::kSystem, IEventQueue::getSystemTarget()); + + if (m_pmWatchThread) { + // make sure the thread has setup the runloop. + { + CLock lock(m_pmMutex); + while (!(bool)*m_pmThreadReady) { + m_pmThreadReady->wait(); + } + } + + // now exit the thread's runloop and wait for it to exit + LOG((CLOG_DEBUG "stopping watchSystemPowerThread")); + CFRunLoopStop(m_pmRunloop); + m_pmWatchThread->wait(); + delete m_pmWatchThread; + m_pmWatchThread = NULL; + } + delete m_pmThreadReady; + delete m_pmMutex; + + EVENTQUEUE->removeHandler(COSXScreen::getConfirmSleepEvent(), + getEventTarget()); + + RemoveEventHandler(m_switchEventHandlerRef); + +#if defined(MAC_OS_X_VERSION_10_5) + CGDisplayRemoveReconfigurationCallback(displayReconfigurationCallback, this); +#else + DMRemoveExtendedNotifyProc(m_displayManagerNotificationUPP, + NULL, &m_PSN, 0); + + if (m_hiddenWindow) { + ReleaseWindow(m_hiddenWindow); + m_hiddenWindow = NULL; + } + + if (m_userInputWindow) { + ReleaseWindow(m_userInputWindow); + m_userInputWindow = NULL; + } +#endif + + delete m_keyState; + delete m_screensaver; +} + +void* +COSXScreen::getEventTarget() const +{ + return const_cast(this); +} + +bool +COSXScreen::getClipboard(ClipboardID, IClipboard* dst) const +{ + CClipboard::copy(dst, &m_pasteboard); + return true; +} + +void +COSXScreen::getShape(SInt32& x, SInt32& y, SInt32& w, SInt32& h) const +{ + x = m_x; + y = m_y; + w = m_w; + h = m_h; +} + +void +COSXScreen::getCursorPos(SInt32& x, SInt32& y) const +{ + Point mouse; + GetGlobalMouse(&mouse); + x = mouse.h; + y = mouse.v; + m_cursorPosValid = true; + m_xCursor = x; + m_yCursor = y; +} + +void +COSXScreen::reconfigure(UInt32) +{ + // do nothing +} + +void +COSXScreen::warpCursor(SInt32 x, SInt32 y) +{ + // move cursor without generating events + CGPoint pos; + pos.x = x; + pos.y = y; + CGWarpMouseCursorPosition(pos); + + // save new cursor position + m_xCursor = x; + m_yCursor = y; + m_cursorPosValid = true; +} + +void +COSXScreen::fakeInputBegin() +{ + // FIXME -- not implemented +} + +void +COSXScreen::fakeInputEnd() +{ + // FIXME -- not implemented +} + +SInt32 +COSXScreen::getJumpZoneSize() const +{ + return 1; +} + +bool +COSXScreen::isAnyMouseButtonDown() const +{ + return (GetCurrentButtonState() != 0); +} + +void +COSXScreen::getCursorCenter(SInt32& x, SInt32& y) const +{ + x = m_xCenter; + y = m_yCenter; +} + +UInt32 +COSXScreen::registerHotKey(KeyID key, KeyModifierMask mask) +{ + // get mac virtual key and modifier mask matching synergy key and mask + UInt32 macKey, macMask; + if (!m_keyState->mapSynergyHotKeyToMac(key, mask, macKey, macMask)) { + LOG((CLOG_DEBUG "could not map hotkey id=%04x mask=%04x", key, mask)); + return 0; + } + + // choose hotkey id + UInt32 id; + if (!m_oldHotKeyIDs.empty()) { + id = m_oldHotKeyIDs.back(); + m_oldHotKeyIDs.pop_back(); + } + else { + id = m_hotKeys.size() + 1; + } + + // if this hot key has modifiers only then we'll handle it specially + EventHotKeyRef ref = NULL; + bool okay; + if (key == kKeyNone) { + if (m_modifierHotKeys.count(mask) > 0) { + // already registered + okay = false; + } + else { + m_modifierHotKeys[mask] = id; + okay = true; + } + } + else { + EventHotKeyID hkid = { 'SNRG', (UInt32)id }; + OSStatus status = RegisterEventHotKey(macKey, macMask, hkid, + GetApplicationEventTarget(), 0, + &ref); + okay = (status == noErr); + m_hotKeyToIDMap[CHotKeyItem(macKey, macMask)] = id; + } + + if (!okay) { + m_oldHotKeyIDs.push_back(id); + m_hotKeyToIDMap.erase(CHotKeyItem(macKey, macMask)); + LOG((CLOG_WARN "failed to register hotkey %s (id=%04x mask=%04x)", CKeyMap::formatKey(key, mask).c_str(), key, mask)); + return 0; + } + + m_hotKeys.insert(std::make_pair(id, CHotKeyItem(ref, macKey, macMask))); + + LOG((CLOG_DEBUG "registered hotkey %s (id=%04x mask=%04x) as id=%d", CKeyMap::formatKey(key, mask).c_str(), key, mask, id)); + return id; +} + +void +COSXScreen::unregisterHotKey(UInt32 id) +{ + // look up hotkey + HotKeyMap::iterator i = m_hotKeys.find(id); + if (i == m_hotKeys.end()) { + return; + } + + // unregister with OS + bool okay; + if (i->second.getRef() != NULL) { + okay = (UnregisterEventHotKey(i->second.getRef()) == noErr); + } + else { + okay = false; + // XXX -- this is inefficient + for (ModifierHotKeyMap::iterator j = m_modifierHotKeys.begin(); + j != m_modifierHotKeys.end(); ++j) { + if (j->second == id) { + m_modifierHotKeys.erase(j); + okay = true; + break; + } + } + } + if (!okay) { + LOG((CLOG_WARN "failed to unregister hotkey id=%d", id)); + } + else { + LOG((CLOG_DEBUG "unregistered hotkey id=%d", id)); + } + + // discard hot key from map and record old id for reuse + m_hotKeyToIDMap.erase(i->second); + m_hotKeys.erase(i); + m_oldHotKeyIDs.push_back(id); + if (m_activeModifierHotKey == id) { + m_activeModifierHotKey = 0; + m_activeModifierHotKeyMask = 0; + } +} + +void +COSXScreen::constructMouseButtonEventMap() +{ + const CGEventType source[NumButtonIDs][3] = { + kCGEventLeftMouseUp,kCGEventLeftMouseDragged,kCGEventLeftMouseDown, + kCGEventOtherMouseUp,kCGEventOtherMouseDragged,kCGEventOtherMouseDown, + kCGEventRightMouseUp,kCGEventRightMouseDragged,kCGEventRightMouseDown, + kCGEventOtherMouseUp,kCGEventOtherMouseDragged,kCGEventOtherMouseDown, + kCGEventOtherMouseUp,kCGEventOtherMouseDragged,kCGEventOtherMouseDown}; + + for (UInt16 button = 0; button < NumButtonIDs; button++) { + MouseButtonEventMapType new_map; + for (UInt16 state = (UInt32) kMouseButtonUp; state < kMouseButtonStateMax; state++) { + CGEventType curEvent = source[button][state]; + new_map[state] = curEvent; + } + MouseButtonEventMap[button] = new_map; + } +} + +void +COSXScreen::postMouseEvent(CGPoint& pos) const +{ + // check if cursor position is valid on the client display configuration + // stkamp@users.sourceforge.net + CGDisplayCount displayCount = 0; + CGGetDisplaysWithPoint(pos, 0, NULL, &displayCount); + if (displayCount == 0) { + // cursor position invalid - clamp to bounds of last valid display. + // find the last valid display using the last cursor position. + displayCount = 0; + CGDirectDisplayID displayID; + CGGetDisplaysWithPoint(CGPointMake(m_xCursor, m_yCursor), 1, + &displayID, &displayCount); + if (displayCount != 0) { + CGRect displayRect = CGDisplayBounds(displayID); + if (pos.x < displayRect.origin.x) { + pos.x = displayRect.origin.x; + } + else if (pos.x > displayRect.origin.x + + displayRect.size.width - 1) { + pos.x = displayRect.origin.x + displayRect.size.width - 1; + } + if (pos.y < displayRect.origin.y) { + pos.y = displayRect.origin.y; + } + else if (pos.y > displayRect.origin.y + + displayRect.size.height - 1) { + pos.y = displayRect.origin.y + displayRect.size.height - 1; + } + } + } + + CGEventType type = kCGEventMouseMoved; + + SInt8 button = m_buttonState.getFirstButtonDown(); + if (button != -1) { + MouseButtonEventMapType thisButtonType = MouseButtonEventMap[button]; + type = thisButtonType[kMouseButtonDragged]; + } + + CGEventRef event = CGEventCreateMouseEvent(NULL, type, pos, button); + CGEventPost(kCGHIDEventTap, event); + + CFRelease(event); +} + +void +COSXScreen::fakeMouseButton(ButtonID id, bool press) +{ + NXEventHandle handle = NXOpenEventStatus(); + double clickTime = NXClickTime(handle); + + if ((ARCH->time() - m_lastDoubleClick) <= clickTime) { + // drop all down and up fakes immedately after a double click. + // TODO: perhaps there is a better way to do this, usually in + // finder, if you tripple click a folder, it will open it and + // then select a folder under the cursor -- and perhaps other + // strange behaviour might happen? + LOG((CLOG_DEBUG1 "dropping mouse button %s", + press ? "press" : "release")); + return; + } + + // Buttons are indexed from one, but the button down array is indexed from zero + UInt32 index = id - kButtonLeft; + if (index >= NumButtonIDs) { + return; + } + + CGPoint pos; + if (!m_cursorPosValid) { + SInt32 x, y; + getCursorPos(x, y); + } + pos.x = m_xCursor; + pos.y = m_yCursor; + + // variable used to detect mouse coordinate differences between + // old & new mouse clicks. Used in double click detection. + SInt32 xDiff = m_xCursor - m_lastSingleClickXCursor; + SInt32 yDiff = m_yCursor - m_lastSingleClickYCursor; + double diff = sqrt(xDiff * xDiff + yDiff * yDiff); + // max sqrt(x^2 + y^2) difference allowed to double click + // since we don't have double click distance in NX APIs + // we define our own defaults. + const double maxDiff = sqrt(2) + 0.0001; + + if (press && (id == kButtonLeft) && + ((ARCH->time() - m_lastSingleClick) <= clickTime) && + diff <= maxDiff) { + + LOG((CLOG_DEBUG1 "faking mouse left double click")); + + // finder does not seem to detect double clicks from two separate + // CGEventCreateMouseEvent calls. so, if we detect a double click we + // use CGEventSetIntegerValueField to tell the OS. + // + // the caveat here is that findor will see this as a single click + // followed by a double click (even though there should be only a + // double click). this may cause weird behaviour in other apps. + // + // for some reason using the old CGPostMouseEvent function, doesn't + // cause double clicks (though i'm sure it did work at some point). + + CGEventRef event = CGEventCreateMouseEvent( + NULL, kCGEventLeftMouseDown, pos, kCGMouseButtonLeft); + + CGEventSetIntegerValueField(event, kCGMouseEventClickState, 2); + m_buttonState.set(index, kMouseButtonDown); + CGEventPost(kCGHIDEventTap, event); + + CGEventSetType(event, kCGEventLeftMouseUp); + m_buttonState.set(index, kMouseButtonUp); + CGEventPost(kCGHIDEventTap, event); + + CFRelease(event); + + m_lastDoubleClick = ARCH->time(); + } + else { + + // ... otherwise, perform a single press or release as normal. + + MouseButtonState state = press ? kMouseButtonDown : kMouseButtonUp; + + LOG((CLOG_DEBUG1 "faking mouse button %s", press ? "press" : "release")); + + MouseButtonEventMapType thisButtonMap = MouseButtonEventMap[index]; + CGEventType type = thisButtonMap[state]; + + CGEventRef event = CGEventCreateMouseEvent(NULL, type, pos, index); + + m_buttonState.set(index, state); + CGEventPost(kCGHIDEventTap, event); + + CFRelease(event); + + m_lastSingleClick = ARCH->time(); + m_lastSingleClickXCursor = m_xCursor; + m_lastSingleClickYCursor = m_yCursor; + } +} + +void +COSXScreen::fakeMouseMove(SInt32 x, SInt32 y) const +{ + // synthesize event + CGPoint pos; + pos.x = x; + pos.y = y; + postMouseEvent(pos); + + // save new cursor position + m_xCursor = static_cast(pos.x); + m_yCursor = static_cast(pos.y); + m_cursorPosValid = true; +} + +void +COSXScreen::fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const +{ + // OS X does not appear to have a fake relative mouse move function. + // simulate it by getting the current mouse position and adding to + // that. this can yield the wrong answer but there's not much else + // we can do. + + // get current position + Point oldPos; + GetGlobalMouse(&oldPos); + + // synthesize event + CGPoint pos; + m_xCursor = static_cast(oldPos.h); + m_yCursor = static_cast(oldPos.v); + pos.x = oldPos.h + dx; + pos.y = oldPos.v + dy; + postMouseEvent(pos); + + // we now assume we don't know the current cursor position + m_cursorPosValid = false; +} + +void +COSXScreen::fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const +{ + if (xDelta != 0 || yDelta != 0) { +#if defined(MAC_OS_X_VERSION_10_5) + // create a scroll event, post it and release it. not sure if kCGScrollEventUnitLine + // is the right choice here over kCGScrollEventUnitPixel + CGEventRef scrollEvent = CGEventCreateScrollWheelEvent( + NULL, kCGScrollEventUnitLine, 2, + mapScrollWheelFromSynergy(yDelta), + -mapScrollWheelFromSynergy(xDelta)); + + CGEventPost(kCGHIDEventTap, scrollEvent); + CFRelease(scrollEvent); +#else + + CGPostScrollWheelEvent( + 2, mapScrollWheelFromSynergy(yDelta), + -mapScrollWheelFromSynergy(xDelta)); +#endif + } +} + +void +COSXScreen::showCursor() +{ + LOG((CLOG_DEBUG "showing cursor")); + CGDisplayShowCursor(m_displayID); + CFStringRef propertyString = CFStringCreateWithCString(NULL, "SetsCursorInBackground", kCFStringEncodingMacRoman); + CGSSetConnectionProperty(_CGSDefaultConnection(), _CGSDefaultConnection(), propertyString, kCFBooleanFalse); + CFRelease(propertyString); +} + +void +COSXScreen::hideCursor() +{ + CFStringRef propertyString = CFStringCreateWithCString(NULL, "SetsCursorInBackground", kCFStringEncodingMacRoman); + CGSSetConnectionProperty(_CGSDefaultConnection(), _CGSDefaultConnection(), propertyString, kCFBooleanTrue); + CFRelease(propertyString); + + LOG((CLOG_DEBUG "hiding cursor")); + CGDisplayHideCursor(m_displayID); +} + +void +COSXScreen::enable() +{ + // watch the clipboard + m_clipboardTimer = EVENTQUEUE->newTimer(1.0, NULL); + EVENTQUEUE->adoptHandler(CEvent::kTimer, m_clipboardTimer, + new TMethodEventJob(this, + &COSXScreen::handleClipboardCheck)); + + if (m_isPrimary) { + // FIXME -- start watching jump zones + + // kCGEventTapOptionDefault = 0x00000000 (Missing in 10.4, so specified literally) + m_eventTapPort=CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, 0, + kCGEventMaskForAllEvents, + handleCGInputEvent, + this); + } + else { + // FIXME -- prevent system from entering power save mode + + // hide cursor + if (!m_cursorHidden) { + hideCursor(); + m_cursorHidden = true; + } + + // warp the mouse to the cursor center + fakeMouseMove(m_xCenter, m_yCenter); + + // there may be a better way to do this, but we register an event handler even if we're + // not on the primary display (acting as a client). This way, if a local event comes in + // (either keyboard or mouse), we can make sure to show the cursor if we've hidden it. + m_eventTapPort=CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, 0, + kCGEventMaskForAllEvents, + handleCGInputEventSecondary, + this); + } + if(!m_eventTapPort) { + LOG((CLOG_ERR "failed to create quartz event tap")); + } + m_eventTapRLSR=CFMachPortCreateRunLoopSource(kCFAllocatorDefault, m_eventTapPort, 0); + if(!m_eventTapRLSR) { + LOG((CLOG_ERR "failed to create a CFRunLoopSourceRef for the quartz event tap")); + } + CFRunLoopAddSource(CFRunLoopGetCurrent(), m_eventTapRLSR, kCFRunLoopDefaultMode); +} + +void +COSXScreen::disable() +{ + // show cursor if hidden + if (m_cursorHidden) { + showCursor(); + m_cursorHidden = false; + } + + // FIXME -- stop watching jump zones, stop capturing input + + if(m_eventTapRLSR) { + CFRelease(m_eventTapRLSR); + m_eventTapRLSR=NULL; + } + if(m_eventTapPort) { + CFRelease(m_eventTapPort); + m_eventTapPort=NULL; + } + // FIXME -- allow system to enter power saving mode + + // disable drag handling + m_dragNumButtonsDown = 0; + enableDragTimer(false); + + // uninstall clipboard timer + if (m_clipboardTimer != NULL) { + EVENTQUEUE->removeHandler(CEvent::kTimer, m_clipboardTimer); + EVENTQUEUE->deleteTimer(m_clipboardTimer); + m_clipboardTimer = NULL; + } + + m_isOnScreen = m_isPrimary; +} + +void +COSXScreen::enter() +{ + // show cursor + if (m_cursorHidden) { + showCursor(); + m_cursorHidden = false; + } + if (m_isPrimary) { + CGSetLocalEventsSuppressionInterval(0.0); + + // enable global hotkeys + //setGlobalHotKeysEnabled(true); + } + else { + // reset buttons + m_buttonState.reset(); + + // avoid suppression of local hardware events + // stkamp@users.sourceforge.net + CGSetLocalEventsFilterDuringSupressionState( + kCGEventFilterMaskPermitAllEvents, + kCGEventSupressionStateSupressionInterval); + CGSetLocalEventsFilterDuringSupressionState( + (kCGEventFilterMaskPermitLocalKeyboardEvents | + kCGEventFilterMaskPermitSystemDefinedEvents), + kCGEventSupressionStateRemoteMouseDrag); + } + + // now on screen + m_isOnScreen = true; +} + +bool +COSXScreen::leave() +{ + // hide cursor + if (!m_cursorHidden) { + hideCursor(); + m_cursorHidden = true; + } + + if (m_isPrimary) { + // warp to center + warpCursor(m_xCenter, m_yCenter); + + // This used to be necessary to get smooth mouse motion on other screens, + // but now is just to avoid a hesitating cursor when transitioning to + // the primary (this) screen. + CGSetLocalEventsSuppressionInterval(0.0001); + + // disable global hotkeys + //setGlobalHotKeysEnabled(false); + } + else { + // warp the mouse to the cursor center + fakeMouseMove(m_xCenter, m_yCenter); + + // FIXME -- prepare to show cursor if it moves + + // take keyboard focus + // FIXME + } + + // now off screen + m_isOnScreen = false; + + return true; +} + +bool +COSXScreen::setClipboard(ClipboardID, const IClipboard* src) +{ + if(src != NULL) { + LOG((CLOG_DEBUG "setting clipboard")); + CClipboard::copy(&m_pasteboard, src); + } + return true; +} + +void +COSXScreen::checkClipboards() +{ + LOG((CLOG_DEBUG2 "checking clipboard")); + if (m_pasteboard.synchronize()) { + LOG((CLOG_DEBUG "clipboard changed")); + sendClipboardEvent(getClipboardGrabbedEvent(), kClipboardClipboard); + sendClipboardEvent(getClipboardGrabbedEvent(), kClipboardSelection); + } +} + +void +COSXScreen::openScreensaver(bool notify) +{ + m_screensaverNotify = notify; + if (!m_screensaverNotify) { + m_screensaver->disable(); + } +} + +void +COSXScreen::closeScreensaver() +{ + if (!m_screensaverNotify) { + m_screensaver->enable(); + } +} + +void +COSXScreen::screensaver(bool activate) +{ + if (activate) { + m_screensaver->activate(); + } + else { + m_screensaver->deactivate(); + } +} + +void +COSXScreen::resetOptions() +{ + // no options +} + +void +COSXScreen::setOptions(const COptionsList&) +{ + // no options +} + +void +COSXScreen::setSequenceNumber(UInt32 seqNum) +{ + m_sequenceNumber = seqNum; +} + +bool +COSXScreen::isPrimary() const +{ + return m_isPrimary; +} + +void +COSXScreen::sendEvent(CEvent::Type type, void* data) const +{ + EVENTQUEUE->addEvent(CEvent(type, getEventTarget(), data)); +} + +void +COSXScreen::sendClipboardEvent(CEvent::Type type, ClipboardID id) const +{ + CClipboardInfo* info = (CClipboardInfo*)malloc(sizeof(CClipboardInfo)); + info->m_id = id; + info->m_sequenceNumber = m_sequenceNumber; + sendEvent(type, info); +} + +void +COSXScreen::handleSystemEvent(const CEvent& event, void*) +{ + EventRef* carbonEvent = reinterpret_cast(event.getData()); + assert(carbonEvent != NULL); + + UInt32 eventClass = GetEventClass(*carbonEvent); + + switch (eventClass) { + case kEventClassMouse: + switch (GetEventKind(*carbonEvent)) { + case kSynergyEventMouseScroll: + { + OSStatus r; + long xScroll; + long yScroll; + + // get scroll amount + r = GetEventParameter(*carbonEvent, + kSynergyMouseScrollAxisX, + typeLongInteger, + NULL, + sizeof(xScroll), + NULL, + &xScroll); + if (r != noErr) { + xScroll = 0; + } + r = GetEventParameter(*carbonEvent, + kSynergyMouseScrollAxisY, + typeLongInteger, + NULL, + sizeof(yScroll), + NULL, + &yScroll); + if (r != noErr) { + yScroll = 0; + } + + if (xScroll != 0 || yScroll != 0) { + onMouseWheel(-mapScrollWheelToSynergy(xScroll), + mapScrollWheelToSynergy(yScroll)); + } + } + } + break; + + case kEventClassKeyboard: + switch (GetEventKind(*carbonEvent)) { + case kEventHotKeyPressed: + case kEventHotKeyReleased: + onHotKey(*carbonEvent); + break; + } + + break; + + case kEventClassWindow: + SendEventToWindow(*carbonEvent, m_userInputWindow); + switch (GetEventKind(*carbonEvent)) { + case kEventWindowActivated: + LOG((CLOG_DEBUG1 "window activated")); + break; + + case kEventWindowDeactivated: + LOG((CLOG_DEBUG1 "window deactivated")); + break; + + case kEventWindowFocusAcquired: + LOG((CLOG_DEBUG1 "focus acquired")); + break; + + case kEventWindowFocusRelinquish: + LOG((CLOG_DEBUG1 "focus released")); + break; + } + break; + + default: + SendEventToEventTarget(*carbonEvent, GetEventDispatcherTarget()); + break; + } +} + +bool +COSXScreen::onMouseMove(SInt32 mx, SInt32 my) +{ + LOG((CLOG_DEBUG2 "mouse move %+d,%+d", mx, my)); + + SInt32 x = mx - m_xCursor; + SInt32 y = my - m_yCursor; + + if ((x == 0 && y == 0) || (mx == m_xCenter && mx == m_yCenter)) { + return true; + } + + // save position to compute delta of next motion + m_xCursor = mx; + m_yCursor = my; + + if (m_isOnScreen) { + // motion on primary screen + sendEvent(getMotionOnPrimaryEvent(), + CMotionInfo::alloc(m_xCursor, m_yCursor)); + } + else { + // motion on secondary screen. warp mouse back to + // center. + warpCursor(m_xCenter, m_yCenter); + + // examine the motion. if it's about the distance + // from the center of the screen to an edge then + // it's probably a bogus motion that we want to + // ignore (see warpCursorNoFlush() for a further + // description). + static SInt32 bogusZoneSize = 10; + if (-x + bogusZoneSize > m_xCenter - m_x || + x + bogusZoneSize > m_x + m_w - m_xCenter || + -y + bogusZoneSize > m_yCenter - m_y || + y + bogusZoneSize > m_y + m_h - m_yCenter) { + LOG((CLOG_DEBUG "dropped bogus motion %+d,%+d", x, y)); + } + else { + // send motion + sendEvent(getMotionOnSecondaryEvent(), CMotionInfo::alloc(x, y)); + } + } + + return true; +} + +bool +COSXScreen::onMouseButton(bool pressed, UInt16 macButton) +{ + // Buttons 2 and 3 are inverted on the mac + ButtonID button = mapMacButtonToSynergy(macButton); + + if (pressed) { + LOG((CLOG_DEBUG1 "event: button press button=%d", button)); + if (button != kButtonNone) { + KeyModifierMask mask = m_keyState->getActiveModifiers(); + sendEvent(getButtonDownEvent(), CButtonInfo::alloc(button, mask)); + } + } + else { + LOG((CLOG_DEBUG1 "event: button release button=%d", button)); + if (button != kButtonNone) { + KeyModifierMask mask = m_keyState->getActiveModifiers(); + sendEvent(getButtonUpEvent(), CButtonInfo::alloc(button, mask)); + } + } + + // handle drags with any button other than button 1 or 2 + if (macButton > 2) { + if (pressed) { + // one more button + if (m_dragNumButtonsDown++ == 0) { + enableDragTimer(true); + } + } + else { + // one less button + if (--m_dragNumButtonsDown == 0) { + enableDragTimer(false); + } + } + } + + return true; +} + +bool +COSXScreen::onMouseWheel(SInt32 xDelta, SInt32 yDelta) const +{ + LOG((CLOG_DEBUG1 "event: button wheel delta=%+d,%+d", xDelta, yDelta)); + sendEvent(getWheelEvent(), CWheelInfo::alloc(xDelta, yDelta)); + return true; +} + +void +COSXScreen::handleClipboardCheck(const CEvent&, void*) +{ + checkClipboards(); +} + +#if !defined(MAC_OS_X_VERSION_10_5) +pascal void +COSXScreen::displayManagerCallback(void* inUserData, SInt16 inMessage, void*) +{ + COSXScreen* screen = (COSXScreen*)inUserData; + + if (inMessage == kDMNotifyEvent) { + screen->onDisplayChange(); + } +} + +bool +COSXScreen::onDisplayChange() +{ + // screen resolution may have changed. save old shape. + SInt32 xOld = m_x, yOld = m_y, wOld = m_w, hOld = m_h; + + // update shape + updateScreenShape(); + + // do nothing if resolution hasn't changed + if (xOld != m_x || yOld != m_y || wOld != m_w || hOld != m_h) { + if (m_isPrimary) { + // warp mouse to center if off screen + if (!m_isOnScreen) { + warpCursor(m_xCenter, m_yCenter); + } + } + + // send new screen info + sendEvent(getShapeChangedEvent()); + } + + return true; +} +#else +void +COSXScreen::displayReconfigurationCallback(CGDirectDisplayID displayID, CGDisplayChangeSummaryFlags flags, void* inUserData) +{ + COSXScreen* screen = (COSXScreen*)inUserData; + + // Closing or opening the lid when an external monitor is + // connected causes an kCGDisplayBeginConfigurationFlag event + CGDisplayChangeSummaryFlags mask = kCGDisplayBeginConfigurationFlag | kCGDisplayMovedFlag | + kCGDisplaySetModeFlag | kCGDisplayAddFlag | kCGDisplayRemoveFlag | + kCGDisplayEnabledFlag | kCGDisplayDisabledFlag | + kCGDisplayMirrorFlag | kCGDisplayUnMirrorFlag | + kCGDisplayDesktopShapeChangedFlag; + + LOG((CLOG_DEBUG1 "event: display was reconfigured: %x %x %x", flags, mask, flags & mask)); + + if (flags & mask) { /* Something actually did change */ + + LOG((CLOG_DEBUG1 "event: screen changed shape; refreshing dimensions")); + screen->updateScreenShape(displayID, flags); + } +} +#endif + +bool +COSXScreen::onKey(CGEventRef event) +{ + CGEventType eventKind = CGEventGetType(event); + + // get the key and active modifiers + UInt32 virtualKey = CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode); + CGEventFlags macMask = CGEventGetFlags(event); + LOG((CLOG_DEBUG1 "event: Key event kind: %d, keycode=%d", eventKind, virtualKey)); + + // Special handling to track state of modifiers + if (eventKind == kCGEventFlagsChanged) { + // get old and new modifier state + KeyModifierMask oldMask = getActiveModifiers(); + KeyModifierMask newMask = m_keyState->mapModifiersFromOSX(macMask); + m_keyState->handleModifierKeys(getEventTarget(), oldMask, newMask); + + // if the current set of modifiers exactly matches a modifiers-only + // hot key then generate a hot key down event. + if (m_activeModifierHotKey == 0) { + if (m_modifierHotKeys.count(newMask) > 0) { + m_activeModifierHotKey = m_modifierHotKeys[newMask]; + m_activeModifierHotKeyMask = newMask; + EVENTQUEUE->addEvent(CEvent(getHotKeyDownEvent(), + getEventTarget(), + CHotKeyInfo::alloc(m_activeModifierHotKey))); + } + } + + // if a modifiers-only hot key is active and should no longer be + // then generate a hot key up event. + else if (m_activeModifierHotKey != 0) { + KeyModifierMask mask = (newMask & m_activeModifierHotKeyMask); + if (mask != m_activeModifierHotKeyMask) { + EVENTQUEUE->addEvent(CEvent(getHotKeyUpEvent(), + getEventTarget(), + CHotKeyInfo::alloc(m_activeModifierHotKey))); + m_activeModifierHotKey = 0; + m_activeModifierHotKeyMask = 0; + } + } + + return true; + } + + // check for hot key. when we're on a secondary screen we disable + // all hotkeys so we can capture the OS defined hot keys as regular + // keystrokes but that means we don't get our own hot keys either. + // so we check for a key/modifier match in our hot key map. + if (!m_isOnScreen) { + HotKeyToIDMap::const_iterator i = + m_hotKeyToIDMap.find(CHotKeyItem(virtualKey, + m_keyState->mapModifiersToCarbon(macMask) + & 0xff00u)); + if (i != m_hotKeyToIDMap.end()) { + UInt32 id = i->second; + + // determine event type + CEvent::Type type; + //UInt32 eventKind = GetEventKind(event); + if (eventKind == kCGEventKeyDown) { + type = getHotKeyDownEvent(); + } + else if (eventKind == kCGEventKeyUp) { + type = getHotKeyUpEvent(); + } + else { + return false; + } + + EVENTQUEUE->addEvent(CEvent(type, getEventTarget(), + CHotKeyInfo::alloc(id))); + + return true; + } + } + + // decode event type + bool down = (eventKind == kCGEventKeyDown); + bool up = (eventKind == kCGEventKeyUp); + bool isRepeat = (CGEventGetIntegerValueField(event, kCGKeyboardEventAutorepeat) == 1); + + // map event to keys + KeyModifierMask mask; + COSXKeyState::CKeyIDs keys; + KeyButton button = m_keyState->mapKeyFromEvent(keys, &mask, event); + if (button == 0) { + return false; + } + + // check for AltGr in mask. if set we send neither the AltGr nor + // the super modifiers to clients then remove AltGr before passing + // the modifiers to onKey. + KeyModifierMask sendMask = (mask & ~KeyModifierAltGr); + if ((mask & KeyModifierAltGr) != 0) { + sendMask &= ~KeyModifierSuper; + } + mask &= ~KeyModifierAltGr; + + // update button state + if (down) { + m_keyState->onKey(button, true, mask); + } + else if (up) { + if (!m_keyState->isKeyDown(button)) { + // up event for a dead key. throw it away. + return false; + } + m_keyState->onKey(button, false, mask); + } + + // send key events + for (COSXKeyState::CKeyIDs::const_iterator i = keys.begin(); + i != keys.end(); ++i) { + m_keyState->sendKeyEvent(getEventTarget(), down, isRepeat, + *i, sendMask, 1, button); + } + + return true; +} + +bool +COSXScreen::onHotKey(EventRef event) const +{ + // get the hotkey id + EventHotKeyID hkid; + GetEventParameter(event, kEventParamDirectObject, typeEventHotKeyID, + NULL, sizeof(EventHotKeyID), NULL, &hkid); + UInt32 id = hkid.id; + + // determine event type + CEvent::Type type; + UInt32 eventKind = GetEventKind(event); + if (eventKind == kEventHotKeyPressed) { + type = getHotKeyDownEvent(); + } + else if (eventKind == kEventHotKeyReleased) { + type = getHotKeyUpEvent(); + } + else { + return false; + } + + EVENTQUEUE->addEvent(CEvent(type, getEventTarget(), + CHotKeyInfo::alloc(id))); + + return true; +} + +ButtonID +COSXScreen::mapMacButtonToSynergy(UInt16 macButton) const +{ + switch (macButton) { + case 1: + return kButtonLeft; + + case 2: + return kButtonRight; + + case 3: + return kButtonMiddle; + } + + return static_cast(macButton); +} + +SInt32 +COSXScreen::mapScrollWheelToSynergy(SInt32 x) const +{ + // return accelerated scrolling but not exponentially scaled as it is + // on the mac. + double d = (1.0 + getScrollSpeed()) * x / getScrollSpeedFactor(); + return static_cast(120.0 * d); +} + +SInt32 +COSXScreen::mapScrollWheelFromSynergy(SInt32 x) const +{ + // use server's acceleration with a little boost since other platforms + // take one wheel step as a larger step than the mac does. + return static_cast(3.0 * x / 120.0); +} + +double +COSXScreen::getScrollSpeed() const +{ + double scaling = 0.0; + + CFPropertyListRef pref = ::CFPreferencesCopyValue( + CFSTR("com.apple.scrollwheel.scaling") , + kCFPreferencesAnyApplication, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + if (pref != NULL) { + CFTypeID id = CFGetTypeID(pref); + if (id == CFNumberGetTypeID()) { + CFNumberRef value = static_cast(pref); + if (CFNumberGetValue(value, kCFNumberDoubleType, &scaling)) { + if (scaling < 0.0) { + scaling = 0.0; + } + } + } + CFRelease(pref); + } + + return scaling; +} + +double +COSXScreen::getScrollSpeedFactor() const +{ + return pow(10.0, getScrollSpeed()); +} + +void +COSXScreen::enableDragTimer(bool enable) +{ + UInt32 modifiers; + MouseTrackingResult res; + + if (enable && m_dragTimer == NULL) { + m_dragTimer = EVENTQUEUE->newTimer(0.01, NULL); + EVENTQUEUE->adoptHandler(CEvent::kTimer, m_dragTimer, + new TMethodEventJob(this, + &COSXScreen::handleDrag)); + TrackMouseLocationWithOptions(NULL, 0, 0, &m_dragLastPoint, &modifiers, &res); + } + else if (!enable && m_dragTimer != NULL) { + EVENTQUEUE->removeHandler(CEvent::kTimer, m_dragTimer); + EVENTQUEUE->deleteTimer(m_dragTimer); + m_dragTimer = NULL; + } +} + +void +COSXScreen::handleDrag(const CEvent&, void*) +{ + Point p; + UInt32 modifiers; + MouseTrackingResult res; + + TrackMouseLocationWithOptions(NULL, 0, 0, &p, &modifiers, &res); + + if (res != kMouseTrackingTimedOut && (p.h != m_dragLastPoint.h || p.v != m_dragLastPoint.v)) { + m_dragLastPoint = p; + onMouseMove((SInt32)p.h, (SInt32)p.v); + } +} + +void +COSXScreen::updateButtons() +{ + UInt32 buttons = GetCurrentButtonState(); + + m_buttonState.overwrite(buttons); +} + +IKeyState* +COSXScreen::getKeyState() const +{ + return m_keyState; +} + +void +COSXScreen::updateScreenShape(const CGDirectDisplayID, const CGDisplayChangeSummaryFlags flags) +{ + updateScreenShape(); +} + +void +COSXScreen::updateScreenShape() +{ + // get info for each display + CGDisplayCount displayCount = 0; + + if (CGGetActiveDisplayList(0, NULL, &displayCount) != CGDisplayNoErr) { + return; + } + + if (displayCount == 0) { + return; + } + + CGDirectDisplayID* displays = new CGDirectDisplayID[displayCount]; + if (displays == NULL) { + return; + } + + if (CGGetActiveDisplayList(displayCount, + displays, &displayCount) != CGDisplayNoErr) { + delete[] displays; + return; + } + + // get smallest rect enclosing all display rects + CGRect totalBounds = CGRectZero; + for (CGDisplayCount i = 0; i < displayCount; ++i) { + CGRect bounds = CGDisplayBounds(displays[i]); + totalBounds = CGRectUnion(totalBounds, bounds); + } + + // get shape of default screen + m_x = (SInt32)totalBounds.origin.x; + m_y = (SInt32)totalBounds.origin.y; + m_w = (SInt32)totalBounds.size.width; + m_h = (SInt32)totalBounds.size.height; + + // get center of default screen + CGDirectDisplayID main = CGMainDisplayID(); + const CGRect rect = CGDisplayBounds(main); + m_xCenter = (rect.origin.x + rect.size.width) / 2; + m_yCenter = (rect.origin.y + rect.size.height) / 2; + + delete[] displays; + // We want to notify the peer screen whether we are primary screen or not + sendEvent(getShapeChangedEvent()); + + LOG((CLOG_DEBUG "screen shape: center=%d,%d size=%dx%d on %u %s (%s)", + m_x, m_y, m_w, m_h, displayCount, + (displayCount == 1) ? "display" : "displays")); +} + +#pragma mark - + +// +// FAST USER SWITCH NOTIFICATION SUPPORT +// +// COSXScreen::userSwitchCallback(void*) +// +// gets called if a fast user switch occurs +// + +pascal OSStatus +COSXScreen::userSwitchCallback(EventHandlerCallRef nextHandler, + EventRef theEvent, + void* inUserData) +{ + COSXScreen* screen = (COSXScreen*)inUserData; + UInt32 kind = GetEventKind(theEvent); + + if (kind == kEventSystemUserSessionDeactivated) { + LOG((CLOG_DEBUG "user session deactivated")); + EVENTQUEUE->addEvent(CEvent(IScreen::getSuspendEvent(), + screen->getEventTarget())); + } + else if (kind == kEventSystemUserSessionActivated) { + LOG((CLOG_DEBUG "user session activated")); + EVENTQUEUE->addEvent(CEvent(IScreen::getResumeEvent(), + screen->getEventTarget())); + } + return (CallNextEventHandler(nextHandler, theEvent)); +} + +#pragma mark - + +// +// SLEEP/WAKEUP NOTIFICATION SUPPORT +// +// COSXScreen::watchSystemPowerThread(void*) +// +// main of thread monitoring system power (sleep/wakup) using a CFRunLoop +// + +void +COSXScreen::watchSystemPowerThread(void*) +{ + io_object_t notifier; + IONotificationPortRef notificationPortRef; + CFRunLoopSourceRef runloopSourceRef = 0; + + m_pmRunloop = CFRunLoopGetCurrent(); + // install system power change callback + m_pmRootPort = IORegisterForSystemPower(this, ¬ificationPortRef, + powerChangeCallback, ¬ifier); + if (m_pmRootPort == 0) { + LOG((CLOG_WARN "IORegisterForSystemPower failed")); + } + else { + runloopSourceRef = + IONotificationPortGetRunLoopSource(notificationPortRef); + CFRunLoopAddSource(m_pmRunloop, runloopSourceRef, + kCFRunLoopCommonModes); + } + + // thread is ready + { + CLock lock(m_pmMutex); + *m_pmThreadReady = true; + m_pmThreadReady->signal(); + } + + // if we were unable to initialize then exit. we must do this after + // setting m_pmThreadReady to true otherwise the parent thread will + // block waiting for it. + if (m_pmRootPort == 0) { + return; + } + + // start the run loop + LOG((CLOG_DEBUG "started watchSystemPowerThread")); + CFRunLoopRun(); + + // cleanup + if (notificationPortRef) { + CFRunLoopRemoveSource(m_pmRunloop, + runloopSourceRef, kCFRunLoopDefaultMode); + CFRunLoopSourceInvalidate(runloopSourceRef); + CFRelease(runloopSourceRef); + } + + CLock lock(m_pmMutex); + IODeregisterForSystemPower(¬ifier); + m_pmRootPort = 0; + LOG((CLOG_DEBUG "stopped watchSystemPowerThread")); +} + +void +COSXScreen::powerChangeCallback(void* refcon, io_service_t service, + natural_t messageType, void* messageArg) +{ + ((COSXScreen*)refcon)->handlePowerChangeRequest(messageType, messageArg); +} + +void +COSXScreen::handlePowerChangeRequest(natural_t messageType, void* messageArg) +{ + // we've received a power change notification + switch (messageType) { + case kIOMessageSystemWillSleep: + // COSXScreen has to handle this in the main thread so we have to + // queue a confirm sleep event here. we actually don't allow the + // system to sleep until the event is handled. + EVENTQUEUE->addEvent(CEvent(COSXScreen::getConfirmSleepEvent(), + getEventTarget(), messageArg, + CEvent::kDontFreeData)); + return; + + case kIOMessageSystemHasPoweredOn: + LOG((CLOG_DEBUG "system wakeup")); + EVENTQUEUE->addEvent(CEvent(IScreen::getResumeEvent(), + getEventTarget())); + break; + + default: + break; + } + + CLock lock(m_pmMutex); + if (m_pmRootPort != 0) { + IOAllowPowerChange(m_pmRootPort, (long)messageArg); + } +} + +CEvent::Type +COSXScreen::getConfirmSleepEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_confirmSleepEvent, + "COSXScreen::confirmSleep"); +} + +void +COSXScreen::handleConfirmSleep(const CEvent& event, void*) +{ + long messageArg = (long)event.getData(); + if (messageArg != 0) { + CLock lock(m_pmMutex); + if (m_pmRootPort != 0) { + // deliver suspend event immediately. + EVENTQUEUE->addEvent(CEvent(IScreen::getSuspendEvent(), + getEventTarget(), NULL, + CEvent::kDeliverImmediately)); + + LOG((CLOG_DEBUG "system will sleep")); + IOAllowPowerChange(m_pmRootPort, messageArg); + } + } +} + +#pragma mark - + +// +// GLOBAL HOTKEY OPERATING MODE SUPPORT (10.3) +// +// CoreGraphics private API (OSX 10.3) +// Source: http://ichiro.nnip.org/osx/Cocoa/GlobalHotkey.html +// +// We load the functions dynamically because they're not available in +// older SDKs. We don't use weak linking because we want users of +// older SDKs to build an app that works on newer systems and older +// SDKs will not provide the symbols. +// +// FIXME: This is hosed as of OS 10.5; patches to repair this are +// a good thing. +// +#if 0 + +#ifdef __cplusplus +extern "C" { +#endif + +typedef int CGSConnection; +typedef enum { + CGSGlobalHotKeyEnable = 0, + CGSGlobalHotKeyDisable = 1, +} CGSGlobalHotKeyOperatingMode; + +extern CGSConnection _CGSDefaultConnection(void) WEAK_IMPORT_ATTRIBUTE; +extern CGError CGSGetGlobalHotKeyOperatingMode(CGSConnection connection, CGSGlobalHotKeyOperatingMode *mode) WEAK_IMPORT_ATTRIBUTE; +extern CGError CGSSetGlobalHotKeyOperatingMode(CGSConnection connection, CGSGlobalHotKeyOperatingMode mode) WEAK_IMPORT_ATTRIBUTE; + +typedef CGSConnection (*_CGSDefaultConnection_t)(void); +typedef CGError (*CGSGetGlobalHotKeyOperatingMode_t)(CGSConnection connection, CGSGlobalHotKeyOperatingMode *mode); +typedef CGError (*CGSSetGlobalHotKeyOperatingMode_t)(CGSConnection connection, CGSGlobalHotKeyOperatingMode mode); + +static _CGSDefaultConnection_t s__CGSDefaultConnection; +static CGSGetGlobalHotKeyOperatingMode_t s_CGSGetGlobalHotKeyOperatingMode; +static CGSSetGlobalHotKeyOperatingMode_t s_CGSSetGlobalHotKeyOperatingMode; + +#ifdef __cplusplus +} +#endif + +#define LOOKUP(name_) \ + s_ ## name_ = NULL; \ + if (NSIsSymbolNameDefinedWithHint("_" #name_, "CoreGraphics")) { \ + s_ ## name_ = (name_ ## _t)NSAddressOfSymbol( \ + NSLookupAndBindSymbolWithHint( \ + "_" #name_, "CoreGraphics")); \ + } + +bool +COSXScreen::isGlobalHotKeyOperatingModeAvailable() +{ + if (!s_testedForGHOM) { + s_testedForGHOM = true; + LOOKUP(_CGSDefaultConnection); + LOOKUP(CGSGetGlobalHotKeyOperatingMode); + LOOKUP(CGSSetGlobalHotKeyOperatingMode); + s_hasGHOM = (s__CGSDefaultConnection != NULL && + s_CGSGetGlobalHotKeyOperatingMode != NULL && + s_CGSSetGlobalHotKeyOperatingMode != NULL); + } + return s_hasGHOM; +} + +void +COSXScreen::setGlobalHotKeysEnabled(bool enabled) +{ + if (isGlobalHotKeyOperatingModeAvailable()) { + CGSConnection conn = s__CGSDefaultConnection(); + + CGSGlobalHotKeyOperatingMode mode; + s_CGSGetGlobalHotKeyOperatingMode(conn, &mode); + + if (enabled && mode == CGSGlobalHotKeyDisable) { + s_CGSSetGlobalHotKeyOperatingMode(conn, CGSGlobalHotKeyEnable); + } + else if (!enabled && mode == CGSGlobalHotKeyEnable) { + s_CGSSetGlobalHotKeyOperatingMode(conn, CGSGlobalHotKeyDisable); + } + } +} + +bool +COSXScreen::getGlobalHotKeysEnabled() +{ + CGSGlobalHotKeyOperatingMode mode; + if (isGlobalHotKeyOperatingModeAvailable()) { + CGSConnection conn = s__CGSDefaultConnection(); + s_CGSGetGlobalHotKeyOperatingMode(conn, &mode); + } + else { + mode = CGSGlobalHotKeyEnable; + } + return (mode == CGSGlobalHotKeyEnable); +} + +#endif + +// +// COSXScreen::CHotKeyItem +// + +COSXScreen::CHotKeyItem::CHotKeyItem(UInt32 keycode, UInt32 mask) : + m_ref(NULL), + m_keycode(keycode), + m_mask(mask) +{ + // do nothing +} + +COSXScreen::CHotKeyItem::CHotKeyItem(EventHotKeyRef ref, + UInt32 keycode, UInt32 mask) : + m_ref(ref), + m_keycode(keycode), + m_mask(mask) +{ + // do nothing +} + +EventHotKeyRef +COSXScreen::CHotKeyItem::getRef() const +{ + return m_ref; +} + +bool +COSXScreen::CHotKeyItem::operator<(const CHotKeyItem& x) const +{ + return (m_keycode < x.m_keycode || + (m_keycode == x.m_keycode && m_mask < x.m_mask)); +} + +// Quartz event tap support for the secondary display. This make sure that we +// will show the cursor if a local event comes in while synergy has the cursor off the screen. +CGEventRef +COSXScreen::handleCGInputEventSecondary(CGEventTapProxy proxy, + CGEventType type, + CGEventRef event, + void* refcon) +{ + COSXScreen* screen = (COSXScreen*)refcon; + if (screen->m_cursorHidden) { + CGPoint pos; + bool showCursor = true; + if (type == kCGEventMouseMoved) { + pos = CGEventGetLocation(event); + if (pos.x == screen->m_xCenter && pos.y == screen->m_yCenter) { + showCursor = false; + } + } + if (showCursor) { + LOG((CLOG_DEBUG "Trying to show cursor from local event. (type = %d)", type)); + screen->showCursor(); + screen->m_cursorHidden = false; + } + } + LOG((CLOG_DEBUG2 "Local event? (type = %d)", type)); + return event; +} + +// Quartz event tap support +CGEventRef +COSXScreen::handleCGInputEvent(CGEventTapProxy proxy, + CGEventType type, + CGEventRef event, + void* refcon) +{ + COSXScreen* screen = (COSXScreen*)refcon; + CGPoint pos; + + // Patch by Perceptum to ignore what seems to be an "Out of Bounds" + // event that is emitted by touching the trackpad on a MacBook + // Suspicion that it is related to the new Gestures trackpad + // bryan@perceptum.biz + if(type > NX_LASTEVENT) { + LOG((CLOG_NOTE "Ignoring Out of Bounds Quartz Event type: 0x%02x [max 0x%02x]", type, NX_LASTEVENT)); + return NULL; + } + + switch(type) { + case kCGEventLeftMouseDown: + case kCGEventRightMouseDown: + case kCGEventOtherMouseDown: + screen->onMouseButton(true, CGEventGetIntegerValueField(event, kCGMouseEventButtonNumber) + 1); + break; + case kCGEventLeftMouseUp: + case kCGEventRightMouseUp: + case kCGEventOtherMouseUp: + screen->onMouseButton(false, CGEventGetIntegerValueField(event, kCGMouseEventButtonNumber) + 1); + break; + case kCGEventMouseMoved: + case kCGEventLeftMouseDragged: + case kCGEventRightMouseDragged: + case kCGEventOtherMouseDragged: + pos = CGEventGetLocation(event); + screen->onMouseMove(pos.x, pos.y); + + // The system ignores our cursor-centering calls if + // we don't return the event. This should be harmless, + // but might register as slight movement to other apps + // on the system. It hasn't been a problem before, though. + return event; + break; + case kCGEventScrollWheel: + screen->onMouseWheel(screen->mapScrollWheelToSynergy( + CGEventGetIntegerValueField(event, kCGScrollWheelEventDeltaAxis2)), + screen->mapScrollWheelToSynergy( + CGEventGetIntegerValueField(event, kCGScrollWheelEventDeltaAxis1))); + break; + case kCGEventKeyDown: + case kCGEventKeyUp: + case kCGEventFlagsChanged: + screen->onKey(event); + break; + case kCGEventTapDisabledByTimeout: + // Re-enable our event-tap + CGEventTapEnable(screen->m_eventTapPort, true); + LOG((CLOG_NOTE "quartz event tap was disabled by timeout, re-enabling")); + break; + case kCGEventTapDisabledByUserInput: + LOG((CLOG_ERR "quartz event tap was disabled by user input")); + break; + case NX_NULLEVENT: + break; + case NX_SYSDEFINED: + // Unknown, forward it + return event; + break; + case NX_NUMPROCS: + break; + default: + LOG((CLOG_NOTE "unknown quartz event type: 0x%02x", type)); + } + + if(screen->m_isOnScreen) { + return event; + } else { + return NULL; + } +} + +void +COSXScreen::CMouseButtonState::set(UInt32 button, MouseButtonState state) +{ + bool newState = (state == kMouseButtonDown); + m_buttons.set(button, newState); +} + +bool +COSXScreen::CMouseButtonState::any() +{ + return m_buttons.any(); +} + +void +COSXScreen::CMouseButtonState::reset() +{ + m_buttons.reset(); +} + +void +COSXScreen::CMouseButtonState::overwrite(UInt32 buttons) +{ + m_buttons = std::bitset(buttons); +} + +bool +COSXScreen::CMouseButtonState::test(UInt32 button) const +{ + return m_buttons.test(button); +} + +SInt8 +COSXScreen::CMouseButtonState::getFirstButtonDown() const +{ + if (m_buttons.any()) { + for (unsigned short button = 0; button < m_buttons.size(); button++) { + if (m_buttons.test(button)) { + return button; + } + } + } + return -1; +} diff --git a/src/lib/platform/COSXScreen.h b/src/lib/platform/COSXScreen.h new file mode 100644 index 00000000..e90bb533 --- /dev/null +++ b/src/lib/platform/COSXScreen.h @@ -0,0 +1,339 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef COSXSCREEN_H +#define COSXSCREEN_H + +#include + +#include "stdmap.h" +#include "stdvector.h" + +#include +#include "COSXClipboard.h" +#include "CPlatformScreen.h" + +#include +#include +#include +#include +#include + +extern "C" { + typedef int CGSConnectionID; + CGError CGSSetConnectionProperty(CGSConnectionID cid, CGSConnectionID targetCID, CFStringRef key, CFTypeRef value); + int _CGSDefaultConnection(); +} + + +template +class CCondVar; +class CEventQueueTimer; +class CMutex; +class CThread; +class COSXKeyState; +class COSXScreenSaver; + +//! Implementation of IPlatformScreen for OS X +class COSXScreen : public CPlatformScreen { +public: + COSXScreen(bool isPrimary); + virtual ~COSXScreen(); + + // IScreen overrides + virtual void* getEventTarget() const; + virtual bool getClipboard(ClipboardID id, IClipboard*) const; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const; + virtual void getCursorPos(SInt32& x, SInt32& y) const; + + // IPrimaryScreen overrides + virtual void reconfigure(UInt32 activeSides); + virtual void warpCursor(SInt32 x, SInt32 y); + virtual UInt32 registerHotKey(KeyID key, KeyModifierMask mask); + virtual void unregisterHotKey(UInt32 id); + virtual void fakeInputBegin(); + virtual void fakeInputEnd(); + virtual SInt32 getJumpZoneSize() const; + virtual bool isAnyMouseButtonDown() const; + virtual void getCursorCenter(SInt32& x, SInt32& y) const; + virtual void gameDeviceTimingResp(UInt16 freq) { } + + // ISecondaryScreen overrides + virtual void fakeMouseButton(ButtonID id, bool press); + virtual void fakeMouseMove(SInt32 x, SInt32 y) const; + virtual void fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const; + virtual void fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const; + virtual void fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const { } + virtual void fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const { } + virtual void fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const { } + virtual void queueGameDeviceTimingReq() const { } + + // IPlatformScreen overrides + virtual void enable(); + virtual void disable(); + virtual void enter(); + virtual bool leave(); + virtual bool setClipboard(ClipboardID, const IClipboard*); + virtual void checkClipboards(); + virtual void openScreensaver(bool notify); + virtual void closeScreensaver(); + virtual void screensaver(bool activate); + virtual void resetOptions(); + virtual void setOptions(const COptionsList& options); + virtual void setSequenceNumber(UInt32); + virtual bool isPrimary() const; + virtual void gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2) { } + +protected: + // IPlatformScreen overrides + virtual void handleSystemEvent(const CEvent&, void*); + virtual void updateButtons(); + virtual IKeyState* getKeyState() const; + +private: + void updateScreenShape(); + void updateScreenShape(const CGDirectDisplayID, const CGDisplayChangeSummaryFlags); + void postMouseEvent(CGPoint&) const; + + // convenience function to send events + void sendEvent(CEvent::Type type, void* = NULL) const; + void sendClipboardEvent(CEvent::Type type, ClipboardID id) const; + + // message handlers + bool onMouseMove(SInt32 mx, SInt32 my); + // mouse button handler. pressed is true if this is a mousedown + // event, false if it is a mouseup event. macButton is the index + // of the button pressed using the mac button mapping. + bool onMouseButton(bool pressed, UInt16 macButton); + bool onMouseWheel(SInt32 xDelta, SInt32 yDelta) const; + + #if !defined(MAC_OS_X_VERSION_10_5) + bool onDisplayChange(); + #endif + void constructMouseButtonEventMap(); + + bool onKey(CGEventRef event); + + bool onHotKey(EventRef event) const; + + // Added here to allow the carbon cursor hack to be called. + void showCursor(); + void hideCursor(); + + // map mac mouse button to synergy buttons + ButtonID mapMacButtonToSynergy(UInt16) const; + + // map mac scroll wheel value to a synergy scroll wheel value + SInt32 mapScrollWheelToSynergy(SInt32) const; + + // map synergy scroll wheel value to a mac scroll wheel value + SInt32 mapScrollWheelFromSynergy(SInt32) const; + + // get the current scroll wheel speed + double getScrollSpeed() const; + + // get the current scroll wheel speed + double getScrollSpeedFactor() const; + + // enable/disable drag handling for buttons 3 and up + void enableDragTimer(bool enable); + + // drag timer handler + void handleDrag(const CEvent&, void*); + + // clipboard check timer handler + void handleClipboardCheck(const CEvent&, void*); + +#if defined(MAC_OS_X_VERSION_10_5) + // Resolution switch callback + static void displayReconfigurationCallback(CGDirectDisplayID, + CGDisplayChangeSummaryFlags, void*); +#else + static pascal void displayManagerCallback(void* inUserData, + SInt16 inMessage, void* inNotifyData); +#endif + // fast user switch callback + static pascal OSStatus + userSwitchCallback(EventHandlerCallRef nextHandler, + EventRef theEvent, void* inUserData); + + // sleep / wakeup support + void watchSystemPowerThread(void*); + static void testCanceled(CFRunLoopTimerRef timer, void*info); + static void powerChangeCallback(void* refcon, io_service_t service, + natural_t messageType, void* messageArgument); + void handlePowerChangeRequest(natural_t messageType, + void* messageArgument); + + static CEvent::Type getConfirmSleepEvent(); + void handleConfirmSleep(const CEvent& event, void*); + + // global hotkey operating mode + static bool isGlobalHotKeyOperatingModeAvailable(); + static void setGlobalHotKeysEnabled(bool enabled); + static bool getGlobalHotKeysEnabled(); + + // Quartz event tap support + static CGEventRef handleCGInputEvent(CGEventTapProxy proxy, + CGEventType type, + CGEventRef event, + void* refcon); + static CGEventRef handleCGInputEventSecondary(CGEventTapProxy proxy, + CGEventType type, + CGEventRef event, + void* refcon); +private: + struct CHotKeyItem { + public: + CHotKeyItem(UInt32, UInt32); + CHotKeyItem(EventHotKeyRef, UInt32, UInt32); + + EventHotKeyRef getRef() const; + + bool operator<(const CHotKeyItem&) const; + + private: + EventHotKeyRef m_ref; + UInt32 m_keycode; + UInt32 m_mask; + }; + + enum MouseButtonState { + kMouseButtonUp = 0, + kMouseButtonDragged, + kMouseButtonDown, + kMouseButtonStateMax + }; + + + class CMouseButtonState { + public: + void set(UInt32 button, MouseButtonState state); + bool any(); + void reset(); + void overwrite(UInt32 buttons); + + bool test(UInt32 button) const; + SInt8 getFirstButtonDown() const; + private: + std::bitset m_buttons; + }; + + typedef std::map HotKeyMap; + typedef std::vector HotKeyIDList; + typedef std::map ModifierHotKeyMap; + typedef std::map HotKeyToIDMap; + + // true if screen is being used as a primary screen, false otherwise + bool m_isPrimary; + + // true if mouse has entered the screen + bool m_isOnScreen; + + // the display + CGDirectDisplayID m_displayID; + + // screen shape stuff + SInt32 m_x, m_y; + SInt32 m_w, m_h; + SInt32 m_xCenter, m_yCenter; + + // mouse state + mutable SInt32 m_xCursor, m_yCursor; + mutable bool m_cursorPosValid; + + /* FIXME: this data structure is explicitly marked mutable due + to a need to track the state of buttons since the remote + side only lets us know of change events, and because the + fakeMouseButton button method is marked 'const'. This is + Evil, and this should be moved to a place where it need not + be mutable as soon as possible. */ + mutable CMouseButtonState m_buttonState; + typedef std::map MouseButtonEventMapType; + std::vector MouseButtonEventMap; + + bool m_cursorHidden; + SInt32 m_dragNumButtonsDown; + Point m_dragLastPoint; + CEventQueueTimer* m_dragTimer; + + // keyboard stuff + COSXKeyState* m_keyState; + + // clipboards + COSXClipboard m_pasteboard; + UInt32 m_sequenceNumber; + + // screen saver stuff + COSXScreenSaver* m_screensaver; + bool m_screensaverNotify; + + // clipboard stuff + bool m_ownClipboard; + CEventQueueTimer* m_clipboardTimer; + + // window object that gets user input events when the server + // has focus. + WindowRef m_hiddenWindow; + // window object that gets user input events when the server + // does not have focus. + WindowRef m_userInputWindow; + +#if !defined(MAC_OS_X_VERSION_10_5) + // display manager stuff (to get screen resolution switches). + DMExtendedNotificationUPP m_displayManagerNotificationUPP; + ProcessSerialNumber m_PSN; +#endif + + // fast user switching + EventHandlerRef m_switchEventHandlerRef; + + // sleep / wakeup + CMutex* m_pmMutex; + CThread* m_pmWatchThread; + CCondVar* m_pmThreadReady; + CFRunLoopRef m_pmRunloop; + io_connect_t m_pmRootPort; + + // hot key stuff + HotKeyMap m_hotKeys; + HotKeyIDList m_oldHotKeyIDs; + ModifierHotKeyMap m_modifierHotKeys; + UInt32 m_activeModifierHotKey; + KeyModifierMask m_activeModifierHotKeyMask; + HotKeyToIDMap m_hotKeyToIDMap; + + // global hotkey operating mode + static bool s_testedForGHOM; + static bool s_hasGHOM; + + // events + static CEvent::Type s_confirmSleepEvent; + + // Quartz input event support + CFMachPortRef m_eventTapPort; + CFRunLoopSourceRef m_eventTapRLSR; + + // for double click coalescing. + double m_lastSingleClick; + double m_lastDoubleClick; + SInt32 m_lastSingleClickXCursor; + SInt32 m_lastSingleClickYCursor; +}; + +#endif diff --git a/src/lib/platform/COSXScreenSaver.cpp b/src/lib/platform/COSXScreenSaver.cpp new file mode 100644 index 00000000..a4457fe9 --- /dev/null +++ b/src/lib/platform/COSXScreenSaver.cpp @@ -0,0 +1,178 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#import "COSXScreenSaver.h" +#import "COSXScreenSaverUtil.h" +#import "CLog.h" +#import "IEventQueue.h" +#import "IPrimaryScreen.h" +#import + +// +// COSXScreenSaver +// + +COSXScreenSaver::COSXScreenSaver(void* eventTarget) : + m_eventTarget(eventTarget), + m_enabled(true) +{ + m_autoReleasePool = screenSaverUtilCreatePool(); + m_screenSaverController = screenSaverUtilCreateController(); + + // install launch/termination event handlers + EventTypeSpec launchEventTypes[2]; + launchEventTypes[0].eventClass = kEventClassApplication; + launchEventTypes[0].eventKind = kEventAppLaunched; + launchEventTypes[1].eventClass = kEventClassApplication; + launchEventTypes[1].eventKind = kEventAppTerminated; + + EventHandlerUPP launchTerminationEventHandler = + NewEventHandlerUPP(launchTerminationCallback); + InstallApplicationEventHandler(launchTerminationEventHandler, 2, + launchEventTypes, this, + &m_launchTerminationEventHandlerRef); + DisposeEventHandlerUPP(launchTerminationEventHandler); + + m_screenSaverPSN.highLongOfPSN = 0; + m_screenSaverPSN.lowLongOfPSN = 0; + + // test if screensaver is running and find process number + if (isActive()) { + ProcessInfoRec procInfo; + Str31 procName; // pascal string. first byte holds length. + memset(&procInfo, 0, sizeof(procInfo)); + procInfo.processName = procName; + procInfo.processInfoLength = sizeof(ProcessInfoRec); + + ProcessSerialNumber psn; + OSErr err = GetNextProcess(&psn); + while (err == 0) { + memset(procName, 0, sizeof(procName)); + err = GetProcessInformation(&psn, &procInfo); + if (err != 0) { + break; + } + if (strcmp("ScreenSaverEngine", (const char*)&procName[1]) == 0) { + m_screenSaverPSN = psn; + break; + } + err = GetNextProcess(&psn); + } + } +} + +COSXScreenSaver::~COSXScreenSaver() +{ + RemoveEventHandler(m_launchTerminationEventHandlerRef); +// screenSaverUtilReleaseController(m_screenSaverController); + screenSaverUtilReleasePool(m_autoReleasePool); +} + +void +COSXScreenSaver::enable() +{ + m_enabled = true; + screenSaverUtilEnable(m_screenSaverController); +} + +void +COSXScreenSaver::disable() +{ + m_enabled = false; + screenSaverUtilDisable(m_screenSaverController); +} + +void +COSXScreenSaver::activate() +{ + screenSaverUtilActivate(m_screenSaverController); +} + +void +COSXScreenSaver::deactivate() +{ + screenSaverUtilDeactivate(m_screenSaverController, m_enabled); +} + +bool +COSXScreenSaver::isActive() const +{ + return (screenSaverUtilIsActive(m_screenSaverController) != 0); +} + +void +COSXScreenSaver::processLaunched(ProcessSerialNumber psn) +{ + CFStringRef processName; + OSStatus err = CopyProcessName(&psn, &processName); + + if (err == 0 && CFEqual(CFSTR("ScreenSaverEngine"), processName)) { + m_screenSaverPSN = psn; + LOG((CLOG_DEBUG1 "ScreenSaverEngine launched. Enabled=%d", m_enabled)); + if (m_enabled) { + EVENTQUEUE->addEvent( + CEvent(IPrimaryScreen::getScreensaverActivatedEvent(), + m_eventTarget)); + } + } +} + +void +COSXScreenSaver::processTerminated(ProcessSerialNumber psn) +{ + if (m_screenSaverPSN.highLongOfPSN == psn.highLongOfPSN && + m_screenSaverPSN.lowLongOfPSN == psn.lowLongOfPSN) { + LOG((CLOG_DEBUG1 "ScreenSaverEngine terminated. Enabled=%d", m_enabled)); + if (m_enabled) { + EVENTQUEUE->addEvent( + CEvent(IPrimaryScreen::getScreensaverDeactivatedEvent(), + m_eventTarget)); + } + + m_screenSaverPSN.highLongOfPSN = 0; + m_screenSaverPSN.lowLongOfPSN = 0; + } +} + +pascal OSStatus +COSXScreenSaver::launchTerminationCallback( + EventHandlerCallRef nextHandler, + EventRef theEvent, void* userData) +{ + OSStatus result; + ProcessSerialNumber psn; + EventParamType actualType; + UInt32 actualSize; + + result = GetEventParameter(theEvent, kEventParamProcessID, + typeProcessSerialNumber, &actualType, + sizeof(psn), &actualSize, &psn); + + if ((result == noErr) && + (actualSize > 0) && + (actualType == typeProcessSerialNumber)) { + COSXScreenSaver* screenSaver = (COSXScreenSaver*)userData; + UInt32 eventKind = GetEventKind(theEvent); + if (eventKind == kEventAppLaunched) { + screenSaver->processLaunched(psn); + } + else if (eventKind == kEventAppTerminated) { + screenSaver->processTerminated(psn); + } + } + return (CallNextEventHandler(nextHandler, theEvent)); +} diff --git a/src/lib/platform/COSXScreenSaver.h b/src/lib/platform/COSXScreenSaver.h new file mode 100644 index 00000000..331f1c42 --- /dev/null +++ b/src/lib/platform/COSXScreenSaver.h @@ -0,0 +1,57 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef COSXSCREENSAVER_H +#define COSXSCREENSAVER_H + +#include "IScreenSaver.h" +#include + +//! OSX screen saver implementation +class COSXScreenSaver : public IScreenSaver { +public: + COSXScreenSaver(void* eventTarget); + virtual ~COSXScreenSaver(); + + // IScreenSaver overrides + virtual void enable(); + virtual void disable(); + virtual void activate(); + virtual void deactivate(); + virtual bool isActive() const; + +private: + void processLaunched(ProcessSerialNumber psn); + void processTerminated(ProcessSerialNumber psn); + + static pascal OSStatus + launchTerminationCallback( + EventHandlerCallRef nextHandler, + EventRef theEvent, void* userData); + +private: + // the target for the events we generate + void* m_eventTarget; + + bool m_enabled; + void* m_screenSaverController; + void* m_autoReleasePool; + EventHandlerRef m_launchTerminationEventHandlerRef; + ProcessSerialNumber m_screenSaverPSN; +}; + +#endif diff --git a/src/lib/platform/COSXScreenSaverUtil.h b/src/lib/platform/COSXScreenSaverUtil.h new file mode 100644 index 00000000..019615e4 --- /dev/null +++ b/src/lib/platform/COSXScreenSaverUtil.h @@ -0,0 +1,42 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef COSXSCREENSAVERUTIL_H +#define COSXSCREENSAVERUTIL_H + +#include "common.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +void* screenSaverUtilCreatePool(); +void screenSaverUtilReleasePool(void*); + +void* screenSaverUtilCreateController(); +void screenSaverUtilReleaseController(void*); +void screenSaverUtilEnable(void*); +void screenSaverUtilDisable(void*); +void screenSaverUtilActivate(void*); +void screenSaverUtilDeactivate(void*, int isEnabled); +int screenSaverUtilIsActive(void*); + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/src/lib/platform/COSXScreenSaverUtil.m b/src/lib/platform/COSXScreenSaverUtil.m new file mode 100644 index 00000000..edfc7b2e --- /dev/null +++ b/src/lib/platform/COSXScreenSaverUtil.m @@ -0,0 +1,81 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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. + */ + +#import "COSXScreenSaverUtil.h" +#import "OSXScreenSaverControl.h" +#import + +// +// screenSaverUtil functions +// +// Note: these helper functions exist only so we can avoid using ObjC++. +// autoconf/automake don't know about ObjC++ and I don't know how to +// teach them about it. +// + +void* +screenSaverUtilCreatePool() +{ + return [[NSAutoreleasePool alloc] init]; +} + +void +screenSaverUtilReleasePool(void* pool) +{ + [(NSAutoreleasePool*)pool release]; +} + +void* +screenSaverUtilCreateController() +{ + return [[ScreenSaverController controller] retain]; +} + +void +screenSaverUtilReleaseController(void* controller) +{ + [(ScreenSaverController*)controller release]; +} + +void +screenSaverUtilEnable(void* controller) +{ + [(ScreenSaverController*)controller setScreenSaverCanRun:YES]; +} + +void +screenSaverUtilDisable(void* controller) +{ + [(ScreenSaverController*)controller setScreenSaverCanRun:NO]; +} + +void +screenSaverUtilActivate(void* controller) +{ + [(ScreenSaverController*)controller setScreenSaverCanRun:YES]; + [(ScreenSaverController*)controller screenSaverStartNow]; +} + +void +screenSaverUtilDeactivate(void* controller, int isEnabled) +{ + [(ScreenSaverController*)controller screenSaverStopNow]; + [(ScreenSaverController*)controller setScreenSaverCanRun:isEnabled]; +} + +int +screenSaverUtilIsActive(void* controller) +{ + return [(ScreenSaverController*)controller screenSaverIsRunning]; +} diff --git a/src/lib/platform/CSynergyHook.cpp b/src/lib/platform/CSynergyHook.cpp new file mode 100644 index 00000000..70a499da --- /dev/null +++ b/src/lib/platform/CSynergyHook.cpp @@ -0,0 +1,1146 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CSynergyHook.h" +#include "ProtocolTypes.h" +#include +#include + +#if _MSC_VER >= 1400 +// VS2005 hack - we don't use assert here because we don't want to link with the CRT. +#undef assert +#if _DEBUG +#define assert(_X_) if (!(_X_)) __debugbreak() +#else +#define assert(_X_) __noop() +#endif +// VS2005 is a bit more smart than VC6 and optimize simple copy loop to +// intrinsic memcpy. +#pragma function(memcpy) +#endif + +// +// debugging compile flag. when not zero the server doesn't grab +// the keyboard when the mouse leaves the server screen. this +// makes it possible to use the debugger (via the keyboard) when +// all user input would normally be caught by the hook procedures. +// +#define NO_GRAB_KEYBOARD 0 + +// +// debugging compile flag. when not zero the server will not +// install low level hooks. +// +#define NO_LOWLEVEL_HOOKS 0 + +// +// extra mouse wheel stuff +// + +enum EWheelSupport { + kWheelNone, + kWheelOld, + kWheelWin2000, + kWheelModern +}; + +// declare extended mouse hook struct. useable on win2k +typedef struct tagMOUSEHOOKSTRUCTWin2000 { + MOUSEHOOKSTRUCT mhs; + DWORD mouseData; +} MOUSEHOOKSTRUCTWin2000; + +#if !defined(SM_MOUSEWHEELPRESENT) +#define SM_MOUSEWHEELPRESENT 75 +#endif + +// X button stuff +#if !defined(WM_XBUTTONDOWN) +#define WM_XBUTTONDOWN 0x020B +#define WM_XBUTTONUP 0x020C +#define WM_XBUTTONDBLCLK 0x020D +#define WM_NCXBUTTONDOWN 0x00AB +#define WM_NCXBUTTONUP 0x00AC +#define WM_NCXBUTTONDBLCLK 0x00AD +#define MOUSEEVENTF_XDOWN 0x0080 +#define MOUSEEVENTF_XUP 0x0100 +#define XBUTTON1 0x0001 +#define XBUTTON2 0x0002 +#endif + + +// +// globals +// + +#if defined(_MSC_VER) +#pragma comment(linker, "-section:shared,rws") +#pragma data_seg("shared") +#endif +// all data in this shared section *must* be initialized + +static HINSTANCE g_hinstance = NULL; +static DWORD g_processID = 0; +static EWheelSupport g_wheelSupport = kWheelNone; +static UINT g_wmMouseWheel = 0; +static DWORD g_threadID = 0; +static HHOOK g_keyboard = NULL; +static HHOOK g_mouse = NULL; +static HHOOK g_getMessage = NULL; +static HHOOK g_keyboardLL = NULL; +static HHOOK g_mouseLL = NULL; +static bool g_screenSaver = false; +static EHookMode g_mode = kHOOK_DISABLE; +static UInt32 g_zoneSides = 0; +static SInt32 g_zoneSize = 0; +static SInt32 g_xScreen = 0; +static SInt32 g_yScreen = 0; +static SInt32 g_wScreen = 0; +static SInt32 g_hScreen = 0; +static WPARAM g_deadVirtKey = 0; +static WPARAM g_deadRelease = 0; +static LPARAM g_deadLParam = 0; +static BYTE g_deadKeyState[256] = { 0 }; +static DWORD g_hookThread = 0; +static DWORD g_attachedThread = 0; +static bool g_fakeInput = false; + +#if defined(_MSC_VER) +#pragma data_seg() +#endif + +// keep linker quiet about floating point stuff. we don't use any +// floating point operations but our includes may define some +// (unused) floating point values. +#ifndef _DEBUG +extern "C" { +int _fltused=0; +} +#endif + + +// +// internal functions +// + +static +void +detachThread() +{ + if (g_attachedThread != 0 && g_hookThread != g_attachedThread) { + AttachThreadInput(g_hookThread, g_attachedThread, FALSE); + g_attachedThread = 0; + } +} + +static +bool +attachThreadToForeground() +{ + // only attach threads if using low level hooks. a low level hook + // runs in the thread that installed the hook but we have to make + // changes that require being attached to the target thread (which + // should be the foreground window). a regular hook runs in the + // thread that just removed the event from its queue so we're + // already in the right thread. + if (g_hookThread != 0) { + HWND window = GetForegroundWindow(); + if (window == NULL) + return false; + + DWORD threadID = GetWindowThreadProcessId(window, NULL); + // skip if no change + if (g_attachedThread != threadID) { + // detach from previous thread + detachThread(); + + // attach to new thread + if (threadID != 0 && threadID != g_hookThread) { + AttachThreadInput(g_hookThread, threadID, TRUE); + g_attachedThread = threadID; + } + return true; + } + } + return false; +} + +#if !NO_GRAB_KEYBOARD +static +WPARAM +makeKeyMsg(UINT virtKey, char c, bool noAltGr) +{ + return MAKEWPARAM(MAKEWORD(virtKey & 0xff, (BYTE)c), noAltGr ? 1 : 0); +} + +static +void +keyboardGetState(BYTE keys[256]) +{ + // we have to use GetAsyncKeyState() rather than GetKeyState() because + // we don't pass through most keys so the event synchronous state + // doesn't get updated. we do that because certain modifier keys have + // side effects, like alt and the windows key. + SHORT key; + for (int i = 0; i < 256; ++i) { + key = GetAsyncKeyState(i); + keys[i] = (BYTE)((key < 0) ? 0x80u : 0); + } + key = GetKeyState(VK_CAPITAL); + keys[VK_CAPITAL] = (BYTE)(((key < 0) ? 0x80 : 0) | (key & 1)); +} + +static +bool +doKeyboardHookHandler(WPARAM wParam, LPARAM lParam) +{ + // check for special events indicating if we should start or stop + // passing events through and not report them to the server. this + // is used to allow the server to synthesize events locally but + // not pick them up as user events. + if (wParam == SYNERGY_HOOK_FAKE_INPUT_VIRTUAL_KEY && + ((lParam >> 16) & 0xffu) == SYNERGY_HOOK_FAKE_INPUT_SCANCODE) { + // update flag + g_fakeInput = ((lParam & 0x80000000u) == 0); + PostThreadMessage(g_threadID, SYNERGY_MSG_DEBUG, + 0xff000000u | wParam, lParam); + + // discard event + return true; + } + + // if we're expecting fake input then just pass the event through + // and do not forward to the server + if (g_fakeInput) { + PostThreadMessage(g_threadID, SYNERGY_MSG_DEBUG, + 0xfe000000u | wParam, lParam); + return false; + } + + // VK_RSHIFT may be sent with an extended scan code but right shift + // is not an extended key so we reset that bit. + if (wParam == VK_RSHIFT) { + lParam &= ~0x01000000u; + } + + // tell server about event + PostThreadMessage(g_threadID, SYNERGY_MSG_DEBUG, wParam, lParam); + + // ignore dead key release + if ((g_deadVirtKey == wParam || g_deadRelease == wParam) && + (lParam & 0x80000000u) != 0) { + g_deadRelease = 0; + PostThreadMessage(g_threadID, SYNERGY_MSG_DEBUG, + wParam | 0x04000000, lParam); + return false; + } + + // we need the keyboard state for ToAscii() + BYTE keys[256]; + keyboardGetState(keys); + + // ToAscii() maps ctrl+letter to the corresponding control code + // and ctrl+backspace to delete. we don't want those translations + // so clear the control modifier state. however, if we want to + // simulate AltGr (which is ctrl+alt) then we must not clear it. + UINT control = keys[VK_CONTROL] | keys[VK_LCONTROL] | keys[VK_RCONTROL]; + UINT menu = keys[VK_MENU] | keys[VK_LMENU] | keys[VK_RMENU]; + if ((control & 0x80) == 0 || (menu & 0x80) == 0) { + keys[VK_LCONTROL] = 0; + keys[VK_RCONTROL] = 0; + keys[VK_CONTROL] = 0; + } + else { + keys[VK_LCONTROL] = 0x80; + keys[VK_RCONTROL] = 0x80; + keys[VK_CONTROL] = 0x80; + keys[VK_LMENU] = 0x80; + keys[VK_RMENU] = 0x80; + keys[VK_MENU] = 0x80; + } + + // ToAscii() needs to know if a menu is active for some reason. + // we don't know and there doesn't appear to be any way to find + // out. so we'll just assume a menu is active if the menu key + // is down. + // FIXME -- figure out some way to check if a menu is active + UINT flags = 0; + if ((menu & 0x80) != 0) + flags |= 1; + + // if we're on the server screen then just pass numpad keys with alt + // key down as-is. we won't pick up the resulting character but the + // local app will. if on a client screen then grab keys as usual; + // if the client is a windows system it'll synthesize the expected + // character. if not then it'll probably just do nothing. + if (g_mode != kHOOK_RELAY_EVENTS) { + // we don't use virtual keys because we don't know what the + // state of the numlock key is. we'll hard code the scan codes + // instead. hopefully this works across all keyboards. + UINT sc = (lParam & 0x01ff0000u) >> 16; + if (menu && + (sc >= 0x47u && sc <= 0x52u && sc != 0x4au && sc != 0x4eu)) { + return false; + } + } + + WORD c = 0; + + // map the key event to a character. we have to put the dead + // key back first and this has the side effect of removing it. + if (g_deadVirtKey != 0) { + if(ToAscii((UINT)g_deadVirtKey, (g_deadLParam & 0x10ff0000u) >> 16, + g_deadKeyState, &c, flags) == 2) + { + // If ToAscii returned 2, it means that we accidentally removed + // a double dead key instead of restoring it. Thus, we call + // ToAscii again with the same parameters to restore the + // internal dead key state. + ToAscii((UINT)g_deadVirtKey, (g_deadLParam & 0x10ff0000u) >> 16, + g_deadKeyState, &c, flags); + + // We need to keep track of this because g_deadVirtKey will be + // cleared later on; this would cause the dead key release to + // incorrectly restore the dead key state. + g_deadRelease = g_deadVirtKey; + } + } + + UINT scanCode = ((lParam & 0x10ff0000u) >> 16); + int n = ToAscii((UINT)wParam, scanCode, keys, &c, flags); + + // if mapping failed and ctrl and alt are pressed then try again + // with both not pressed. this handles the case where ctrl and + // alt are being used as individual modifiers rather than AltGr. + // we note that's the case in the message sent back to synergy + // because there's no simple way to deduce it after the fact. + // we have to put the dead key back first, if there was one. + bool noAltGr = false; + if (n == 0 && (control & 0x80) != 0 && (menu & 0x80) != 0) { + noAltGr = true; + PostThreadMessage(g_threadID, SYNERGY_MSG_DEBUG, + wParam | 0x05000000, lParam); + if (g_deadVirtKey != 0) { + if(ToAscii((UINT)g_deadVirtKey, (g_deadLParam & 0x10ff0000u) >> 16, + g_deadKeyState, &c, flags) == 2) + { + ToAscii((UINT)g_deadVirtKey, (g_deadLParam & 0x10ff0000u) >> 16, + g_deadKeyState, &c, flags); + g_deadRelease = g_deadVirtKey; + } + } + BYTE keys2[256]; + for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i) { + keys2[i] = keys[i]; + } + keys2[VK_LCONTROL] = 0; + keys2[VK_RCONTROL] = 0; + keys2[VK_CONTROL] = 0; + keys2[VK_LMENU] = 0; + keys2[VK_RMENU] = 0; + keys2[VK_MENU] = 0; + n = ToAscii((UINT)wParam, scanCode, keys2, &c, flags); + } + + PostThreadMessage(g_threadID, SYNERGY_MSG_DEBUG, + wParam | ((c & 0xff) << 8) | + ((n & 0xff) << 16) | 0x06000000, + lParam); + WPARAM charAndVirtKey = 0; + bool clearDeadKey = false; + switch (n) { + default: + // key is a dead key + + if(lParam & 0x80000000u) + // This handles the obscure situation where a key has been + // pressed which is both a dead key and a normal character + // depending on which modifiers have been pressed. We + // break here to prevent it from being considered a dead + // key. + break; + + g_deadVirtKey = wParam; + g_deadLParam = lParam; + for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i) { + g_deadKeyState[i] = keys[i]; + } + break; + + case 0: + // key doesn't map to a character. this can happen if + // non-character keys are pressed after a dead key. + charAndVirtKey = makeKeyMsg((UINT)wParam, (char)0, noAltGr); + break; + + case 1: + // key maps to a character composed with dead key + charAndVirtKey = makeKeyMsg((UINT)wParam, (char)LOBYTE(c), noAltGr); + clearDeadKey = true; + break; + + case 2: { + // previous dead key not composed. send a fake key press + // and release for the dead key to our window. + WPARAM deadCharAndVirtKey = + makeKeyMsg((UINT)g_deadVirtKey, (char)LOBYTE(c), noAltGr); + PostThreadMessage(g_threadID, SYNERGY_MSG_KEY, + deadCharAndVirtKey, g_deadLParam & 0x7fffffffu); + PostThreadMessage(g_threadID, SYNERGY_MSG_KEY, + deadCharAndVirtKey, g_deadLParam | 0x80000000u); + + // use uncomposed character + charAndVirtKey = makeKeyMsg((UINT)wParam, (char)HIBYTE(c), noAltGr); + clearDeadKey = true; + break; + } + } + + // put back the dead key, if any, for the application to use + if (g_deadVirtKey != 0) { + ToAscii((UINT)g_deadVirtKey, (g_deadLParam & 0x10ff0000u) >> 16, + g_deadKeyState, &c, flags); + } + + // clear out old dead key state + if (clearDeadKey) { + g_deadVirtKey = 0; + g_deadLParam = 0; + } + + // forward message to our window. do this whether or not we're + // forwarding events to clients because this'll keep our thread's + // key state table up to date. that's important for querying + // the scroll lock toggle state. + // XXX -- with hot keys for actions we may only need to do this when + // forwarding. + if (charAndVirtKey != 0) { + PostThreadMessage(g_threadID, SYNERGY_MSG_DEBUG, + charAndVirtKey | 0x07000000, lParam); + PostThreadMessage(g_threadID, SYNERGY_MSG_KEY, charAndVirtKey, lParam); + } + + if (g_mode == kHOOK_RELAY_EVENTS) { + // let certain keys pass through + switch (wParam) { + case VK_CAPITAL: + case VK_NUMLOCK: + case VK_SCROLL: + // pass event on. we want to let these through to + // the window proc because otherwise the keyboard + // lights may not stay synchronized. + break; + + case VK_HANGUL: + // pass these modifiers if using a low level hook, discard + // them if not. + if (g_hookThread == 0) { + return true; + } + break; + + default: + // discard + return true; + } + } + + return false; +} + +static +bool +keyboardHookHandler(WPARAM wParam, LPARAM lParam) +{ + attachThreadToForeground(); + return doKeyboardHookHandler(wParam, lParam); +} +#endif + +static +bool +doMouseHookHandler(WPARAM wParam, SInt32 x, SInt32 y, SInt32 data) +{ + switch (wParam) { + case WM_LBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_XBUTTONDOWN: + case WM_LBUTTONDBLCLK: + case WM_MBUTTONDBLCLK: + case WM_RBUTTONDBLCLK: + case WM_XBUTTONDBLCLK: + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + case WM_XBUTTONUP: + case WM_NCLBUTTONDOWN: + case WM_NCMBUTTONDOWN: + case WM_NCRBUTTONDOWN: + case WM_NCXBUTTONDOWN: + case WM_NCLBUTTONDBLCLK: + case WM_NCMBUTTONDBLCLK: + case WM_NCRBUTTONDBLCLK: + case WM_NCXBUTTONDBLCLK: + case WM_NCLBUTTONUP: + case WM_NCMBUTTONUP: + case WM_NCRBUTTONUP: + case WM_NCXBUTTONUP: + // always relay the event. eat it if relaying. + PostThreadMessage(g_threadID, SYNERGY_MSG_MOUSE_BUTTON, wParam, data); + return (g_mode == kHOOK_RELAY_EVENTS); + + case WM_MOUSEWHEEL: + if (g_mode == kHOOK_RELAY_EVENTS) { + // relay event + PostThreadMessage(g_threadID, SYNERGY_MSG_MOUSE_WHEEL, data, 0); + } + return (g_mode == kHOOK_RELAY_EVENTS); + + case WM_NCMOUSEMOVE: + case WM_MOUSEMOVE: + if (g_mode == kHOOK_RELAY_EVENTS) { + // relay and eat event + PostThreadMessage(g_threadID, SYNERGY_MSG_MOUSE_MOVE, x, y); + return true; + } + else if (g_mode == kHOOK_WATCH_JUMP_ZONE) { + // low level hooks can report bogus mouse positions that are + // outside of the screen. jeez. naturally we end up getting + // fake motion in the other direction to get the position back + // on the screen, which plays havoc with switch on double tap. + // CServer deals with that. we'll clamp positions onto the + // screen. also, if we discard events for positions outside + // of the screen then the mouse appears to get a bit jerky + // near the edge. we can either accept that or pass the bogus + // events. we'll try passing the events. + bool bogus = false; + if (x < g_xScreen) { + x = g_xScreen; + bogus = true; + } + else if (x >= g_xScreen + g_wScreen) { + x = g_xScreen + g_wScreen - 1; + bogus = true; + } + if (y < g_yScreen) { + y = g_yScreen; + bogus = true; + } + else if (y >= g_yScreen + g_hScreen) { + y = g_yScreen + g_hScreen - 1; + bogus = true; + } + + // check for mouse inside jump zone + bool inside = false; + if (!inside && (g_zoneSides & kLeftMask) != 0) { + inside = (x < g_xScreen + g_zoneSize); + } + if (!inside && (g_zoneSides & kRightMask) != 0) { + inside = (x >= g_xScreen + g_wScreen - g_zoneSize); + } + if (!inside && (g_zoneSides & kTopMask) != 0) { + inside = (y < g_yScreen + g_zoneSize); + } + if (!inside && (g_zoneSides & kBottomMask) != 0) { + inside = (y >= g_yScreen + g_hScreen - g_zoneSize); + } + + // relay the event + PostThreadMessage(g_threadID, SYNERGY_MSG_MOUSE_MOVE, x, y); + + // if inside and not bogus then eat the event + return inside && !bogus; + } + } + + // pass the event + return false; +} + +static +bool +mouseHookHandler(WPARAM wParam, SInt32 x, SInt32 y, SInt32 data) +{ +// attachThreadToForeground(); + return doMouseHookHandler(wParam, x, y, data); +} + +#if !NO_GRAB_KEYBOARD +static +LRESULT CALLBACK +keyboardHook(int code, WPARAM wParam, LPARAM lParam) +{ + if (code >= 0) { + // handle the message + if (keyboardHookHandler(wParam, lParam)) { + return 1; + } + } + + return CallNextHookEx(g_keyboard, code, wParam, lParam); +} +#endif + +static +LRESULT CALLBACK +mouseHook(int code, WPARAM wParam, LPARAM lParam) +{ + if (code >= 0) { + // decode message + const MOUSEHOOKSTRUCT* info = (const MOUSEHOOKSTRUCT*)lParam; + SInt32 x = (SInt32)info->pt.x; + SInt32 y = (SInt32)info->pt.y; + SInt32 w = 0; + if (wParam == WM_MOUSEWHEEL) { + // win2k and other systems supporting WM_MOUSEWHEEL in + // the mouse hook are gratuitously different (and poorly + // documented). if a low-level mouse hook is in place + // it should capture these events so we'll never see + // them. + switch (g_wheelSupport) { + case kWheelModern: + w = static_cast(LOWORD(info->dwExtraInfo)); + break; + + case kWheelWin2000: { + const MOUSEHOOKSTRUCTWin2000* info2k = + (const MOUSEHOOKSTRUCTWin2000*)lParam; + w = static_cast(HIWORD(info2k->mouseData)); + break; + } + + default: + break; + } + } + + // handle the message. note that we don't handle X buttons + // here. that's okay because they're only supported on + // win2k and winxp and up and on those platforms we'll get + // get the mouse events through the low level hook. + if (mouseHookHandler(wParam, x, y, w)) { + return 1; + } + } + + return CallNextHookEx(g_mouse, code, wParam, lParam); +} + +static +LRESULT CALLBACK +getMessageHook(int code, WPARAM wParam, LPARAM lParam) +{ + if (code >= 0) { + if (g_screenSaver) { + MSG* msg = reinterpret_cast(lParam); + if (msg->message == WM_SYSCOMMAND && + msg->wParam == SC_SCREENSAVE) { + // broadcast screen saver started message + PostThreadMessage(g_threadID, + SYNERGY_MSG_SCREEN_SAVER, TRUE, 0); + } + } + if (g_mode == kHOOK_RELAY_EVENTS) { + MSG* msg = reinterpret_cast(lParam); + if (g_wheelSupport == kWheelOld && msg->message == g_wmMouseWheel) { + // post message to our window + PostThreadMessage(g_threadID, + SYNERGY_MSG_MOUSE_WHEEL, + static_cast(msg->wParam & 0xffffu), 0); + + // zero out the delta in the message so it's (hopefully) + // ignored + msg->wParam = 0; + } + } + } + + return CallNextHookEx(g_getMessage, code, wParam, lParam); +} + +#if (_WIN32_WINNT >= 0x0400) && defined(_MSC_VER) && !NO_LOWLEVEL_HOOKS + +// +// low-level keyboard hook -- this allows us to capture and handle +// alt+tab, alt+esc, ctrl+esc, and windows key hot keys. on the down +// side, key repeats are not reported to us. +// + +#if !NO_GRAB_KEYBOARD +static +LRESULT CALLBACK +keyboardLLHook(int code, WPARAM wParam, LPARAM lParam) +{ + if (code >= 0) { + // decode the message + KBDLLHOOKSTRUCT* info = reinterpret_cast(lParam); + WPARAM wParam = info->vkCode; + LPARAM lParam = 1; // repeat code + lParam |= (info->scanCode << 16); // scan code + if (info->flags & LLKHF_EXTENDED) { + lParam |= (1lu << 24); // extended key + } + if (info->flags & LLKHF_ALTDOWN) { + lParam |= (1lu << 29); // context code + } + if (info->flags & LLKHF_UP) { + lParam |= (1lu << 31); // transition + } + // FIXME -- bit 30 should be set if key was already down but + // we don't know that info. as a result we'll never generate + // key repeat events. + + // handle the message + if (keyboardHookHandler(wParam, lParam)) { + return 1; + } + } + + return CallNextHookEx(g_keyboardLL, code, wParam, lParam); +} +#endif + +// +// low-level mouse hook -- this allows us to capture and handle mouse +// events very early. the earlier the better. +// + +static +LRESULT CALLBACK +mouseLLHook(int code, WPARAM wParam, LPARAM lParam) +{ + if (code >= 0) { + // decode the message + MSLLHOOKSTRUCT* info = reinterpret_cast(lParam); + SInt32 x = static_cast(info->pt.x); + SInt32 y = static_cast(info->pt.y); + SInt32 w = static_cast(HIWORD(info->mouseData)); + + // handle the message + if (mouseHookHandler(wParam, x, y, w)) { + return 1; + } + } + + return CallNextHookEx(g_mouseLL, code, wParam, lParam); +} + +#endif + +static +EWheelSupport +getWheelSupport() +{ + // get operating system + OSVERSIONINFO info; + info.dwOSVersionInfoSize = sizeof(info); + if (!GetVersionEx(&info)) { + return kWheelNone; + } + + // see if modern wheel is present + if (GetSystemMetrics(SM_MOUSEWHEELPRESENT)) { + // note if running on win2k + if (info.dwPlatformId == VER_PLATFORM_WIN32_NT && + info.dwMajorVersion == 5 && + info.dwMinorVersion == 0) { + return kWheelWin2000; + } + return kWheelModern; + } + + // not modern. see if we've got old-style support. +#if defined(MSH_WHEELSUPPORT) + UINT wheelSupportMsg = RegisterWindowMessage(MSH_WHEELSUPPORT); + HWND wheelSupportWindow = FindWindow(MSH_WHEELMODULE_CLASS, + MSH_WHEELMODULE_TITLE); + if (wheelSupportWindow != NULL && wheelSupportMsg != 0) { + if (SendMessage(wheelSupportWindow, wheelSupportMsg, 0, 0) != 0) { + g_wmMouseWheel = RegisterWindowMessage(MSH_MOUSEWHEEL); + if (g_wmMouseWheel != 0) { + return kWheelOld; + } + } + } +#endif + + // assume modern. we don't do anything special in this case + // except respond to WM_MOUSEWHEEL messages. GetSystemMetrics() + // can apparently return FALSE even if a mouse wheel is present + // though i'm not sure exactly when it does that (WinME returns + // FALSE for my logitech USB trackball). + return kWheelModern; +} + + +// +// external functions +// + +BOOL WINAPI +DllMain(HINSTANCE instance, DWORD reason, LPVOID) +{ + if (reason == DLL_PROCESS_ATTACH) { + DisableThreadLibraryCalls(instance); + if (g_processID == 0) { + g_hinstance = instance; + g_processID = GetCurrentProcessId(); + } + } + else if (reason == DLL_PROCESS_DETACH) { + if (g_processID == GetCurrentProcessId()) { + uninstall(); + uninstallScreenSaver(); + g_processID = 0; + g_hinstance = NULL; + } + } + return TRUE; +} + +extern "C" { + +// VS2005 hack to not link with the CRT +#if _MSC_VER >= 1400 +BOOL WINAPI _DllMainCRTStartup( + HINSTANCE instance, DWORD reason, LPVOID lpreserved) +{ + return DllMain(instance, reason, lpreserved); +} + +// VS2005 is a bit more bright than VC6 and optimize simple copy loop to +// intrinsic memcpy. +void * __cdecl memcpy(void * _Dst, const void * _Src, size_t _MaxCount) +{ + void * _DstBackup = _Dst; + switch (_MaxCount & 3) { + case 3: + ((char*)_Dst)[0] = ((char*)_Src)[0]; + ++(char*&)_Dst; + ++(char*&)_Src; + --_MaxCount; + case 2: + ((char*)_Dst)[0] = ((char*)_Src)[0]; + ++(char*&)_Dst; + ++(char*&)_Src; + --_MaxCount; + case 1: + ((char*)_Dst)[0] = ((char*)_Src)[0]; + ++(char*&)_Dst; + ++(char*&)_Src; + --_MaxCount; + break; + case 0: + break; + + default: + __assume(0); + break; + } + + // I think it's faster on intel to deference than modify the pointer. + const size_t max = _MaxCount / sizeof(UINT_PTR); + for (size_t i = 0; i < max; ++i) { + ((UINT_PTR*)_Dst)[i] = ((UINT_PTR*)_Src)[i]; + } + + (UINT_PTR*&)_Dst += max; + (UINT_PTR*&)_Src += max; + + switch (_MaxCount & 3) { + case 3: + ((char*)_Dst)[0] = ((char*)_Src)[0]; + ++(char*&)_Dst; + ++(char*&)_Src; + case 2: + ((char*)_Dst)[0] = ((char*)_Src)[0]; + ++(char*&)_Dst; + ++(char*&)_Src; + case 1: + ((char*)_Dst)[0] = ((char*)_Src)[0]; + ++(char*&)_Dst; + ++(char*&)_Src; + break; + case 0: + break; + + default: + __assume(0); + break; + } + + return _DstBackup; +} +#endif + +int +init(DWORD threadID) +{ + assert(g_hinstance != NULL); + + // try to open process that last called init() to see if it's + // still running or if it died without cleaning up. + if (g_processID != 0 && g_processID != GetCurrentProcessId()) { + HANDLE process = OpenProcess(STANDARD_RIGHTS_REQUIRED, + FALSE, g_processID); + if (process != NULL) { + // old process (probably) still exists so refuse to + // reinitialize this DLL (and thus steal it from the + // old process). + CloseHandle(process); + return 0; + } + + // clean up after old process. the system should've already + // removed the hooks so we just need to reset our state. + g_hinstance = GetModuleHandle(_T("synrgyhk")); + g_processID = GetCurrentProcessId(); + g_wheelSupport = kWheelNone; + g_threadID = 0; + g_keyboard = NULL; + g_mouse = NULL; + g_getMessage = NULL; + g_keyboardLL = NULL; + g_mouseLL = NULL; + g_screenSaver = false; + } + + // save thread id. we'll post messages to this thread's + // message queue. + g_threadID = threadID; + + // set defaults + g_mode = kHOOK_DISABLE; + g_zoneSides = 0; + g_zoneSize = 0; + g_xScreen = 0; + g_yScreen = 0; + g_wScreen = 0; + g_hScreen = 0; + + return 1; +} + +int +cleanup(void) +{ + assert(g_hinstance != NULL); + + if (g_processID == GetCurrentProcessId()) { + g_threadID = 0; + } + + return 1; +} + +EHookResult +install() +{ + assert(g_hinstance != NULL); + assert(g_keyboard == NULL); + assert(g_mouse == NULL); + assert(g_getMessage == NULL || g_screenSaver); + + // must be initialized + if (g_threadID == 0) { + return kHOOK_FAILED; + } + + // discard old dead keys + g_deadVirtKey = 0; + g_deadLParam = 0; + + // reset fake input flag + g_fakeInput = false; + + // check for mouse wheel support + g_wheelSupport = getWheelSupport(); + + // install GetMessage hook (unless already installed) + if (g_wheelSupport == kWheelOld && g_getMessage == NULL) { + g_getMessage = SetWindowsHookEx(WH_GETMESSAGE, + &getMessageHook, + g_hinstance, + 0); + } + + // install low-level hooks. we require that they both get installed. +#if (_WIN32_WINNT >= 0x0400) && defined(_MSC_VER) && !NO_LOWLEVEL_HOOKS + g_mouseLL = SetWindowsHookEx(WH_MOUSE_LL, + &mouseLLHook, + g_hinstance, + 0); +#if !NO_GRAB_KEYBOARD + g_keyboardLL = SetWindowsHookEx(WH_KEYBOARD_LL, + &keyboardLLHook, + g_hinstance, + 0); + if (g_mouseLL == NULL || g_keyboardLL == NULL) { + if (g_keyboardLL != NULL) { + UnhookWindowsHookEx(g_keyboardLL); + g_keyboardLL = NULL; + } + if (g_mouseLL != NULL) { + UnhookWindowsHookEx(g_mouseLL); + g_mouseLL = NULL; + } + } +#endif +#endif + + // install regular hooks + if (g_mouseLL == NULL) { + g_mouse = SetWindowsHookEx(WH_MOUSE, + &mouseHook, + g_hinstance, + 0); + } +#if !NO_GRAB_KEYBOARD + if (g_keyboardLL == NULL) { + g_keyboard = SetWindowsHookEx(WH_KEYBOARD, + &keyboardHook, + g_hinstance, + 0); + } +#endif + + // check that we got all the hooks we wanted + if ((g_getMessage == NULL && g_wheelSupport == kWheelOld) || +#if !NO_GRAB_KEYBOARD + (g_keyboardLL == NULL && g_keyboard == NULL) || +#endif + (g_mouseLL == NULL && g_mouse == NULL)) { + uninstall(); + return kHOOK_FAILED; + } + + if (g_keyboardLL != NULL || g_mouseLL != NULL) { + g_hookThread = GetCurrentThreadId(); + return kHOOK_OKAY_LL; + } + + return kHOOK_OKAY; +} + +int +uninstall(void) +{ + assert(g_hinstance != NULL); + + // discard old dead keys + g_deadVirtKey = 0; + g_deadLParam = 0; + + // detach from thread + detachThread(); + + // uninstall hooks + if (g_keyboardLL != NULL) { + UnhookWindowsHookEx(g_keyboardLL); + g_keyboardLL = NULL; + } + if (g_mouseLL != NULL) { + UnhookWindowsHookEx(g_mouseLL); + g_mouseLL = NULL; + } + if (g_keyboard != NULL) { + UnhookWindowsHookEx(g_keyboard); + g_keyboard = NULL; + } + if (g_mouse != NULL) { + UnhookWindowsHookEx(g_mouse); + g_mouse = NULL; + } + if (g_getMessage != NULL && !g_screenSaver) { + UnhookWindowsHookEx(g_getMessage); + g_getMessage = NULL; + } + g_wheelSupport = kWheelNone; + + return 1; +} + +int +installScreenSaver(void) +{ + assert(g_hinstance != NULL); + + // must be initialized + if (g_threadID == 0) { + return 0; + } + + // generate screen saver messages + g_screenSaver = true; + + // install hook unless it's already installed + if (g_getMessage == NULL) { + g_getMessage = SetWindowsHookEx(WH_GETMESSAGE, + &getMessageHook, + g_hinstance, + 0); + } + + return (g_getMessage != NULL) ? 1 : 0; +} + +int +uninstallScreenSaver(void) +{ + assert(g_hinstance != NULL); + + // uninstall hook unless the mouse wheel hook is installed + if (g_getMessage != NULL && g_wheelSupport != kWheelOld) { + UnhookWindowsHookEx(g_getMessage); + g_getMessage = NULL; + } + + // screen saver hook is no longer installed + g_screenSaver = false; + + return 1; +} + +void +setSides(UInt32 sides) +{ + g_zoneSides = sides; +} + +void +setZone(SInt32 x, SInt32 y, SInt32 w, SInt32 h, SInt32 jumpZoneSize) +{ + g_zoneSize = jumpZoneSize; + g_xScreen = x; + g_yScreen = y; + g_wScreen = w; + g_hScreen = h; +} + +void +setMode(EHookMode mode) +{ + if (mode == g_mode) { + // no change + return; + } + g_mode = mode; +} + +} diff --git a/src/lib/platform/CSynergyHook.h b/src/lib/platform/CSynergyHook.h new file mode 100644 index 00000000..5d2fafb2 --- /dev/null +++ b/src/lib/platform/CSynergyHook.h @@ -0,0 +1,95 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CSYNERGYHOOK_H +#define CSYNERGYHOOK_H + +// hack: vs2005 doesn't declare _WIN32_WINNT, so we need to hard code it. +// however, some say that this should be hard coded since it defines the +// target system, but since this is suposed to compile on pre-XP, maybe +// we should just leave it like this. +#if _MSC_VER == 1400 +#define _WIN32_WINNT 0x0400 +#endif + +#include "BasicTypes.h" +#define WIN32_LEAN_AND_MEAN +#include + +// fix: cmake defines the library name in lower case (synrgyhk_EXPORTS) as +// opposed to upper case (SYNRGYHK_EXPORTS), so rather than figuring out +// how to change cmake's behaviour, it's easier to just change the code. +#if defined(synrgyhk_EXPORTS) +#define CSYNERGYHOOK_API __declspec(dllexport) +#else +#define CSYNERGYHOOK_API __declspec(dllimport) +#endif + +#define SYNERGY_MSG_MARK WM_APP + 0x0011 // mark id; +#define SYNERGY_MSG_KEY WM_APP + 0x0012 // vk code; key data +#define SYNERGY_MSG_MOUSE_BUTTON WM_APP + 0x0013 // button msg; +#define SYNERGY_MSG_MOUSE_WHEEL WM_APP + 0x0014 // delta; +#define SYNERGY_MSG_MOUSE_MOVE WM_APP + 0x0015 // x; y +#define SYNERGY_MSG_POST_WARP WM_APP + 0x0016 // ; +#define SYNERGY_MSG_PRE_WARP WM_APP + 0x0017 // x; y +#define SYNERGY_MSG_SCREEN_SAVER WM_APP + 0x0018 // activated; +#define SYNERGY_MSG_DEBUG WM_APP + 0x0019 // data, data +#define SYNERGY_MSG_INPUT_FIRST SYNERGY_MSG_KEY +#define SYNERGY_MSG_INPUT_LAST SYNERGY_MSG_PRE_WARP +#define SYNERGY_HOOK_LAST_MSG SYNERGY_MSG_DEBUG + +#define SYNERGY_HOOK_FAKE_INPUT_VIRTUAL_KEY VK_CANCEL +#define SYNERGY_HOOK_FAKE_INPUT_SCANCODE 0 + +extern "C" { + +enum EHookResult { + kHOOK_FAILED, + kHOOK_OKAY, + kHOOK_OKAY_LL +}; + +enum EHookMode { + kHOOK_DISABLE, + kHOOK_WATCH_JUMP_ZONE, + kHOOK_RELAY_EVENTS +}; + +typedef int (*InitFunc)(DWORD targetQueueThreadID); +typedef int (*CleanupFunc)(void); +typedef EHookResult (*InstallFunc)(void); +typedef int (*UninstallFunc)(void); +typedef int (*InstallScreenSaverFunc)(void); +typedef int (*UninstallScreenSaverFunc)(void); +typedef void (*SetSidesFunc)(UInt32); +typedef void (*SetZoneFunc)(SInt32, SInt32, SInt32, SInt32, SInt32); +typedef void (*SetModeFunc)(int); + +CSYNERGYHOOK_API int init(DWORD); +CSYNERGYHOOK_API int cleanup(void); +CSYNERGYHOOK_API EHookResult install(void); +CSYNERGYHOOK_API int uninstall(void); +CSYNERGYHOOK_API int installScreenSaver(void); +CSYNERGYHOOK_API int uninstallScreenSaver(void); +CSYNERGYHOOK_API void setSides(UInt32 sides); +CSYNERGYHOOK_API void setZone(SInt32 x, SInt32 y, SInt32 w, SInt32 h, + SInt32 jumpZoneSize); +CSYNERGYHOOK_API void setMode(EHookMode mode); + +} + +#endif diff --git a/src/lib/platform/CXWindowsClipboard.cpp b/src/lib/platform/CXWindowsClipboard.cpp new file mode 100644 index 00000000..bf98fc9a --- /dev/null +++ b/src/lib/platform/CXWindowsClipboard.cpp @@ -0,0 +1,1510 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsClipboard.h" +#include "CXWindowsClipboardTextConverter.h" +#include "CXWindowsClipboardUCS2Converter.h" +#include "CXWindowsClipboardUTF8Converter.h" +#include "CXWindowsClipboardHTMLConverter.h" +#include "CXWindowsClipboardBMPConverter.h" +#include "CXWindowsUtil.h" +#include "CThread.h" +#include "CLog.h" +#include "CStopwatch.h" +#include "CArch.h" +#include "stdvector.h" +#include +#include + +// +// CXWindowsClipboard +// + +CXWindowsClipboard::CXWindowsClipboard(Display* display, + Window window, ClipboardID id) : + m_display(display), + m_window(window), + m_id(id), + m_open(false), + m_time(0), + m_owner(false), + m_timeOwned(0), + m_timeLost(0) +{ + // get some atoms + m_atomTargets = XInternAtom(m_display, "TARGETS", False); + m_atomMultiple = XInternAtom(m_display, "MULTIPLE", False); + m_atomTimestamp = XInternAtom(m_display, "TIMESTAMP", False); + m_atomInteger = XInternAtom(m_display, "INTEGER", False); + m_atomAtom = XInternAtom(m_display, "ATOM", False); + m_atomAtomPair = XInternAtom(m_display, "ATOM_PAIR", False); + m_atomData = XInternAtom(m_display, "CLIP_TEMPORARY", False); + m_atomINCR = XInternAtom(m_display, "INCR", False); + m_atomMotifClipLock = XInternAtom(m_display, "_MOTIF_CLIP_LOCK", False); + m_atomMotifClipHeader = XInternAtom(m_display, "_MOTIF_CLIP_HEADER", False); + m_atomMotifClipAccess = XInternAtom(m_display, + "_MOTIF_CLIP_LOCK_ACCESS_VALID", False); + m_atomGDKSelection = XInternAtom(m_display, "GDK_SELECTION", False); + + // set selection atom based on clipboard id + switch (id) { + case kClipboardClipboard: + m_selection = XInternAtom(m_display, "CLIPBOARD", False); + break; + + case kClipboardSelection: + default: + m_selection = XA_PRIMARY; + break; + } + + // add converters, most desired first + m_converters.push_back(new CXWindowsClipboardHTMLConverter(m_display, + "text/html")); + m_converters.push_back(new CXWindowsClipboardBMPConverter(m_display)); + m_converters.push_back(new CXWindowsClipboardUTF8Converter(m_display, + "text/plain;charset=UTF-8")); + m_converters.push_back(new CXWindowsClipboardUTF8Converter(m_display, + "UTF8_STRING")); + m_converters.push_back(new CXWindowsClipboardUCS2Converter(m_display, + "text/plain;charset=ISO-10646-UCS-2")); + m_converters.push_back(new CXWindowsClipboardUCS2Converter(m_display, + "text/unicode")); + m_converters.push_back(new CXWindowsClipboardTextConverter(m_display, + "text/plain")); + m_converters.push_back(new CXWindowsClipboardTextConverter(m_display, + "STRING")); + + // we have no data + clearCache(); +} + +CXWindowsClipboard::~CXWindowsClipboard() +{ + clearReplies(); + clearConverters(); +} + +void +CXWindowsClipboard::lost(Time time) +{ + LOG((CLOG_DEBUG "lost clipboard %d ownership at %d", m_id, time)); + if (m_owner) { + m_owner = false; + m_timeLost = time; + clearCache(); + } +} + +void +CXWindowsClipboard::addRequest(Window owner, Window requestor, + Atom target, ::Time time, Atom property) +{ + // must be for our window and we must have owned the selection + // at the given time. + bool success = false; + if (owner == m_window) { + LOG((CLOG_DEBUG1 "request for clipboard %d, target %s by 0x%08x (property=%s)", m_selection, CXWindowsUtil::atomToString(m_display, target).c_str(), requestor, CXWindowsUtil::atomToString(m_display, property).c_str())); + if (wasOwnedAtTime(time)) { + if (target == m_atomMultiple) { + // add a multiple request. property may not be None + // according to ICCCM. + if (property != None) { + success = insertMultipleReply(requestor, time, property); + } + } + else { + addSimpleRequest(requestor, target, time, property); + + // addSimpleRequest() will have already handled failure + success = true; + } + } + else { + LOG((CLOG_DEBUG1 "failed, not owned at time %d", time)); + } + } + + if (!success) { + // send failure + LOG((CLOG_DEBUG1 "failed")); + insertReply(new CReply(requestor, target, time)); + } + + // send notifications that are pending + pushReplies(); +} + +bool +CXWindowsClipboard::addSimpleRequest(Window requestor, + Atom target, ::Time time, Atom property) +{ + // obsolete requestors may supply a None property. in + // that case we use the target as the property to store + // the conversion. + if (property == None) { + property = target; + } + + // handle targets + CString data; + Atom type = None; + int format = 0; + if (target == m_atomTargets) { + type = getTargetsData(data, &format); + } + else if (target == m_atomTimestamp) { + type = getTimestampData(data, &format); + } + else { + IXWindowsClipboardConverter* converter = getConverter(target); + if (converter != NULL) { + IClipboard::EFormat clipboardFormat = converter->getFormat(); + if (m_added[clipboardFormat]) { + try { + data = converter->fromIClipboard(m_data[clipboardFormat]); + format = converter->getDataSize(); + type = converter->getAtom(); + } + catch (...) { + // ignore -- cannot convert + } + } + } + } + + if (type != None) { + // success + LOG((CLOG_DEBUG1 "success")); + insertReply(new CReply(requestor, target, time, + property, data, type, format)); + return true; + } + else { + // failure + LOG((CLOG_DEBUG1 "failed")); + insertReply(new CReply(requestor, target, time)); + return false; + } +} + +bool +CXWindowsClipboard::processRequest(Window requestor, + ::Time /*time*/, Atom property) +{ + CReplyMap::iterator index = m_replies.find(requestor); + if (index == m_replies.end()) { + // unknown requestor window + return false; + } + LOG((CLOG_DEBUG1 "received property %s delete from 0x08%x", CXWindowsUtil::atomToString(m_display, property).c_str(), requestor)); + + // find the property in the known requests. it should be the + // first property but we'll check 'em all if we have to. + CReplyList& replies = index->second; + for (CReplyList::iterator index2 = replies.begin(); + index2 != replies.end(); ++index2) { + CReply* reply = *index2; + if (reply->m_replied && reply->m_property == property) { + // if reply is complete then remove it and start the + // next one. + pushReplies(index, replies, index2); + return true; + } + } + + return false; +} + +bool +CXWindowsClipboard::destroyRequest(Window requestor) +{ + CReplyMap::iterator index = m_replies.find(requestor); + if (index == m_replies.end()) { + // unknown requestor window + return false; + } + + // destroy all replies for this window + clearReplies(index->second); + m_replies.erase(index); + + // note -- we don't stop watching the window for events because + // we're called in response to the window being destroyed. + + return true; +} + +Window +CXWindowsClipboard::getWindow() const +{ + return m_window; +} + +Atom +CXWindowsClipboard::getSelection() const +{ + return m_selection; +} + +bool +CXWindowsClipboard::empty() +{ + assert(m_open); + + LOG((CLOG_DEBUG "empty clipboard %d", m_id)); + + // assert ownership of clipboard + XSetSelectionOwner(m_display, m_selection, m_window, m_time); + if (XGetSelectionOwner(m_display, m_selection) != m_window) { + LOG((CLOG_DEBUG "failed to grab clipboard %d", m_id)); + return false; + } + + // clear all data. since we own the data now, the cache is up + // to date. + clearCache(); + m_cached = true; + + // FIXME -- actually delete motif clipboard items? + // FIXME -- do anything to motif clipboard properties? + + // save time + m_timeOwned = m_time; + m_timeLost = 0; + + // we're the owner now + m_owner = true; + LOG((CLOG_DEBUG "grabbed clipboard %d", m_id)); + + return true; +} + +void +CXWindowsClipboard::add(EFormat format, const CString& data) +{ + assert(m_open); + assert(m_owner); + + LOG((CLOG_DEBUG "add %d bytes to clipboard %d format: %d", data.size(), m_id, format)); + + m_data[format] = data; + m_added[format] = true; + + // FIXME -- set motif clipboard item? +} + +bool +CXWindowsClipboard::open(Time time) const +{ + assert(!m_open); + + LOG((CLOG_DEBUG "open clipboard %d", m_id)); + + // assume not motif + m_motif = false; + + // lock clipboard + if (m_id == kClipboardClipboard) { + if (!motifLockClipboard()) { + return false; + } + + // check if motif owns the selection. unlock motif clipboard + // if it does not. + m_motif = motifOwnsClipboard(); + LOG((CLOG_DEBUG1 "motif does %sown clipboard", m_motif ? "" : "not ")); + if (!m_motif) { + motifUnlockClipboard(); + } + } + + // now open + m_open = true; + m_time = time; + + // be sure to flush the cache later if it's dirty + m_checkCache = true; + + return true; +} + +void +CXWindowsClipboard::close() const +{ + assert(m_open); + + LOG((CLOG_DEBUG "close clipboard %d", m_id)); + + // unlock clipboard + if (m_motif) { + motifUnlockClipboard(); + } + + m_motif = false; + m_open = false; +} + +IClipboard::Time +CXWindowsClipboard::getTime() const +{ + checkCache(); + return m_timeOwned; +} + +bool +CXWindowsClipboard::has(EFormat format) const +{ + assert(m_open); + + fillCache(); + return m_added[format]; +} + +CString +CXWindowsClipboard::get(EFormat format) const +{ + assert(m_open); + + fillCache(); + return m_data[format]; +} + +void +CXWindowsClipboard::clearConverters() +{ + for (ConverterList::iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + delete *index; + } + m_converters.clear(); +} + +IXWindowsClipboardConverter* +CXWindowsClipboard::getConverter(Atom target, bool onlyIfNotAdded) const +{ + IXWindowsClipboardConverter* converter = NULL; + for (ConverterList::const_iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + converter = *index; + if (converter->getAtom() == target) { + break; + } + } + if (converter == NULL) { + LOG((CLOG_DEBUG1 " no converter for target %s", CXWindowsUtil::atomToString(m_display, target).c_str())); + return NULL; + } + + // optionally skip already handled targets + if (onlyIfNotAdded) { + if (m_added[converter->getFormat()]) { + LOG((CLOG_DEBUG1 " skipping handled format %d", converter->getFormat())); + return NULL; + } + } + + return converter; +} + +void +CXWindowsClipboard::checkCache() const +{ + if (!m_checkCache) { + return; + } + m_checkCache = false; + + // get the time the clipboard ownership was taken by the current + // owner. + if (m_motif) { + m_timeOwned = motifGetTime(); + } + else { + m_timeOwned = icccmGetTime(); + } + + // if we can't get the time then use the time passed to us + if (m_timeOwned == 0) { + m_timeOwned = m_time; + } + + // if the cache is dirty then flush it + if (m_timeOwned != m_cacheTime) { + clearCache(); + } +} + +void +CXWindowsClipboard::clearCache() const +{ + const_cast(this)->doClearCache(); +} + +void +CXWindowsClipboard::doClearCache() +{ + m_checkCache = false; + m_cached = false; + for (SInt32 index = 0; index < kNumFormats; ++index) { + m_data[index] = ""; + m_added[index] = false; + } +} + +void +CXWindowsClipboard::fillCache() const +{ + // get the selection data if not already cached + checkCache(); + if (!m_cached) { + const_cast(this)->doFillCache(); + } +} + +void +CXWindowsClipboard::doFillCache() +{ + if (m_motif) { + motifFillCache(); + } + else { + icccmFillCache(); + } + m_checkCache = false; + m_cached = true; + m_cacheTime = m_timeOwned; +} + +void +CXWindowsClipboard::icccmFillCache() +{ + LOG((CLOG_DEBUG "ICCCM fill clipboard %d", m_id)); + + // see if we can get the list of available formats from the selection. + // if not then use a default list of formats. note that some clipboard + // owners are broken and report TARGETS as the type of the TARGETS data + // instead of the correct type ATOM; allow either. + const Atom atomTargets = m_atomTargets; + Atom target; + CString data; + if (!icccmGetSelection(atomTargets, &target, &data) || + (target != m_atomAtom && target != m_atomTargets)) { + LOG((CLOG_DEBUG1 "selection doesn't support TARGETS")); + data = ""; + CXWindowsUtil::appendAtomData(data, XA_STRING); + } + + CXWindowsUtil::convertAtomProperty(data); + const Atom* targets = reinterpret_cast(data.data()); + const UInt32 numTargets = data.size() / sizeof(Atom); + LOG((CLOG_DEBUG " available targets: %s", CXWindowsUtil::atomsToString(m_display, targets, numTargets).c_str())); + + // try each converter in order (because they're in order of + // preference). + for (ConverterList::const_iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + IXWindowsClipboardConverter* converter = *index; + + // skip already handled targets + if (m_added[converter->getFormat()]) { + continue; + } + + // see if atom is in target list + Atom target = None; + // XXX -- just ask for the converter's target to see if it's + // available rather than checking TARGETS. i've seen clipboard + // owners that don't report all the targets they support. + target = converter->getAtom(); + /* + for (UInt32 i = 0; i < numTargets; ++i) { + if (converter->getAtom() == targets[i]) { + target = targets[i]; + break; + } + } + */ + if (target == None) { + continue; + } + + // get the data + Atom actualTarget; + CString targetData; + if (!icccmGetSelection(target, &actualTarget, &targetData)) { + LOG((CLOG_DEBUG1 " no data for target %s", CXWindowsUtil::atomToString(m_display, target).c_str())); + continue; + } + + // add to clipboard and note we've done it + IClipboard::EFormat format = converter->getFormat(); + m_data[format] = converter->toIClipboard(targetData); + m_added[format] = true; + LOG((CLOG_DEBUG " added format %d for target %s (%u %s)", format, CXWindowsUtil::atomToString(m_display, target).c_str(), targetData.size(), targetData.size() == 1 ? "byte" : "bytes")); + } +} + +bool +CXWindowsClipboard::icccmGetSelection(Atom target, + Atom* actualTarget, CString* data) const +{ + assert(actualTarget != NULL); + assert(data != NULL); + + // request data conversion + CICCCMGetClipboard getter(m_window, m_time, m_atomData); + if (!getter.readClipboard(m_display, m_selection, + target, actualTarget, data)) { + LOG((CLOG_DEBUG1 "can't get data for selection target %s", CXWindowsUtil::atomToString(m_display, target).c_str())); + LOGC(getter.m_error, (CLOG_WARN "ICCCM violation by clipboard owner")); + return false; + } + else if (*actualTarget == None) { + LOG((CLOG_DEBUG1 "selection conversion failed for target %s", CXWindowsUtil::atomToString(m_display, target).c_str())); + return false; + } + return true; +} + +IClipboard::Time +CXWindowsClipboard::icccmGetTime() const +{ + Atom actualTarget; + CString data; + if (icccmGetSelection(m_atomTimestamp, &actualTarget, &data) && + actualTarget == m_atomInteger) { + Time time = *reinterpret_cast(data.data()); + LOG((CLOG_DEBUG1 "got ICCCM time %d", time)); + return time; + } + else { + // no timestamp + LOG((CLOG_DEBUG1 "can't get ICCCM time")); + return 0; + } +} + +bool +CXWindowsClipboard::motifLockClipboard() const +{ + // fail if anybody owns the lock (even us, so this is non-recursive) + Window lockOwner = XGetSelectionOwner(m_display, m_atomMotifClipLock); + if (lockOwner != None) { + LOG((CLOG_DEBUG1 "motif lock owner 0x%08x", lockOwner)); + return false; + } + + // try to grab the lock + // FIXME -- is this right? there's a race condition here -- + // A grabs successfully, B grabs successfully, A thinks it + // still has the grab until it gets a SelectionClear. + Time time = CXWindowsUtil::getCurrentTime(m_display, m_window); + XSetSelectionOwner(m_display, m_atomMotifClipLock, m_window, time); + lockOwner = XGetSelectionOwner(m_display, m_atomMotifClipLock); + if (lockOwner != m_window) { + LOG((CLOG_DEBUG1 "motif lock owner 0x%08x", lockOwner)); + return false; + } + + LOG((CLOG_DEBUG1 "locked motif clipboard")); + return true; +} + +void +CXWindowsClipboard::motifUnlockClipboard() const +{ + LOG((CLOG_DEBUG1 "unlocked motif clipboard")); + + // fail if we don't own the lock + Window lockOwner = XGetSelectionOwner(m_display, m_atomMotifClipLock); + if (lockOwner != m_window) { + return; + } + + // release lock + Time time = CXWindowsUtil::getCurrentTime(m_display, m_window); + XSetSelectionOwner(m_display, m_atomMotifClipLock, None, time); +} + +bool +CXWindowsClipboard::motifOwnsClipboard() const +{ + // get the current selection owner + // FIXME -- this can't be right. even if the window is destroyed + // Motif will still have a valid clipboard. how can we tell if + // some other client owns CLIPBOARD? + Window owner = XGetSelectionOwner(m_display, m_selection); + if (owner == None) { + return false; + } + + // get the Motif clipboard header property from the root window + Atom target; + SInt32 format; + CString data; + Window root = RootWindow(m_display, DefaultScreen(m_display)); + if (!CXWindowsUtil::getWindowProperty(m_display, root, + m_atomMotifClipHeader, + &data, &target, &format, False)) { + return false; + } + + // check the owner window against the current clipboard owner + const CMotifClipHeader* header = + reinterpret_cast(data.data()); + if (data.size() >= sizeof(CMotifClipHeader) && + header->m_id == kMotifClipHeader) { + if (static_cast(header->m_selectionOwner) == owner) { + return true; + } + } + + return false; +} + +void +CXWindowsClipboard::motifFillCache() +{ + LOG((CLOG_DEBUG "Motif fill clipboard %d", m_id)); + + // get the Motif clipboard header property from the root window + Atom target; + SInt32 format; + CString data; + Window root = RootWindow(m_display, DefaultScreen(m_display)); + if (!CXWindowsUtil::getWindowProperty(m_display, root, + m_atomMotifClipHeader, + &data, &target, &format, False)) { + return; + } + + // check that the header is okay + const CMotifClipHeader* header = + reinterpret_cast(data.data()); + if (data.size() < sizeof(CMotifClipHeader) || + header->m_id != kMotifClipHeader || + header->m_numItems < 1) { + return; + } + + // get the Motif item property from the root window + char name[18 + 20]; + sprintf(name, "_MOTIF_CLIP_ITEM_%d", header->m_item); + Atom atomItem = XInternAtom(m_display, name, False); + data = ""; + if (!CXWindowsUtil::getWindowProperty(m_display, root, + atomItem, &data, + &target, &format, False)) { + return; + } + + // check that the item is okay + const CMotifClipItem* item = + reinterpret_cast(data.data()); + if (data.size() < sizeof(CMotifClipItem) || + item->m_id != kMotifClipItem || + item->m_numFormats - item->m_numDeletedFormats < 1) { + return; + } + + // format list is after static item structure elements + const SInt32 numFormats = item->m_numFormats - item->m_numDeletedFormats; + const SInt32* formats = reinterpret_cast(item->m_size + + reinterpret_cast(data.data())); + + // get the available formats + typedef std::map CMotifFormatMap; + CMotifFormatMap motifFormats; + for (SInt32 i = 0; i < numFormats; ++i) { + // get Motif format property from the root window + sprintf(name, "_MOTIF_CLIP_ITEM_%d", formats[i]); + Atom atomFormat = XInternAtom(m_display, name, False); + CString data; + if (!CXWindowsUtil::getWindowProperty(m_display, root, + atomFormat, &data, + &target, &format, False)) { + continue; + } + + // check that the format is okay + const CMotifClipFormat* motifFormat = + reinterpret_cast(data.data()); + if (data.size() < sizeof(CMotifClipFormat) || + motifFormat->m_id != kMotifClipFormat || + motifFormat->m_length < 0 || + motifFormat->m_type == None || + motifFormat->m_deleted != 0) { + continue; + } + + // save it + motifFormats.insert(std::make_pair(motifFormat->m_type, data)); + } + //const UInt32 numMotifFormats = motifFormats.size(); + + // try each converter in order (because they're in order of + // preference). + for (ConverterList::const_iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + IXWindowsClipboardConverter* converter = *index; + + // skip already handled targets + if (m_added[converter->getFormat()]) { + continue; + } + + // see if atom is in target list + CMotifFormatMap::const_iterator index2 = + motifFormats.find(converter->getAtom()); + if (index2 == motifFormats.end()) { + continue; + } + + // get format + const CMotifClipFormat* motifFormat = + reinterpret_cast( + index2->second.data()); + const Atom target = motifFormat->m_type; + + // get the data (finally) + Atom actualTarget; + CString targetData; + if (!motifGetSelection(motifFormat, &actualTarget, &targetData)) { + LOG((CLOG_DEBUG1 " no data for target %s", CXWindowsUtil::atomToString(m_display, target).c_str())); + continue; + } + + // add to clipboard and note we've done it + IClipboard::EFormat format = converter->getFormat(); + m_data[format] = converter->toIClipboard(targetData); + m_added[format] = true; + LOG((CLOG_DEBUG " added format %d for target %s", format, CXWindowsUtil::atomToString(m_display, target).c_str())); + } +} + +bool +CXWindowsClipboard::motifGetSelection(const CMotifClipFormat* format, + Atom* actualTarget, CString* data) const +{ + // if the current clipboard owner and the owner indicated by the + // motif clip header are the same then transfer via a property on + // the root window, otherwise transfer as a normal ICCCM client. + if (!motifOwnsClipboard()) { + return icccmGetSelection(format->m_type, actualTarget, data); + } + + // use motif way + // FIXME -- this isn't right. it'll only work if the data is + // already stored on the root window and only if it fits in a + // property. motif has some scheme for transferring part by + // part that i don't know. + char name[18 + 20]; + sprintf(name, "_MOTIF_CLIP_ITEM_%d", format->m_data); + Atom target = XInternAtom(m_display, name, False); + Window root = RootWindow(m_display, DefaultScreen(m_display)); + return CXWindowsUtil::getWindowProperty(m_display, root, + target, data, + actualTarget, NULL, False); +} + +IClipboard::Time +CXWindowsClipboard::motifGetTime() const +{ + return icccmGetTime(); +} + +bool +CXWindowsClipboard::insertMultipleReply(Window requestor, + ::Time time, Atom property) +{ + // get the requested targets + Atom target; + SInt32 format; + CString data; + if (!CXWindowsUtil::getWindowProperty(m_display, requestor, + property, &data, &target, &format, False)) { + // can't get the requested targets + return false; + } + + // fail if the requested targets isn't of the correct form + if (format != 32 || target != m_atomAtomPair) { + return false; + } + + // data is a list of atom pairs: target, property + CXWindowsUtil::convertAtomProperty(data); + const Atom* targets = reinterpret_cast(data.data()); + const UInt32 numTargets = data.size() / sizeof(Atom); + + // add replies for each target + bool changed = false; + for (UInt32 i = 0; i < numTargets; i += 2) { + const Atom target = targets[i + 0]; + const Atom property = targets[i + 1]; + if (!addSimpleRequest(requestor, target, time, property)) { + // note that we can't perform the requested conversion + CXWindowsUtil::replaceAtomData(data, i, None); + changed = true; + } + } + + // update the targets property if we changed it + if (changed) { + CXWindowsUtil::setWindowProperty(m_display, requestor, + property, data.data(), data.size(), + target, format); + } + + // add reply for MULTIPLE request + insertReply(new CReply(requestor, m_atomMultiple, + time, property, CString(), None, 32)); + + return true; +} + +void +CXWindowsClipboard::insertReply(CReply* reply) +{ + assert(reply != NULL); + + // note -- we must respond to requests in order if requestor,target,time + // are the same, otherwise we can use whatever order we like with one + // exception: each reply in a MULTIPLE reply must be handled in order + // as well. those replies will almost certainly not share targets so + // we can't simply use requestor,target,time as map index. + // + // instead we'll use just the requestor. that's more restrictive than + // necessary but we're guaranteed to do things in the right order. + // note that we could also include the time in the map index and still + // ensure the right order. but since that'll just make it harder to + // find the right reply when handling property notify events we stick + // to just the requestor. + + const bool newWindow = (m_replies.count(reply->m_requestor) == 0); + m_replies[reply->m_requestor].push_back(reply); + + // adjust requestor's event mask if we haven't done so already. we + // want events in case the window is destroyed or any of its + // properties change. + if (newWindow) { + // note errors while we adjust event masks + bool error = false; + { + CXWindowsUtil::CErrorLock lock(m_display, &error); + + // get and save the current event mask + XWindowAttributes attr; + XGetWindowAttributes(m_display, reply->m_requestor, &attr); + m_eventMasks[reply->m_requestor] = attr.your_event_mask; + + // add the events we want + XSelectInput(m_display, reply->m_requestor, attr.your_event_mask | + StructureNotifyMask | PropertyChangeMask); + } + + // if we failed then the window has already been destroyed + if (error) { + m_replies.erase(reply->m_requestor); + delete reply; + } + } +} + +void +CXWindowsClipboard::pushReplies() +{ + // send the first reply for each window if that reply hasn't + // been sent yet. + for (CReplyMap::iterator index = m_replies.begin(); + index != m_replies.end(); ) { + assert(!index->second.empty()); + if (!index->second.front()->m_replied) { + pushReplies(index, index->second, index->second.begin()); + } + else { + ++index; + } + } +} + +void +CXWindowsClipboard::pushReplies(CReplyMap::iterator& mapIndex, + CReplyList& replies, CReplyList::iterator index) +{ + CReply* reply = *index; + while (sendReply(reply)) { + // reply is complete. discard it and send the next reply, + // if any. + index = replies.erase(index); + delete reply; + if (index == replies.end()) { + break; + } + reply = *index; + } + + // if there are no more replies in the list then remove the list + // and stop watching the requestor for events. + if (replies.empty()) { + CXWindowsUtil::CErrorLock lock(m_display); + Window requestor = mapIndex->first; + XSelectInput(m_display, requestor, m_eventMasks[requestor]); + m_replies.erase(mapIndex++); + m_eventMasks.erase(requestor); + } + else { + ++mapIndex; + } +} + +bool +CXWindowsClipboard::sendReply(CReply* reply) +{ + assert(reply != NULL); + + // bail out immediately if reply is done + if (reply->m_done) { + LOG((CLOG_DEBUG1 "clipboard: finished reply to 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property)); + return true; + } + + // start in failed state if property is None + bool failed = (reply->m_property == None); + if (!failed) { + LOG((CLOG_DEBUG1 "clipboard: setting property on 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property)); + + // send using INCR if already sending incrementally or if reply + // is too large, otherwise just send it. + const UInt32 maxRequestSize = 3 * XMaxRequestSize(m_display); + const bool useINCR = (reply->m_data.size() > maxRequestSize); + + // send INCR reply if incremental and we haven't replied yet + if (useINCR && !reply->m_replied) { + UInt32 size = reply->m_data.size(); + if (!CXWindowsUtil::setWindowProperty(m_display, + reply->m_requestor, reply->m_property, + &size, 4, m_atomINCR, 32)) { + failed = true; + } + } + + // send more INCR reply or entire non-incremental reply + else { + // how much more data should we send? + UInt32 size = reply->m_data.size() - reply->m_ptr; + if (size > maxRequestSize) + size = maxRequestSize; + + // send it + if (!CXWindowsUtil::setWindowProperty(m_display, + reply->m_requestor, reply->m_property, + reply->m_data.data() + reply->m_ptr, + size, + reply->m_type, reply->m_format)) { + failed = true; + } + else { + reply->m_ptr += size; + + // we've finished the reply if we just sent the zero + // size incremental chunk or if we're not incremental. + reply->m_done = (size == 0 || !useINCR); + } + } + } + + // if we've failed then delete the property and say we're done. + // if we haven't replied yet then we can send a failure notify, + // otherwise we've failed in the middle of an incremental + // transfer; i don't know how to cancel that so i'll just send + // the final zero-length property. + // FIXME -- how do you gracefully cancel an incremental transfer? + if (failed) { + LOG((CLOG_DEBUG1 "clipboard: sending failure to 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property)); + reply->m_done = true; + if (reply->m_property != None) { + CXWindowsUtil::CErrorLock lock(m_display); + XDeleteProperty(m_display, reply->m_requestor, reply->m_property); + } + + if (!reply->m_replied) { + sendNotify(reply->m_requestor, m_selection, + reply->m_target, None, + reply->m_time); + + // don't wait for any reply (because we're not expecting one) + return true; + } + else { + static const char dummy = 0; + CXWindowsUtil::setWindowProperty(m_display, + reply->m_requestor, reply->m_property, + &dummy, + 0, + reply->m_type, reply->m_format); + + // wait for delete notify + return false; + } + } + + // send notification if we haven't yet + if (!reply->m_replied) { + LOG((CLOG_DEBUG1 "clipboard: sending notify to 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property)); + reply->m_replied = true; + + // dump every property on the requestor window to the debug2 + // log. we've seen what appears to be a bug in lesstif and + // knowing the properties may help design a workaround, if + // it becomes necessary. + if (CLOG->getFilter() >= kDEBUG2) { + CXWindowsUtil::CErrorLock lock(m_display); + int n; + Atom* props = XListProperties(m_display, reply->m_requestor, &n); + LOG((CLOG_DEBUG2 "properties of 0x%08x:", reply->m_requestor)); + for (int i = 0; i < n; ++i) { + Atom target; + CString data; + char* name = XGetAtomName(m_display, props[i]); + if (!CXWindowsUtil::getWindowProperty(m_display, + reply->m_requestor, + props[i], &data, &target, NULL, False)) { + LOG((CLOG_DEBUG2 " %s: ", name)); + } + else { + // if there are any non-ascii characters in string + // then print the binary data. + static const char* hex = "0123456789abcdef"; + for (CString::size_type j = 0; j < data.size(); ++j) { + if (data[j] < 32 || data[j] > 126) { + CString tmp; + tmp.reserve(data.size() * 3); + for (j = 0; j < data.size(); ++j) { + unsigned char v = (unsigned char)data[j]; + tmp += hex[v >> 16]; + tmp += hex[v & 15]; + tmp += ' '; + } + data = tmp; + break; + } + } + char* type = XGetAtomName(m_display, target); + LOG((CLOG_DEBUG2 " %s (%s): %s", name, type, data.c_str())); + if (type != NULL) { + XFree(type); + } + } + if (name != NULL) { + XFree(name); + } + } + if (props != NULL) { + XFree(props); + } + } + + sendNotify(reply->m_requestor, m_selection, + reply->m_target, reply->m_property, + reply->m_time); + } + + // wait for delete notify + return false; +} + +void +CXWindowsClipboard::clearReplies() +{ + for (CReplyMap::iterator index = m_replies.begin(); + index != m_replies.end(); ++index) { + clearReplies(index->second); + } + m_replies.clear(); + m_eventMasks.clear(); +} + +void +CXWindowsClipboard::clearReplies(CReplyList& replies) +{ + for (CReplyList::iterator index = replies.begin(); + index != replies.end(); ++index) { + delete *index; + } + replies.clear(); +} + +void +CXWindowsClipboard::sendNotify(Window requestor, + Atom selection, Atom target, Atom property, Time time) +{ + XEvent event; + event.xselection.type = SelectionNotify; + event.xselection.display = m_display; + event.xselection.requestor = requestor; + event.xselection.selection = selection; + event.xselection.target = target; + event.xselection.property = property; + event.xselection.time = time; + CXWindowsUtil::CErrorLock lock(m_display); + XSendEvent(m_display, requestor, False, 0, &event); +} + +bool +CXWindowsClipboard::wasOwnedAtTime(::Time time) const +{ + // not owned if we've never owned the selection + checkCache(); + if (m_timeOwned == 0) { + return false; + } + + // if time is CurrentTime then return true if we still own the + // selection and false if we do not. else if we still own the + // selection then get the current time, otherwise use + // m_timeLost as the end time. + Time lost = m_timeLost; + if (m_timeLost == 0) { + if (time == CurrentTime) { + return true; + } + else { + lost = CXWindowsUtil::getCurrentTime(m_display, m_window); + } + } + else { + if (time == CurrentTime) { + return false; + } + } + + // compare time to range + Time duration = lost - m_timeOwned; + Time when = time - m_timeOwned; + return (/*when >= 0 &&*/ when <= duration); +} + +Atom +CXWindowsClipboard::getTargetsData(CString& data, int* format) const +{ + assert(format != NULL); + + // add standard targets + CXWindowsUtil::appendAtomData(data, m_atomTargets); + CXWindowsUtil::appendAtomData(data, m_atomMultiple); + CXWindowsUtil::appendAtomData(data, m_atomTimestamp); + + // add targets we can convert to + for (ConverterList::const_iterator index = m_converters.begin(); + index != m_converters.end(); ++index) { + IXWindowsClipboardConverter* converter = *index; + + // skip formats we don't have + if (m_added[converter->getFormat()]) { + CXWindowsUtil::appendAtomData(data, converter->getAtom()); + } + } + + *format = 32; + return m_atomAtom; +} + +Atom +CXWindowsClipboard::getTimestampData(CString& data, int* format) const +{ + assert(format != NULL); + + checkCache(); + CXWindowsUtil::appendTimeData(data, m_timeOwned); + *format = 32; + return m_atomInteger; +} + + +// +// CXWindowsClipboard::CICCCMGetClipboard +// + +CXWindowsClipboard::CICCCMGetClipboard::CICCCMGetClipboard( + Window requestor, Time time, Atom property) : + m_requestor(requestor), + m_time(time), + m_property(property), + m_incr(false), + m_failed(false), + m_done(false), + m_reading(false), + m_data(NULL), + m_actualTarget(NULL), + m_error(false) +{ + // do nothing +} + +CXWindowsClipboard::CICCCMGetClipboard::~CICCCMGetClipboard() +{ + // do nothing +} + +bool +CXWindowsClipboard::CICCCMGetClipboard::readClipboard(Display* display, + Atom selection, Atom target, Atom* actualTarget, CString* data) +{ + assert(actualTarget != NULL); + assert(data != NULL); + + LOG((CLOG_DEBUG1 "request selection=%s, target=%s, window=%x", CXWindowsUtil::atomToString(display, selection).c_str(), CXWindowsUtil::atomToString(display, target).c_str(), m_requestor)); + + m_atomNone = XInternAtom(display, "NONE", False); + m_atomIncr = XInternAtom(display, "INCR", False); + + // save output pointers + m_actualTarget = actualTarget; + m_data = data; + + // assume failure + *m_actualTarget = None; + *m_data = ""; + + // delete target property + XDeleteProperty(display, m_requestor, m_property); + + // select window for property changes + XWindowAttributes attr; + XGetWindowAttributes(display, m_requestor, &attr); + XSelectInput(display, m_requestor, + attr.your_event_mask | PropertyChangeMask); + + // request data conversion + XConvertSelection(display, selection, target, + m_property, m_requestor, m_time); + + // synchronize with server before we start following timeout countdown + XSync(display, False); + + // Xlib inexplicably omits the ability to wait for an event with + // a timeout. (it's inexplicable because there's no portable way + // to do it.) we'll poll until we have what we're looking for or + // a timeout expires. we use a timeout so we don't get locked up + // by badly behaved selection owners. + XEvent xevent; + std::vector events; + CStopwatch timeout(true); + static const double s_timeout = 0.25; // FIXME -- is this too short? + bool noWait = false; + while (!m_done && !m_failed) { + // fail if timeout has expired + if (timeout.getTime() >= s_timeout) { + m_failed = true; + break; + } + + // process events if any otherwise sleep + if (noWait || XPending(display) > 0) { + while (!m_done && !m_failed && (noWait || XPending(display) > 0)) { + XNextEvent(display, &xevent); + if (!processEvent(display, &xevent)) { + // not processed so save it + events.push_back(xevent); + } + else { + // reset timer since we've made some progress + timeout.reset(); + + // don't sleep anymore, just block waiting for events. + // we're assuming here that the clipboard owner will + // complete the protocol correctly. if we continue to + // sleep we'll get very bad performance. + noWait = true; + } + } + } + else { + ARCH->sleep(0.01); + } + } + + // put unprocessed events back + for (UInt32 i = events.size(); i > 0; --i) { + XPutBackEvent(display, &events[i - 1]); + } + + // restore mask + XSelectInput(display, m_requestor, attr.your_event_mask); + + // return success or failure + LOG((CLOG_DEBUG1 "request %s", m_failed ? "failed" : "succeeded")); + return !m_failed; +} + +bool +CXWindowsClipboard::CICCCMGetClipboard::processEvent( + Display* display, XEvent* xevent) +{ + // process event + switch (xevent->type) { + case DestroyNotify: + if (xevent->xdestroywindow.window == m_requestor) { + m_failed = true; + return true; + } + + // not interested + return false; + + case SelectionNotify: + if (xevent->xselection.requestor == m_requestor) { + // done if we can't convert + if (xevent->xselection.property == None || + xevent->xselection.property == m_atomNone) { + m_done = true; + return true; + } + + // proceed if conversion successful + else if (xevent->xselection.property == m_property) { + m_reading = true; + break; + } + } + + // otherwise not interested + return false; + + case PropertyNotify: + // proceed if conversion successful and we're receiving more data + if (xevent->xproperty.window == m_requestor && + xevent->xproperty.atom == m_property && + xevent->xproperty.state == PropertyNewValue) { + if (!m_reading) { + // we haven't gotten the SelectionNotify yet + return true; + } + break; + } + + // otherwise not interested + return false; + + default: + // not interested + return false; + } + + // get the data from the property + Atom target; + const CString::size_type oldSize = m_data->size(); + if (!CXWindowsUtil::getWindowProperty(display, m_requestor, + m_property, m_data, &target, NULL, True)) { + // unable to read property + m_failed = true; + return true; + } + + // note if incremental. if we're already incremental then the + // selection owner is busted. if the INCR property has no size + // then the selection owner is busted. + if (target == m_atomIncr) { + if (m_incr) { + m_failed = true; + m_error = true; + } + else if (m_data->size() == oldSize) { + m_failed = true; + m_error = true; + } + else { + m_incr = true; + + // discard INCR data + *m_data = ""; + } + } + + // handle incremental chunks + else if (m_incr) { + // if first incremental chunk then save target + if (oldSize == 0) { + LOG((CLOG_DEBUG1 " INCR first chunk, target %s", CXWindowsUtil::atomToString(display, target).c_str())); + *m_actualTarget = target; + } + + // secondary chunks must have the same target + else { + if (target != *m_actualTarget) { + LOG((CLOG_WARN " INCR target mismatch")); + m_failed = true; + m_error = true; + } + } + + // note if this is the final chunk + if (m_data->size() == oldSize) { + LOG((CLOG_DEBUG1 " INCR final chunk: %d bytes total", m_data->size())); + m_done = true; + } + } + + // not incremental; save the target. + else { + LOG((CLOG_DEBUG1 " target %s", CXWindowsUtil::atomToString(display, target).c_str())); + *m_actualTarget = target; + m_done = true; + } + + // this event has been processed + LOGC(!m_incr, (CLOG_DEBUG1 " got data, %d bytes", m_data->size())); + return true; +} + + +// +// CXWindowsClipboard::CReply +// + +CXWindowsClipboard::CReply::CReply(Window requestor, Atom target, ::Time time) : + m_requestor(requestor), + m_target(target), + m_time(time), + m_property(None), + m_replied(false), + m_done(false), + m_data(), + m_type(None), + m_format(32), + m_ptr(0) +{ + // do nothing +} + +CXWindowsClipboard::CReply::CReply(Window requestor, Atom target, ::Time time, + Atom property, const CString& data, Atom type, int format) : + m_requestor(requestor), + m_target(target), + m_time(time), + m_property(property), + m_replied(false), + m_done(false), + m_data(data), + m_type(type), + m_format(format), + m_ptr(0) +{ + // do nothing +} diff --git a/src/lib/platform/CXWindowsClipboard.h b/src/lib/platform/CXWindowsClipboard.h new file mode 100644 index 00000000..edb431ab --- /dev/null +++ b/src/lib/platform/CXWindowsClipboard.h @@ -0,0 +1,379 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSCLIPBOARD_H +#define CXWINDOWSCLIPBOARD_H + +#include "IClipboard.h" +#include "ClipboardTypes.h" +#include "stdmap.h" +#include "stdlist.h" +#include "stdvector.h" +#if X_DISPLAY_MISSING +# error X11 is required to build synergy +#else +# include +#endif + +class IXWindowsClipboardConverter; + +//! X11 clipboard implementation +class CXWindowsClipboard : public IClipboard { +public: + /*! + Use \c window as the window that owns or interacts with the + clipboard identified by \c id. + */ + CXWindowsClipboard(Display*, Window window, ClipboardID id); + virtual ~CXWindowsClipboard(); + + //! Notify clipboard was lost + /*! + Tells clipboard it lost ownership at the given time. + */ + void lost(Time); + + //! Add clipboard request + /*! + Adds a selection request to the request list. If the given + owner window isn't this clipboard's window then this simply + sends a failure event to the requestor. + */ + void addRequest(Window owner, + Window requestor, Atom target, + ::Time time, Atom property); + + //! Process clipboard request + /*! + Continues processing a selection request. Returns true if the + request was handled, false if the request was unknown. + */ + bool processRequest(Window requestor, + ::Time time, Atom property); + + //! Cancel clipboard request + /*! + Terminate a selection request. Returns true iff the request + was known and handled. + */ + bool destroyRequest(Window requestor); + + //! Get window + /*! + Returns the clipboard's window (passed the c'tor). + */ + Window getWindow() const; + + //! Get selection atom + /*! + Returns the selection atom that identifies the clipboard to X11 + (e.g. XA_PRIMARY). + */ + Atom getSelection() const; + + // IClipboard overrides + virtual bool empty(); + virtual void add(EFormat, const CString& data); + virtual bool open(Time) const; + virtual void close() const; + virtual Time getTime() const; + virtual bool has(EFormat) const; + virtual CString get(EFormat) const; + +private: + // remove all converters from our list + void clearConverters(); + + // get the converter for a clipboard format. returns NULL if no + // suitable converter. iff onlyIfNotAdded is true then also + // return NULL if a suitable converter was found but we already + // have data of the converter's clipboard format. + IXWindowsClipboardConverter* + getConverter(Atom target, + bool onlyIfNotAdded = false) const; + + // convert target atom to clipboard format + EFormat getFormat(Atom target) const; + + // add a non-MULTIPLE request. does not verify that the selection + // was owned at the given time. returns true if the conversion + // could be performed, false otherwise. in either case, the + // reply is inserted. + bool addSimpleRequest( + Window requestor, Atom target, + ::Time time, Atom property); + + // if not already checked then see if the cache is stale and, if so, + // clear it. this has the side effect of updating m_timeOwned. + void checkCache() const; + + // clear the cache, resetting the cached flag and the added flag for + // each format. + void clearCache() const; + void doClearCache(); + + // cache all formats of the selection + void fillCache() const; + void doFillCache(); + + // + // helper classes + // + + // read an ICCCM conforming selection + class CICCCMGetClipboard { + public: + CICCCMGetClipboard(Window requestor, Time time, Atom property); + ~CICCCMGetClipboard(); + + // convert the given selection to the given type. returns + // true iff the conversion was successful or the conversion + // cannot be performed (in which case *actualTarget == None). + bool readClipboard(Display* display, + Atom selection, Atom target, + Atom* actualTarget, CString* data); + + private: + bool processEvent(Display* display, XEvent* event); + + private: + Window m_requestor; + Time m_time; + Atom m_property; + bool m_incr; + bool m_failed; + bool m_done; + + // atoms needed for the protocol + Atom m_atomNone; // NONE, not None + Atom m_atomIncr; + + // true iff we've received the selection notify + bool m_reading; + + // the converted selection data + CString* m_data; + + // the actual type of the data. if this is None then the + // selection owner cannot convert to the requested type. + Atom* m_actualTarget; + + public: + // true iff the selection owner didn't follow ICCCM conventions + bool m_error; + }; + + // Motif structure IDs + enum { kMotifClipFormat = 1, kMotifClipItem, kMotifClipHeader }; + + // _MOTIF_CLIP_HEADER structure + class CMotifClipHeader { + public: + SInt32 m_id; // kMotifClipHeader + SInt32 m_pad1[3]; + SInt32 m_item; + SInt32 m_pad2[4]; + SInt32 m_numItems; + SInt32 m_pad3[3]; + SInt32 m_selectionOwner; // a Window + SInt32 m_pad4[2]; + }; + + // Motif clip item structure + class CMotifClipItem { + public: + SInt32 m_id; // kMotifClipItem + SInt32 m_pad1[5]; + SInt32 m_size; + SInt32 m_numFormats; + SInt32 m_numDeletedFormats; + SInt32 m_pad2[6]; + }; + + // Motif clip format structure + class CMotifClipFormat { + public: + SInt32 m_id; // kMotifClipFormat + SInt32 m_pad1[6]; + SInt32 m_length; + SInt32 m_data; + SInt32 m_type; // an Atom + SInt32 m_pad2[1]; + SInt32 m_deleted; + SInt32 m_pad3[4]; + }; + + // stores data needed to respond to a selection request + class CReply { + public: + CReply(Window, Atom target, ::Time); + CReply(Window, Atom target, ::Time, Atom property, + const CString& data, Atom type, int format); + + public: + // information about the request + Window m_requestor; + Atom m_target; + ::Time m_time; + Atom m_property; + + // true iff we've sent the notification for this reply + bool m_replied; + + // true iff the reply has sent its last message + bool m_done; + + // the data to send and its type and format + CString m_data; + Atom m_type; + int m_format; + + // index of next byte in m_data to send + UInt32 m_ptr; + }; + typedef std::list CReplyList; + typedef std::map CReplyMap; + typedef std::map CReplyEventMask; + + // ICCCM interoperability methods + void icccmFillCache(); + bool icccmGetSelection(Atom target, + Atom* actualTarget, CString* data) const; + Time icccmGetTime() const; + + // motif interoperability methods + bool motifLockClipboard() const; + void motifUnlockClipboard() const; + bool motifOwnsClipboard() const; + void motifFillCache(); + bool motifGetSelection(const CMotifClipFormat*, + Atom* actualTarget, CString* data) const; + Time motifGetTime() const; + + // reply methods + bool insertMultipleReply(Window, ::Time, Atom); + void insertReply(CReply*); + void pushReplies(); + void pushReplies(CReplyMap::iterator&, + CReplyList&, CReplyList::iterator); + bool sendReply(CReply*); + void clearReplies(); + void clearReplies(CReplyList&); + void sendNotify(Window requestor, Atom selection, + Atom target, Atom property, Time time); + bool wasOwnedAtTime(::Time) const; + + // data conversion methods + Atom getTargetsData(CString&, int* format) const; + Atom getTimestampData(CString&, int* format) const; + +private: + typedef std::vector ConverterList; + + Display* m_display; + Window m_window; + ClipboardID m_id; + Atom m_selection; + mutable bool m_open; + mutable Time m_time; + bool m_owner; + mutable Time m_timeOwned; + Time m_timeLost; + + // true iff open and clipboard owned by a motif app + mutable bool m_motif; + + // the added/cached clipboard data + mutable bool m_checkCache; + bool m_cached; + Time m_cacheTime; + bool m_added[kNumFormats]; + CString m_data[kNumFormats]; + + // conversion request replies + CReplyMap m_replies; + CReplyEventMask m_eventMasks; + + // clipboard format converters + ConverterList m_converters; + + // atoms we'll need + Atom m_atomTargets; + Atom m_atomMultiple; + Atom m_atomTimestamp; + Atom m_atomInteger; + Atom m_atomAtom; + Atom m_atomAtomPair; + Atom m_atomData; + Atom m_atomINCR; + Atom m_atomMotifClipLock; + Atom m_atomMotifClipHeader; + Atom m_atomMotifClipAccess; + Atom m_atomGDKSelection; +}; + +//! Clipboard format converter interface +/*! +This interface defines the methods common to all X11 clipboard format +converters. +*/ +class IXWindowsClipboardConverter : public IInterface { +public: + //! @name accessors + //@{ + + //! Get clipboard format + /*! + Return the clipboard format this object converts from/to. + */ + virtual IClipboard::EFormat + getFormat() const = 0; + + //! Get X11 format atom + /*! + Return the atom representing the X selection format that + this object converts from/to. + */ + virtual Atom getAtom() const = 0; + + //! Get X11 property datum size + /*! + Return the size (in bits) of data elements returned by + toIClipboard(). + */ + virtual int getDataSize() const = 0; + + //! Convert from IClipboard format + /*! + Convert from the IClipboard format to the X selection format. + The input data must be in the IClipboard format returned by + getFormat(). The return data will be in the X selection + format returned by getAtom(). + */ + virtual CString fromIClipboard(const CString&) const = 0; + + //! Convert to IClipboard format + /*! + Convert from the X selection format to the IClipboard format + (i.e., the reverse of fromIClipboard()). + */ + virtual CString toIClipboard(const CString&) const = 0; + + //@} +}; + +#endif diff --git a/src/lib/platform/CXWindowsClipboardAnyBitmapConverter.cpp b/src/lib/platform/CXWindowsClipboardAnyBitmapConverter.cpp new file mode 100644 index 00000000..d0511703 --- /dev/null +++ b/src/lib/platform/CXWindowsClipboardAnyBitmapConverter.cpp @@ -0,0 +1,190 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsClipboardAnyBitmapConverter.h" + +// BMP info header structure +struct CBMPInfoHeader { +public: + UInt32 biSize; + SInt32 biWidth; + SInt32 biHeight; + UInt16 biPlanes; + UInt16 biBitCount; + UInt32 biCompression; + UInt32 biSizeImage; + SInt32 biXPelsPerMeter; + SInt32 biYPelsPerMeter; + UInt32 biClrUsed; + UInt32 biClrImportant; +}; + +// BMP is little-endian + +static +void +toLE(UInt8*& dst, UInt16 src) +{ + dst[0] = static_cast(src & 0xffu); + dst[1] = static_cast((src >> 8) & 0xffu); + dst += 2; +} + +static +void +toLE(UInt8*& dst, SInt32 src) +{ + dst[0] = static_cast(src & 0xffu); + dst[1] = static_cast((src >> 8) & 0xffu); + dst[2] = static_cast((src >> 16) & 0xffu); + dst[3] = static_cast((src >> 24) & 0xffu); + dst += 4; +} + +static +void +toLE(UInt8*& dst, UInt32 src) +{ + dst[0] = static_cast(src & 0xffu); + dst[1] = static_cast((src >> 8) & 0xffu); + dst[2] = static_cast((src >> 16) & 0xffu); + dst[3] = static_cast((src >> 24) & 0xffu); + dst += 4; +} + +static inline +UInt16 +fromLEU16(const UInt8* data) +{ + return static_cast(data[0]) | + (static_cast(data[1]) << 8); +} + +static inline +SInt32 +fromLES32(const UInt8* data) +{ + return static_cast(static_cast(data[0]) | + (static_cast(data[1]) << 8) | + (static_cast(data[2]) << 16) | + (static_cast(data[3]) << 24)); +} + +static inline +UInt32 +fromLEU32(const UInt8* data) +{ + return static_cast(data[0]) | + (static_cast(data[1]) << 8) | + (static_cast(data[2]) << 16) | + (static_cast(data[3]) << 24); +} + + +// +// CXWindowsClipboardAnyBitmapConverter +// + +CXWindowsClipboardAnyBitmapConverter::CXWindowsClipboardAnyBitmapConverter() +{ + // do nothing +} + +CXWindowsClipboardAnyBitmapConverter::~CXWindowsClipboardAnyBitmapConverter() +{ + // do nothing +} + +IClipboard::EFormat +CXWindowsClipboardAnyBitmapConverter::getFormat() const +{ + return IClipboard::kBitmap; +} + +int +CXWindowsClipboardAnyBitmapConverter::getDataSize() const +{ + return 8; +} + +CString +CXWindowsClipboardAnyBitmapConverter::fromIClipboard(const CString& bmp) const +{ + // fill BMP info header with native-endian data + CBMPInfoHeader infoHeader; + const UInt8* rawBMPInfoHeader = reinterpret_cast(bmp.data()); + infoHeader.biSize = fromLEU32(rawBMPInfoHeader + 0); + infoHeader.biWidth = fromLES32(rawBMPInfoHeader + 4); + infoHeader.biHeight = fromLES32(rawBMPInfoHeader + 8); + infoHeader.biPlanes = fromLEU16(rawBMPInfoHeader + 12); + infoHeader.biBitCount = fromLEU16(rawBMPInfoHeader + 14); + infoHeader.biCompression = fromLEU32(rawBMPInfoHeader + 16); + infoHeader.biSizeImage = fromLEU32(rawBMPInfoHeader + 20); + infoHeader.biXPelsPerMeter = fromLES32(rawBMPInfoHeader + 24); + infoHeader.biYPelsPerMeter = fromLES32(rawBMPInfoHeader + 28); + infoHeader.biClrUsed = fromLEU32(rawBMPInfoHeader + 32); + infoHeader.biClrImportant = fromLEU32(rawBMPInfoHeader + 36); + + // check that format is acceptable + if (infoHeader.biSize != 40 || + infoHeader.biWidth == 0 || infoHeader.biHeight == 0 || + infoHeader.biPlanes != 0 || infoHeader.biCompression != 0 || + (infoHeader.biBitCount != 24 && infoHeader.biBitCount != 32)) { + return CString(); + } + + // convert to image format + const UInt8* rawBMPPixels = rawBMPInfoHeader + 40; + if (infoHeader.biBitCount == 24) { + return doBGRFromIClipboard(rawBMPPixels, + infoHeader.biWidth, infoHeader.biHeight); + } + else { + return doBGRAFromIClipboard(rawBMPPixels, + infoHeader.biWidth, infoHeader.biHeight); + } +} + +CString +CXWindowsClipboardAnyBitmapConverter::toIClipboard(const CString& image) const +{ + // convert to raw BMP data + UInt32 w, h, depth; + CString rawBMP = doToIClipboard(image, w, h, depth); + if (rawBMP.empty() || w == 0 || h == 0 || (depth != 24 && depth != 32)) { + return CString(); + } + + // fill BMP info header with little-endian data + UInt8 infoHeader[40]; + UInt8* dst = infoHeader; + toLE(dst, static_cast(40)); + toLE(dst, static_cast(w)); + toLE(dst, static_cast(h)); + toLE(dst, static_cast(1)); + toLE(dst, static_cast(depth)); + toLE(dst, static_cast(0)); // BI_RGB + toLE(dst, static_cast(image.size())); + toLE(dst, static_cast(2834)); // 72 dpi + toLE(dst, static_cast(2834)); // 72 dpi + toLE(dst, static_cast(0)); + toLE(dst, static_cast(0)); + + // construct image + return CString(reinterpret_cast(infoHeader), + sizeof(infoHeader)) + rawBMP; +} diff --git a/src/lib/platform/CXWindowsClipboardAnyBitmapConverter.h b/src/lib/platform/CXWindowsClipboardAnyBitmapConverter.h new file mode 100644 index 00000000..97887245 --- /dev/null +++ b/src/lib/platform/CXWindowsClipboardAnyBitmapConverter.h @@ -0,0 +1,62 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSCLIPBOARDANYBITMAPCONVERTER_H +#define CXWINDOWSCLIPBOARDANYBITMAPCONVERTER_H + +#include "CXWindowsClipboard.h" + +//! Convert to/from some text encoding +class CXWindowsClipboardAnyBitmapConverter : + public IXWindowsClipboardConverter { +public: + CXWindowsClipboardAnyBitmapConverter(); + virtual ~CXWindowsClipboardAnyBitmapConverter(); + + // IXWindowsClipboardConverter overrides + virtual IClipboard::EFormat + getFormat() const; + virtual Atom getAtom() const = 0; + virtual int getDataSize() const; + virtual CString fromIClipboard(const CString&) const; + virtual CString toIClipboard(const CString&) const; + +protected: + //! Convert from IClipboard format + /*! + Convert raw BGR pixel data to another image format. + */ + virtual CString doBGRFromIClipboard(const UInt8* bgrData, + UInt32 w, UInt32 h) const = 0; + + //! Convert from IClipboard format + /*! + Convert raw BGRA pixel data to another image format. + */ + virtual CString doBGRAFromIClipboard(const UInt8* bgrData, + UInt32 w, UInt32 h) const = 0; + + //! Convert to IClipboard format + /*! + Convert an image into raw BGR or BGRA image data and store the + width, height, and image depth (24 or 32). + */ + virtual CString doToIClipboard(const CString&, + UInt32& w, UInt32& h, UInt32& depth) const = 0; +}; + +#endif diff --git a/src/lib/platform/CXWindowsClipboardBMPConverter.cpp b/src/lib/platform/CXWindowsClipboardBMPConverter.cpp new file mode 100644 index 00000000..26707607 --- /dev/null +++ b/src/lib/platform/CXWindowsClipboardBMPConverter.cpp @@ -0,0 +1,142 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsClipboardBMPConverter.h" + +// BMP file header structure +struct CBMPHeader { +public: + UInt16 type; + UInt32 size; + UInt16 reserved1; + UInt16 reserved2; + UInt32 offset; +}; + +// BMP is little-endian +static inline +UInt32 +fromLEU32(const UInt8* data) +{ + return static_cast(data[0]) | + (static_cast(data[1]) << 8) | + (static_cast(data[2]) << 16) | + (static_cast(data[3]) << 24); +} + +static +void +toLE(UInt8*& dst, char src) +{ + dst[0] = static_cast(src); + dst += 1; +} + +static +void +toLE(UInt8*& dst, UInt16 src) +{ + dst[0] = static_cast(src & 0xffu); + dst[1] = static_cast((src >> 8) & 0xffu); + dst += 2; +} + +static +void +toLE(UInt8*& dst, UInt32 src) +{ + dst[0] = static_cast(src & 0xffu); + dst[1] = static_cast((src >> 8) & 0xffu); + dst[2] = static_cast((src >> 16) & 0xffu); + dst[3] = static_cast((src >> 24) & 0xffu); + dst += 4; +} + +// +// CXWindowsClipboardBMPConverter +// + +CXWindowsClipboardBMPConverter::CXWindowsClipboardBMPConverter( + Display* display) : + m_atom(XInternAtom(display, "image/bmp", False)) +{ + // do nothing +} + +CXWindowsClipboardBMPConverter::~CXWindowsClipboardBMPConverter() +{ + // do nothing +} + +IClipboard::EFormat +CXWindowsClipboardBMPConverter::getFormat() const +{ + return IClipboard::kBitmap; +} + +Atom +CXWindowsClipboardBMPConverter::getAtom() const +{ + return m_atom; +} + +int +CXWindowsClipboardBMPConverter::getDataSize() const +{ + return 8; +} + +CString +CXWindowsClipboardBMPConverter::fromIClipboard(const CString& bmp) const +{ + // create BMP image + UInt8 header[14]; + UInt8* dst = header; + toLE(dst, 'B'); + toLE(dst, 'M'); + toLE(dst, static_cast(14 + bmp.size())); + toLE(dst, static_cast(0)); + toLE(dst, static_cast(0)); + toLE(dst, static_cast(14 + 40)); + return CString(reinterpret_cast(header), 14) + bmp; +} + +CString +CXWindowsClipboardBMPConverter::toIClipboard(const CString& bmp) const +{ + // make sure data is big enough for a BMP file + if (bmp.size() <= 14 + 40) { + return CString(); + } + + // check BMP file header + const UInt8* rawBMPHeader = reinterpret_cast(bmp.data()); + if (rawBMPHeader[0] != 'B' || rawBMPHeader[1] != 'M') { + return CString(); + } + + // get offset to image data + UInt32 offset = fromLEU32(rawBMPHeader + 10); + + // construct BMP + if (offset == 14 + 40) { + return bmp.substr(14); + } + else { + return bmp.substr(14, 40) + bmp.substr(offset, bmp.size() - offset); + } +} diff --git a/src/lib/platform/CXWindowsClipboardBMPConverter.h b/src/lib/platform/CXWindowsClipboardBMPConverter.h new file mode 100644 index 00000000..881ab467 --- /dev/null +++ b/src/lib/platform/CXWindowsClipboardBMPConverter.h @@ -0,0 +1,42 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSCLIPBOARDBMPCONVERTER_H +#define CXWINDOWSCLIPBOARDBMPCONVERTER_H + +#include "CXWindowsClipboard.h" + +//! Convert to/from some text encoding +class CXWindowsClipboardBMPConverter : + public IXWindowsClipboardConverter { +public: + CXWindowsClipboardBMPConverter(Display* display); + virtual ~CXWindowsClipboardBMPConverter(); + + // IXWindowsClipboardConverter overrides + virtual IClipboard::EFormat + getFormat() const; + virtual Atom getAtom() const; + virtual int getDataSize() const; + virtual CString fromIClipboard(const CString&) const; + virtual CString toIClipboard(const CString&) const; + +private: + Atom m_atom; +}; + +#endif diff --git a/src/lib/platform/CXWindowsClipboardHTMLConverter.cpp b/src/lib/platform/CXWindowsClipboardHTMLConverter.cpp new file mode 100644 index 00000000..fa0886fd --- /dev/null +++ b/src/lib/platform/CXWindowsClipboardHTMLConverter.cpp @@ -0,0 +1,65 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsClipboardHTMLConverter.h" +#include "CUnicode.h" + +// +// CXWindowsClipboardHTMLConverter +// + +CXWindowsClipboardHTMLConverter::CXWindowsClipboardHTMLConverter( + Display* display, const char* name) : + m_atom(XInternAtom(display, name, False)) +{ + // do nothing +} + +CXWindowsClipboardHTMLConverter::~CXWindowsClipboardHTMLConverter() +{ + // do nothing +} + +IClipboard::EFormat +CXWindowsClipboardHTMLConverter::getFormat() const +{ + return IClipboard::kHTML; +} + +Atom +CXWindowsClipboardHTMLConverter::getAtom() const +{ + return m_atom; +} + +int +CXWindowsClipboardHTMLConverter::getDataSize() const +{ + return 8; +} + +CString +CXWindowsClipboardHTMLConverter::fromIClipboard(const CString& data) const +{ + return CUnicode::UTF8ToUTF16(data); +} + +CString +CXWindowsClipboardHTMLConverter::toIClipboard(const CString& data) const +{ + return CUnicode::UTF16ToUTF8(data); +} diff --git a/src/lib/platform/CXWindowsClipboardHTMLConverter.h b/src/lib/platform/CXWindowsClipboardHTMLConverter.h new file mode 100644 index 00000000..d83e3f4d --- /dev/null +++ b/src/lib/platform/CXWindowsClipboardHTMLConverter.h @@ -0,0 +1,44 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSCLIPBOARDHTMLCONVERTER_H +#define CXWINDOWSCLIPBOARDHTMLCONVERTER_H + +#include "CXWindowsClipboard.h" + +//! Convert to/from HTML encoding +class CXWindowsClipboardHTMLConverter : public IXWindowsClipboardConverter { +public: + /*! + \c name is converted to an atom and that is reported by getAtom(). + */ + CXWindowsClipboardHTMLConverter(Display* display, const char* name); + virtual ~CXWindowsClipboardHTMLConverter(); + + // IXWindowsClipboardConverter overrides + virtual IClipboard::EFormat + getFormat() const; + virtual Atom getAtom() const; + virtual int getDataSize() const; + virtual CString fromIClipboard(const CString&) const; + virtual CString toIClipboard(const CString&) const; + +private: + Atom m_atom; +}; + +#endif diff --git a/src/lib/platform/CXWindowsClipboardTextConverter.cpp b/src/lib/platform/CXWindowsClipboardTextConverter.cpp new file mode 100644 index 00000000..10a03bfd --- /dev/null +++ b/src/lib/platform/CXWindowsClipboardTextConverter.cpp @@ -0,0 +1,77 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsClipboardTextConverter.h" +#include "CUnicode.h" + +// +// CXWindowsClipboardTextConverter +// + +CXWindowsClipboardTextConverter::CXWindowsClipboardTextConverter( + Display* display, const char* name) : + m_atom(XInternAtom(display, name, False)) +{ + // do nothing +} + +CXWindowsClipboardTextConverter::~CXWindowsClipboardTextConverter() +{ + // do nothing +} + +IClipboard::EFormat +CXWindowsClipboardTextConverter::getFormat() const +{ + return IClipboard::kText; +} + +Atom +CXWindowsClipboardTextConverter::getAtom() const +{ + return m_atom; +} + +int +CXWindowsClipboardTextConverter::getDataSize() const +{ + return 8; +} + +CString +CXWindowsClipboardTextConverter::fromIClipboard(const CString& data) const +{ + return CUnicode::UTF8ToText(data); +} + +CString +CXWindowsClipboardTextConverter::toIClipboard(const CString& data) const +{ + // convert to UTF-8 + bool errors; + CString utf8 = CUnicode::textToUTF8(data, &errors); + + // if there were decoding errors then, to support old applications + // that don't understand UTF-8 but can report the exact binary + // UTF-8 representation, see if the data appears to be UTF-8. if + // so then use it as is. + if (errors && CUnicode::isUTF8(data)) { + return data; + } + + return utf8; +} diff --git a/src/lib/platform/CXWindowsClipboardTextConverter.h b/src/lib/platform/CXWindowsClipboardTextConverter.h new file mode 100644 index 00000000..43455d1d --- /dev/null +++ b/src/lib/platform/CXWindowsClipboardTextConverter.h @@ -0,0 +1,44 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSCLIPBOARDTEXTCONVERTER_H +#define CXWINDOWSCLIPBOARDTEXTCONVERTER_H + +#include "CXWindowsClipboard.h" + +//! Convert to/from locale text encoding +class CXWindowsClipboardTextConverter : public IXWindowsClipboardConverter { +public: + /*! + \c name is converted to an atom and that is reported by getAtom(). + */ + CXWindowsClipboardTextConverter(Display* display, const char* name); + virtual ~CXWindowsClipboardTextConverter(); + + // IXWindowsClipboardConverter overrides + virtual IClipboard::EFormat + getFormat() const; + virtual Atom getAtom() const; + virtual int getDataSize() const; + virtual CString fromIClipboard(const CString&) const; + virtual CString toIClipboard(const CString&) const; + +private: + Atom m_atom; +}; + +#endif diff --git a/src/lib/platform/CXWindowsClipboardUCS2Converter.cpp b/src/lib/platform/CXWindowsClipboardUCS2Converter.cpp new file mode 100644 index 00000000..554cd130 --- /dev/null +++ b/src/lib/platform/CXWindowsClipboardUCS2Converter.cpp @@ -0,0 +1,65 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsClipboardUCS2Converter.h" +#include "CUnicode.h" + +// +// CXWindowsClipboardUCS2Converter +// + +CXWindowsClipboardUCS2Converter::CXWindowsClipboardUCS2Converter( + Display* display, const char* name) : + m_atom(XInternAtom(display, name, False)) +{ + // do nothing +} + +CXWindowsClipboardUCS2Converter::~CXWindowsClipboardUCS2Converter() +{ + // do nothing +} + +IClipboard::EFormat +CXWindowsClipboardUCS2Converter::getFormat() const +{ + return IClipboard::kText; +} + +Atom +CXWindowsClipboardUCS2Converter::getAtom() const +{ + return m_atom; +} + +int +CXWindowsClipboardUCS2Converter::getDataSize() const +{ + return 16; +} + +CString +CXWindowsClipboardUCS2Converter::fromIClipboard(const CString& data) const +{ + return CUnicode::UTF8ToUCS2(data); +} + +CString +CXWindowsClipboardUCS2Converter::toIClipboard(const CString& data) const +{ + return CUnicode::UCS2ToUTF8(data); +} diff --git a/src/lib/platform/CXWindowsClipboardUCS2Converter.h b/src/lib/platform/CXWindowsClipboardUCS2Converter.h new file mode 100644 index 00000000..47f4cba6 --- /dev/null +++ b/src/lib/platform/CXWindowsClipboardUCS2Converter.h @@ -0,0 +1,44 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSCLIPBOARDUCS2CONVERTER_H +#define CXWINDOWSCLIPBOARDUCS2CONVERTER_H + +#include "CXWindowsClipboard.h" + +//! Convert to/from UCS-2 encoding +class CXWindowsClipboardUCS2Converter : public IXWindowsClipboardConverter { +public: + /*! + \c name is converted to an atom and that is reported by getAtom(). + */ + CXWindowsClipboardUCS2Converter(Display* display, const char* name); + virtual ~CXWindowsClipboardUCS2Converter(); + + // IXWindowsClipboardConverter overrides + virtual IClipboard::EFormat + getFormat() const; + virtual Atom getAtom() const; + virtual int getDataSize() const; + virtual CString fromIClipboard(const CString&) const; + virtual CString toIClipboard(const CString&) const; + +private: + Atom m_atom; +}; + +#endif diff --git a/src/lib/platform/CXWindowsClipboardUTF8Converter.cpp b/src/lib/platform/CXWindowsClipboardUTF8Converter.cpp new file mode 100644 index 00000000..30263d8f --- /dev/null +++ b/src/lib/platform/CXWindowsClipboardUTF8Converter.cpp @@ -0,0 +1,64 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsClipboardUTF8Converter.h" + +// +// CXWindowsClipboardUTF8Converter +// + +CXWindowsClipboardUTF8Converter::CXWindowsClipboardUTF8Converter( + Display* display, const char* name) : + m_atom(XInternAtom(display, name, False)) +{ + // do nothing +} + +CXWindowsClipboardUTF8Converter::~CXWindowsClipboardUTF8Converter() +{ + // do nothing +} + +IClipboard::EFormat +CXWindowsClipboardUTF8Converter::getFormat() const +{ + return IClipboard::kText; +} + +Atom +CXWindowsClipboardUTF8Converter::getAtom() const +{ + return m_atom; +} + +int +CXWindowsClipboardUTF8Converter::getDataSize() const +{ + return 8; +} + +CString +CXWindowsClipboardUTF8Converter::fromIClipboard(const CString& data) const +{ + return data; +} + +CString +CXWindowsClipboardUTF8Converter::toIClipboard(const CString& data) const +{ + return data; +} diff --git a/src/lib/platform/CXWindowsClipboardUTF8Converter.h b/src/lib/platform/CXWindowsClipboardUTF8Converter.h new file mode 100644 index 00000000..e6ba1588 --- /dev/null +++ b/src/lib/platform/CXWindowsClipboardUTF8Converter.h @@ -0,0 +1,44 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSCLIPBOARDUTF8CONVERTER_H +#define CXWINDOWSCLIPBOARDUTF8CONVERTER_H + +#include "CXWindowsClipboard.h" + +//! Convert to/from UTF-8 encoding +class CXWindowsClipboardUTF8Converter : public IXWindowsClipboardConverter { +public: + /*! + \c name is converted to an atom and that is reported by getAtom(). + */ + CXWindowsClipboardUTF8Converter(Display* display, const char* name); + virtual ~CXWindowsClipboardUTF8Converter(); + + // IXWindowsClipboardConverter overrides + virtual IClipboard::EFormat + getFormat() const; + virtual Atom getAtom() const; + virtual int getDataSize() const; + virtual CString fromIClipboard(const CString&) const; + virtual CString toIClipboard(const CString&) const; + +private: + Atom m_atom; +}; + +#endif diff --git a/src/lib/platform/CXWindowsEventQueueBuffer.cpp b/src/lib/platform/CXWindowsEventQueueBuffer.cpp new file mode 100644 index 00000000..eb0a8ae5 --- /dev/null +++ b/src/lib/platform/CXWindowsEventQueueBuffer.cpp @@ -0,0 +1,287 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsEventQueueBuffer.h" +#include "CLock.h" +#include "CThread.h" +#include "CEvent.h" +#include "IEventQueue.h" +#include +#if HAVE_UNISTD_H +# include +#endif +#if HAVE_POLL +# include +#else +# if HAVE_SYS_SELECT_H +# include +# endif +# if HAVE_SYS_TIME_H +# include +# endif +# if HAVE_SYS_TYPES_H +# include +# endif +#endif + +// +// CEventQueueTimer +// + +class CEventQueueTimer { }; + + +// +// CXWindowsEventQueueBuffer +// + +CXWindowsEventQueueBuffer::CXWindowsEventQueueBuffer( + Display* display, Window window) : + m_display(display), + m_window(window), + m_waiting(false) +{ + assert(m_display != NULL); + assert(m_window != None); + + m_userEvent = XInternAtom(m_display, "SYNERGY_USER_EVENT", False); + // set up for pipe hack + int result = pipe(m_pipefd); + assert(result == 0); + + int pipeflags; + pipeflags = fcntl(m_pipefd[0], F_GETFL); + fcntl(m_pipefd[0], F_SETFL, pipeflags | O_NONBLOCK); + pipeflags = fcntl(m_pipefd[1], F_GETFL); + fcntl(m_pipefd[1], F_SETFL, pipeflags | O_NONBLOCK); +} + +CXWindowsEventQueueBuffer::~CXWindowsEventQueueBuffer() +{ + // release pipe hack resources + close(m_pipefd[0]); + close(m_pipefd[1]); +} + +void +CXWindowsEventQueueBuffer::waitForEvent(double dtimeout) +{ + CThread::testCancel(); + + // clear out the pipe in preparation for waiting. + + char buf[16]; + ssize_t read_response = read(m_pipefd[0], buf, 15); + + // with linux automake, warnings are treated as errors by default + if (read_response < 0) + { + // todo: handle read response + } + + { + CLock lock(&m_mutex); + // we're now waiting for events + m_waiting = true; + + // push out pending events + flush(); + } + // calling flush may have queued up a new event. + if (!CXWindowsEventQueueBuffer::isEmpty()) { + CThread::testCancel(); + return; + } + + // use poll() to wait for a message from the X server or for timeout. + // this is a good deal more efficient than polling and sleeping. +#if HAVE_POLL + struct pollfd pfds[2]; + pfds[0].fd = ConnectionNumber(m_display); + pfds[0].events = POLLIN; + pfds[1].fd = m_pipefd[0]; + pfds[1].events = POLLIN; + int timeout = (dtimeout < 0.0) ? -1 : + static_cast(1000.0 * dtimeout); + int remaining = timeout; + int retval = 0; +#else + struct timeval timeout; + struct timeval* timeoutPtr; + if (dtimeout < 0.0) { + timeoutPtr = NULL; + } + else { + timeout.tv_sec = static_cast(dtimeout); + timeout.tv_usec = static_cast(1.0e+6 * + (dtimeout - timeout.tv_sec)); + timeoutPtr = &timeout; + } + + // initialize file descriptor sets + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(ConnectionNumber(m_display), &rfds); + FD_SET(m_pipefd[0], &rfds); + int nfds; + if (ConnectionNumber(m_display) > m_pipefd[0]) { + nfds = ConnectionNumber(m_display) + 1; + } + else { + nfds = m_pipefd[0] + 1; + } +#endif + // It's possible that the X server has queued events locally + // in xlib's event buffer and not pushed on to the fd. Hence we + // can't simply monitor the fd as we may never be woken up. + // ie addEvent calls flush, XFlush may not send via the fd hence + // there is an event waiting to be sent but we must exit the poll + // before it can. + // Instead we poll for a brief period of time (so if events + // queued locally in the xlib buffer can be processed) + // and continue doing this until timeout is reached. + // The human eye can notice 60hz (ansi) which is 16ms, however + // we want to give the cpu a chance s owe up this to 25ms +#define TIMEOUT_DELAY 25 + + while( ((dtimeout < 0.0) || (remaining > 0)) && QLength(m_display)==0 && retval==0){ +#if HAVE_POLL + retval = poll(pfds, 2, TIMEOUT_DELAY); //16ms = 60hz, but we make it > to play nicely with the cpu + if (pfds[1].revents & POLLIN) { + ssize_t read_response = read(m_pipefd[0], buf, 15); + + // with linux automake, warnings are treated as errors by default + if (read_response < 0) + { + // todo: handle read response + } + + } +#else + retval = select(nfds, + SELECT_TYPE_ARG234 &rfds, + SELECT_TYPE_ARG234 NULL, + SELECT_TYPE_ARG234 NULL, + SELECT_TYPE_ARG5 TIMEOUT_DELAY); + if (FD_SET(m_pipefd[0], &rfds)) { + read(m_pipefd[0], buf, 15); + } +#endif + remaining-=TIMEOUT_DELAY; + } + + { + // we're no longer waiting for events + CLock lock(&m_mutex); + m_waiting = false; + } + + CThread::testCancel(); +} + +IEventQueueBuffer::Type +CXWindowsEventQueueBuffer::getEvent(CEvent& event, UInt32& dataID) +{ + CLock lock(&m_mutex); + + // push out pending events + flush(); + + // get next event + XNextEvent(m_display, &m_event); + + // process event + if (m_event.xany.type == ClientMessage && + m_event.xclient.message_type == m_userEvent) { + dataID = static_cast(m_event.xclient.data.l[0]); + return kUser; + } + else { + event = CEvent(CEvent::kSystem, + IEventQueue::getSystemTarget(), &m_event); + return kSystem; + } +} + +bool +CXWindowsEventQueueBuffer::addEvent(UInt32 dataID) +{ + // prepare a message + XEvent xevent; + xevent.xclient.type = ClientMessage; + xevent.xclient.window = m_window; + xevent.xclient.message_type = m_userEvent; + xevent.xclient.format = 32; + xevent.xclient.data.l[0] = static_cast(dataID); + + // save the message + CLock lock(&m_mutex); + m_postedEvents.push_back(xevent); + + // if we're currently waiting for an event then send saved events to + // the X server now. if we're not waiting then some other thread + // might be using the display connection so we can't safely use it + // too. + if (m_waiting) { + flush(); + // Send a character through the round-trip pipe to wake a thread + // that is waiting for a ConnectionNumber() socket to be readable. + // The flush call can read incoming data from the socket and put + // it in Xlib's input buffer. That sneaks it past the other thread. + ssize_t write_response = write(m_pipefd[1], "!", 1); + + // with linux automake, warnings are treated as errors by default + if (write_response < 0) + { + // todo: handle read response + } + } + + return true; +} + +bool +CXWindowsEventQueueBuffer::isEmpty() const +{ + CLock lock(&m_mutex); + return (XPending(m_display) == 0 ); +} + +CEventQueueTimer* +CXWindowsEventQueueBuffer::newTimer(double, bool) const +{ + return new CEventQueueTimer; +} + +void +CXWindowsEventQueueBuffer::deleteTimer(CEventQueueTimer* timer) const +{ + delete timer; +} + +void +CXWindowsEventQueueBuffer::flush() +{ + // note -- m_mutex must be locked on entry + + // flush the posted event list to the X server + for (size_t i = 0; i < m_postedEvents.size(); ++i) { + XSendEvent(m_display, m_window, False, 0, &m_postedEvents[i]); + } + XFlush(m_display); + m_postedEvents.clear(); +} diff --git a/src/lib/platform/CXWindowsEventQueueBuffer.h b/src/lib/platform/CXWindowsEventQueueBuffer.h new file mode 100644 index 00000000..a1d5d4be --- /dev/null +++ b/src/lib/platform/CXWindowsEventQueueBuffer.h @@ -0,0 +1,61 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSEVENTQUEUEBUFFER_H +#define CXWINDOWSEVENTQUEUEBUFFER_H + +#include "IEventQueueBuffer.h" +#include "CMutex.h" +#include "stdvector.h" +#if X_DISPLAY_MISSING +# error X11 is required to build synergy +#else +# include +#endif + +//! Event queue buffer for X11 +class CXWindowsEventQueueBuffer : public IEventQueueBuffer { +public: + CXWindowsEventQueueBuffer(Display*, Window); + virtual ~CXWindowsEventQueueBuffer(); + + // IEventQueueBuffer overrides + virtual void waitForEvent(double timeout); + virtual Type getEvent(CEvent& event, UInt32& dataID); + virtual bool addEvent(UInt32 dataID); + virtual bool isEmpty() const; + virtual CEventQueueTimer* + newTimer(double duration, bool oneShot) const; + virtual void deleteTimer(CEventQueueTimer*) const; + +private: + void flush(); + +private: + typedef std::vector CEventList; + + CMutex m_mutex; + Display* m_display; + Window m_window; + Atom m_userEvent; + XEvent m_event; + CEventList m_postedEvents; + bool m_waiting; + int m_pipefd[2]; +}; + +#endif diff --git a/src/lib/platform/CXWindowsKeyState.cpp b/src/lib/platform/CXWindowsKeyState.cpp new file mode 100644 index 00000000..2956f440 --- /dev/null +++ b/src/lib/platform/CXWindowsKeyState.cpp @@ -0,0 +1,861 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsKeyState.h" +#include "CXWindowsUtil.h" +#include "CLog.h" +#include "CStringUtil.h" +#include "stdmap.h" +#include +#include +#if X_DISPLAY_MISSING +# error X11 is required to build synergy +#else +# include +# include +# define XK_MISCELLANY +# define XK_XKB_KEYS +# include +#if HAVE_XKB_EXTENSION +# include +#endif +#endif + +static const size_t ModifiersFromXDefaultSize = 32; + +CXWindowsKeyState::CXWindowsKeyState(Display* display, bool useXKB) : + m_display(display), + m_modifierFromX(ModifiersFromXDefaultSize) +{ + init(display, useXKB); +} + +CXWindowsKeyState::CXWindowsKeyState( + Display* display, bool useXKB, + IEventQueue& eventQueue, CKeyMap& keyMap) : + CKeyState(eventQueue, keyMap), + m_display(display), + m_modifierFromX(ModifiersFromXDefaultSize) +{ + init(display, useXKB); +} + +CXWindowsKeyState::~CXWindowsKeyState() +{ +#if HAVE_XKB_EXTENSION + if (m_xkb != NULL) { + XkbFreeKeyboard(m_xkb, 0, True); + } +#endif +} + +void +CXWindowsKeyState::init(Display* display, bool useXKB) +{ + XGetKeyboardControl(m_display, &m_keyboardState); +#if HAVE_XKB_EXTENSION + if (useXKB) { + m_xkb = XkbGetMap(m_display, XkbKeyActionsMask | XkbKeyBehaviorsMask | + XkbAllClientInfoMask, XkbUseCoreKbd); + } + else { + m_xkb = NULL; + } +#endif + setActiveGroup(kGroupPollAndSet); +} + +void +CXWindowsKeyState::setActiveGroup(SInt32 group) +{ + if (group == kGroupPollAndSet) { + // we need to set the group to -1 in order for pollActiveGroup() to + // actually poll for the group + m_group = -1; + m_group = pollActiveGroup(); + } + else if (group == kGroupPoll) { + m_group = -1; + } + else { + assert(group >= 0); + m_group = group; + } +} + +void +CXWindowsKeyState::setAutoRepeat(const XKeyboardState& state) +{ + m_keyboardState = state; +} + +KeyModifierMask +CXWindowsKeyState::mapModifiersFromX(unsigned int state) const +{ + LOG((CLOG_DEBUG2 "mapping state: %i", state)); + UInt32 offset = 8 * getGroupFromState(state); + KeyModifierMask mask = 0; + for (int i = 0; i < 8; ++i) { + if ((state & (1u << i)) != 0) { + LOG((CLOG_DEBUG2 "|= modifier: %i", offset + i)); + if (offset + i >= m_modifierFromX.size()) { + LOG((CLOG_ERR "m_modifierFromX is too small (%d) for the " + "requested offset (%d)", m_modifierFromX.size(), offset+i)); + } else { + mask |= m_modifierFromX[offset + i]; + } + } + } + return mask; +} + +bool +CXWindowsKeyState::mapModifiersToX(KeyModifierMask mask, + unsigned int& modifiers) const +{ + modifiers = 0; + + for (SInt32 i = 0; i < kKeyModifierNumBits; ++i) { + KeyModifierMask bit = (1u << i); + if ((mask & bit) != 0) { + KeyModifierToXMask::const_iterator j = m_modifierToX.find(bit); + if (j == m_modifierToX.end()) { + return false; + } + else { + modifiers |= j->second; + } + } + } + + return true; +} + +void +CXWindowsKeyState::mapKeyToKeycodes(KeyID key, CKeycodeList& keycodes) const +{ + keycodes.clear(); + std::pair range = + m_keyCodeFromKey.equal_range(key); + for (KeyToKeyCodeMap::const_iterator i = range.first; + i != range.second; ++i) { + keycodes.push_back(i->second); + } +} + +bool +CXWindowsKeyState::fakeCtrlAltDel() +{ + // pass keys through unchanged + return false; +} + +KeyModifierMask +CXWindowsKeyState::pollActiveModifiers() const +{ + Window root = DefaultRootWindow(m_display), window; + int xRoot, yRoot, xWindow, yWindow; + unsigned int state = 0; + if (XQueryPointer(m_display, root, &root, &window, + &xRoot, &yRoot, &xWindow, &yWindow, &state) == False) { + state = 0; + } + return mapModifiersFromX(state); +} + +SInt32 +CXWindowsKeyState::pollActiveGroup() const +{ + // fixed condition where any group < -1 would have undetermined behaviour + if (m_group >= 0) { + return m_group; + } + +#if HAVE_XKB_EXTENSION + if (m_xkb != NULL) { + XkbStateRec state; + if (XkbGetState(m_display, XkbUseCoreKbd, &state) == Success) { + return state.group; + } + } +#endif + return 0; +} + +void +CXWindowsKeyState::pollPressedKeys(KeyButtonSet& pressedKeys) const +{ + char keys[32]; + XQueryKeymap(m_display, keys); + for (UInt32 i = 0; i < 32; ++i) { + for (UInt32 j = 0; j < 8; ++j) { + if ((keys[i] & (1u << j)) != 0) { + pressedKeys.insert(8 * i + j); + } + } + } +} + +void +CXWindowsKeyState::getKeyMap(CKeyMap& keyMap) +{ + // get autorepeat info. we must use the global_auto_repeat told to + // us because it may have modified by synergy. + int oldGlobalAutoRepeat = m_keyboardState.global_auto_repeat; + XGetKeyboardControl(m_display, &m_keyboardState); + m_keyboardState.global_auto_repeat = oldGlobalAutoRepeat; + +#if HAVE_XKB_EXTENSION + if (m_xkb != NULL) { + if (XkbGetUpdatedMap(m_display, XkbKeyActionsMask | + XkbKeyBehaviorsMask | XkbAllClientInfoMask, m_xkb) == Success) { + updateKeysymMapXKB(keyMap); + return; + } + } +#endif + updateKeysymMap(keyMap); +} + +void +CXWindowsKeyState::fakeKey(const Keystroke& keystroke) +{ + switch (keystroke.m_type) { + case Keystroke::kButton: + LOG((CLOG_DEBUG1 " %03x (%08x) %s", keystroke.m_data.m_button.m_button, keystroke.m_data.m_button.m_client, keystroke.m_data.m_button.m_press ? "down" : "up")); + if (keystroke.m_data.m_button.m_repeat) { + int c = keystroke.m_data.m_button.m_button; + int i = (c >> 3); + int b = 1 << (c & 7); + if (m_keyboardState.global_auto_repeat == AutoRepeatModeOff || + (c!=113 && c!=116 && (m_keyboardState.auto_repeats[i] & b) == 0)) { + LOG((CLOG_DEBUG1 " discard autorepeat")); + break; + } + } + XTestFakeKeyEvent(m_display, keystroke.m_data.m_button.m_button, + keystroke.m_data.m_button.m_press ? True : False, + CurrentTime); + break; + + case Keystroke::kGroup: + if (keystroke.m_data.m_group.m_absolute) { + LOG((CLOG_DEBUG1 " group %d", keystroke.m_data.m_group.m_group)); +#if HAVE_XKB_EXTENSION + if (m_xkb != NULL) { + if (XkbLockGroup(m_display, XkbUseCoreKbd, + keystroke.m_data.m_group.m_group) == False) { + LOG((CLOG_DEBUG1 "XkbLockGroup request not sent")); + } + } + else +#endif + { + LOG((CLOG_DEBUG1 " ignored")); + } + } + else { + LOG((CLOG_DEBUG1 " group %+d", keystroke.m_data.m_group.m_group)); +#if HAVE_XKB_EXTENSION + if (m_xkb != NULL) { + if (XkbLockGroup(m_display, XkbUseCoreKbd, + getEffectiveGroup(pollActiveGroup(), + keystroke.m_data.m_group.m_group)) == False) { + LOG((CLOG_DEBUG1 "XkbLockGroup request not sent")); + } + } + else +#endif + { + LOG((CLOG_DEBUG1 " ignored")); + } + } + break; + } + XFlush(m_display); +} + +void +CXWindowsKeyState::updateKeysymMap(CKeyMap& keyMap) +{ + // there are up to 4 keysyms per keycode + static const int maxKeysyms = 4; + + LOG((CLOG_DEBUG1 "non-XKB mapping")); + + // prepare map from X modifier to KeyModifierMask. certain bits + // are predefined. + std::fill(m_modifierFromX.begin(), m_modifierFromX.end(), 0); + m_modifierFromX[ShiftMapIndex] = KeyModifierShift; + m_modifierFromX[LockMapIndex] = KeyModifierCapsLock; + m_modifierFromX[ControlMapIndex] = KeyModifierControl; + m_modifierToX.clear(); + m_modifierToX[KeyModifierShift] = ShiftMask; + m_modifierToX[KeyModifierCapsLock] = LockMask; + m_modifierToX[KeyModifierControl] = ControlMask; + + // prepare map from KeyID to KeyCode + m_keyCodeFromKey.clear(); + + // get the number of keycodes + int minKeycode, maxKeycode; + XDisplayKeycodes(m_display, &minKeycode, &maxKeycode); + int numKeycodes = maxKeycode - minKeycode + 1; + + // get the keyboard mapping for all keys + int keysymsPerKeycode; + KeySym* allKeysyms = XGetKeyboardMapping(m_display, + minKeycode, numKeycodes, + &keysymsPerKeycode); + + // it's more convenient to always have maxKeysyms KeySyms per key + { + KeySym* tmpKeysyms = new KeySym[maxKeysyms * numKeycodes]; + for (int i = 0; i < numKeycodes; ++i) { + for (int j = 0; j < maxKeysyms; ++j) { + if (j < keysymsPerKeycode) { + tmpKeysyms[maxKeysyms * i + j] = + allKeysyms[keysymsPerKeycode * i + j]; + } + else { + tmpKeysyms[maxKeysyms * i + j] = NoSymbol; + } + } + } + XFree(allKeysyms); + allKeysyms = tmpKeysyms; + } + + // get the buttons assigned to modifiers. X11 does not predefine + // the meaning of any modifiers except shift, caps lock, and the + // control key. the meaning of a modifier bit (other than those) + // depends entirely on the KeySyms mapped to that bit. unfortunately + // you cannot map a bit back to the KeySym used to produce it. + // for example, let's say button 1 maps to Alt_L without shift and + // Meta_L with shift. now if mod1 is mapped to button 1 that could + // mean the user used Alt or Meta to turn on that modifier and there's + // no way to know which. it's also possible for one button to be + // mapped to multiple bits so both mod1 and mod2 could be generated + // by button 1. + // + // we're going to ignore any modifier for a button except the first. + // with the above example, that means we'll ignore the mod2 modifier + // bit unless it's also mapped to some other button. we're also + // going to ignore all KeySyms except the first modifier KeySym, + // which means button 1 above won't map to Meta, just Alt. + std::map modifierButtons; + XModifierKeymap* modifiers = XGetModifierMapping(m_display); + for (unsigned int i = 0; i < 8; ++i) { + const KeyCode* buttons = + modifiers->modifiermap + i * modifiers->max_keypermod; + for (int j = 0; j < modifiers->max_keypermod; ++j) { + modifierButtons.insert(std::make_pair(buttons[j], i)); + } + } + XFreeModifiermap(modifiers); + modifierButtons.erase(0); + + // Hack to deal with VMware. When a VMware client grabs input the + // player clears out the X modifier map for whatever reason. We're + // notified of the change and arrive here to discover that there + // are no modifiers at all. Since this prevents the modifiers from + // working in the VMware client we'll use the last known good set + // of modifiers when there are no modifiers. If there are modifiers + // we update the last known good set. + if (!modifierButtons.empty()) { + m_lastGoodNonXKBModifiers = modifierButtons; + } + else { + modifierButtons = m_lastGoodNonXKBModifiers; + } + + // add entries for each keycode + CKeyMap::KeyItem item; + for (int i = 0; i < numKeycodes; ++i) { + KeySym* keysyms = allKeysyms + maxKeysyms * i; + KeyCode keycode = static_cast(i + minKeycode); + item.m_button = static_cast(keycode); + item.m_client = 0; + + // determine modifier sensitivity + item.m_sensitive = 0; + + // if the keysyms in levels 2 or 3 exist and differ from levels + // 0 and 1 then the key is sensitive AltGr (Mode_switch) + if ((keysyms[2] != NoSymbol && keysyms[2] != keysyms[0]) || + (keysyms[3] != NoSymbol && keysyms[2] != keysyms[1])) { + item.m_sensitive |= KeyModifierAltGr; + } + + // check if the key is caps-lock sensitive. some systems only + // provide one keysym for keys sensitive to caps-lock. if we + // find that then fill in the missing keysym. + if (keysyms[0] != NoSymbol && keysyms[1] == NoSymbol && + keysyms[2] == NoSymbol && keysyms[3] == NoSymbol) { + KeySym lKeysym, uKeysym; + XConvertCase(keysyms[0], &lKeysym, &uKeysym); + if (lKeysym != uKeysym) { + keysyms[0] = lKeysym; + keysyms[1] = uKeysym; + item.m_sensitive |= KeyModifierCapsLock; + } + } + else if (keysyms[0] != NoSymbol && keysyms[1] != NoSymbol) { + KeySym lKeysym, uKeysym; + XConvertCase(keysyms[0], &lKeysym, &uKeysym); + if (lKeysym != uKeysym && + lKeysym == keysyms[0] && + uKeysym == keysyms[1]) { + item.m_sensitive |= KeyModifierCapsLock; + } + else if (keysyms[2] != NoSymbol && keysyms[3] != NoSymbol) { + XConvertCase(keysyms[2], &lKeysym, &uKeysym); + if (lKeysym != uKeysym && + lKeysym == keysyms[2] && + uKeysym == keysyms[3]) { + item.m_sensitive |= KeyModifierCapsLock; + } + } + } + + // key is sensitive to shift if keysyms in levels 0 and 1 or + // levels 2 and 3 don't match. it's also sensitive to shift + // if it's sensitive to caps-lock. + if ((item.m_sensitive & KeyModifierCapsLock) != 0) { + item.m_sensitive |= KeyModifierShift; + } + else if ((keysyms[0] != NoSymbol && keysyms[1] != NoSymbol && + keysyms[0] != keysyms[1]) || + (keysyms[2] != NoSymbol && keysyms[3] != NoSymbol && + keysyms[2] != keysyms[3])) { + item.m_sensitive |= KeyModifierShift; + } + + // key is sensitive to numlock if any keysym on it is + if (IsKeypadKey(keysyms[0]) || IsPrivateKeypadKey(keysyms[0]) || + IsKeypadKey(keysyms[1]) || IsPrivateKeypadKey(keysyms[1]) || + IsKeypadKey(keysyms[2]) || IsPrivateKeypadKey(keysyms[2]) || + IsKeypadKey(keysyms[3]) || IsPrivateKeypadKey(keysyms[3])) { + item.m_sensitive |= KeyModifierNumLock; + } + + // do each keysym (shift level) + for (int j = 0; j < maxKeysyms; ++j) { + item.m_id = CXWindowsUtil::mapKeySymToKeyID(keysyms[j]); + if (item.m_id == kKeyNone) { + if (j != 0 && modifierButtons.count(keycode) > 0) { + // pretend the modifier works in other shift levels + // because it probably does. + if (keysyms[1] == NoSymbol || j != 3) { + item.m_id = CXWindowsUtil::mapKeySymToKeyID(keysyms[0]); + } + else { + item.m_id = CXWindowsUtil::mapKeySymToKeyID(keysyms[1]); + } + } + if (item.m_id == kKeyNone) { + continue; + } + } + + // group is 0 for levels 0 and 1 and 1 for levels 2 and 3 + item.m_group = (j >= 2) ? 1 : 0; + + // compute required modifiers + item.m_required = 0; + if ((j & 1) != 0) { + item.m_required |= KeyModifierShift; + } + if ((j & 2) != 0) { + item.m_required |= KeyModifierAltGr; + } + + item.m_generates = 0; + item.m_lock = false; + if (modifierButtons.count(keycode) > 0) { + // get flags for modifier keys + CKeyMap::initModifierKey(item); + + // add mapping from X (unless we already have) + if (item.m_generates != 0) { + unsigned int bit = modifierButtons[keycode]; + if (m_modifierFromX[bit] == 0) { + m_modifierFromX[bit] = item.m_generates; + m_modifierToX[item.m_generates] = (1u << bit); + } + } + } + + // add key + keyMap.addKeyEntry(item); + m_keyCodeFromKey.insert(std::make_pair(item.m_id, keycode)); + + // add other ways to synthesize the key + if ((j & 1) != 0) { + // add capslock version of key is sensitive to capslock + KeySym lKeysym, uKeysym; + XConvertCase(keysyms[j], &lKeysym, &uKeysym); + if (lKeysym != uKeysym && + lKeysym == keysyms[j - 1] && + uKeysym == keysyms[j]) { + item.m_required &= ~KeyModifierShift; + item.m_required |= KeyModifierCapsLock; + keyMap.addKeyEntry(item); + item.m_required |= KeyModifierShift; + item.m_required &= ~KeyModifierCapsLock; + } + + // add numlock version of key if sensitive to numlock + if (IsKeypadKey(keysyms[j]) || IsPrivateKeypadKey(keysyms[j])) { + item.m_required &= ~KeyModifierShift; + item.m_required |= KeyModifierNumLock; + keyMap.addKeyEntry(item); + item.m_required |= KeyModifierShift; + item.m_required &= ~KeyModifierNumLock; + } + } + } + } + + delete[] allKeysyms; +} + +#if HAVE_XKB_EXTENSION +void +CXWindowsKeyState::updateKeysymMapXKB(CKeyMap& keyMap) +{ + static const XkbKTMapEntryRec defMapEntry = { + True, // active + 0, // level + { + 0, // mods.mask + 0, // mods.real_mods + 0 // mods.vmods + } + }; + + LOG((CLOG_DEBUG1 "XKB mapping")); + + // find the number of groups + int maxNumGroups = 0; + for (int i = m_xkb->min_key_code; i <= m_xkb->max_key_code; ++i) { + int numGroups = XkbKeyNumGroups(m_xkb, static_cast(i)); + if (numGroups > maxNumGroups) { + maxNumGroups = numGroups; + } + } + + // prepare map from X modifier to KeyModifierMask + std::vector modifierLevel(maxNumGroups * 8, 4); + m_modifierFromX.clear(); + m_modifierFromX.resize(maxNumGroups * 8); + m_modifierToX.clear(); + + // prepare map from KeyID to KeyCode + m_keyCodeFromKey.clear(); + + // Hack to deal with VMware. When a VMware client grabs input the + // player clears out the X modifier map for whatever reason. We're + // notified of the change and arrive here to discover that there + // are no modifiers at all. Since this prevents the modifiers from + // working in the VMware client we'll use the last known good set + // of modifiers when there are no modifiers. If there are modifiers + // we update the last known good set. + bool useLastGoodModifiers = !hasModifiersXKB(); + if (!useLastGoodModifiers) { + m_lastGoodXKBModifiers.clear(); + } + + // check every button. on this pass we save all modifiers as native + // X modifier masks. + CKeyMap::KeyItem item; + for (int i = m_xkb->min_key_code; i <= m_xkb->max_key_code; ++i) { + KeyCode keycode = static_cast(i); + item.m_button = static_cast(keycode); + item.m_client = 0; + + // skip keys with no groups (they generate no symbols) + if (XkbKeyNumGroups(m_xkb, keycode) == 0) { + continue; + } + + // note half-duplex keys + const XkbBehavior& b = m_xkb->server->behaviors[keycode]; + if ((b.type & XkbKB_OpMask) == XkbKB_Lock) { + keyMap.addHalfDuplexButton(item.m_button); + } + + // iterate over all groups + for (int group = 0; group < maxNumGroups; ++group) { + item.m_group = group; + int eGroup = getEffectiveGroup(keycode, group); + + // get key info + XkbKeyTypePtr type = XkbKeyKeyType(m_xkb, keycode, eGroup); + + // set modifiers the item is sensitive to + item.m_sensitive = type->mods.mask; + + // iterate over all shift levels for the button (including none) + for (int j = -1; j < type->map_count; ++j) { + const XkbKTMapEntryRec* mapEntry = + ((j == -1) ? &defMapEntry : type->map + j); + if (!mapEntry->active) { + continue; + } + int level = mapEntry->level; + + // set required modifiers for this item + item.m_required = mapEntry->mods.mask; + if ((item.m_required & LockMask) != 0 && + j != -1 && type->preserve != NULL && + (type->preserve[j].mask & LockMask) != 0) { + // sensitive caps lock and we preserve caps-lock. + // preserving caps-lock means we Xlib functions would + // yield the capitialized KeySym so we'll adjust the + // level accordingly. + if ((level ^ 1) < type->num_levels) { + level ^= 1; + } + } + + // get the keysym for this item + KeySym keysym = XkbKeySymEntry(m_xkb, keycode, level, eGroup); + + // check for group change actions, locking modifiers, and + // modifier masks. + item.m_lock = false; + bool isModifier = false; + UInt32 modifierMask = m_xkb->map->modmap[keycode]; + if (XkbKeyHasActions(m_xkb, keycode) == True) { + XkbAction* action = + XkbKeyActionEntry(m_xkb, keycode, level, eGroup); + if (action->type == XkbSA_SetMods || + action->type == XkbSA_LockMods) { + isModifier = true; + + // note toggles + item.m_lock = (action->type == XkbSA_LockMods); + + // maybe use action's mask + if ((action->mods.flags & XkbSA_UseModMapMods) == 0) { + modifierMask = action->mods.mask; + } + } + else if (action->type == XkbSA_SetGroup || + action->type == XkbSA_LatchGroup || + action->type == XkbSA_LockGroup) { + // ignore group change key + continue; + } + } + level = mapEntry->level; + + // VMware modifier hack + if (useLastGoodModifiers) { + XKBModifierMap::const_iterator k = + m_lastGoodXKBModifiers.find(eGroup * 256 + keycode); + if (k != m_lastGoodXKBModifiers.end()) { + // Use last known good modifier + isModifier = true; + level = k->second.m_level; + modifierMask = k->second.m_mask; + item.m_lock = k->second.m_lock; + } + } + else if (isModifier) { + // Save known good modifier + XKBModifierInfo& info = + m_lastGoodXKBModifiers[eGroup * 256 + keycode]; + info.m_level = level; + info.m_mask = modifierMask; + info.m_lock = item.m_lock; + } + + // record the modifier mask for this key. don't bother + // for keys that change the group. + item.m_generates = 0; + UInt32 modifierBit = + CXWindowsUtil::getModifierBitForKeySym(keysym); + if (isModifier && modifierBit != kKeyModifierBitNone) { + item.m_generates = (1u << modifierBit); + for (SInt32 j = 0; j < 8; ++j) { + // skip modifiers this key doesn't generate + if ((modifierMask & (1u << j)) == 0) { + continue; + } + + // skip keys that map to a modifier that we've + // already seen using fewer modifiers. that is + // if this key must combine with other modifiers + // and we know of a key that combines with fewer + // modifiers (or no modifiers) then prefer the + // other key. + if (level >= modifierLevel[8 * group + j]) { + continue; + } + modifierLevel[8 * group + j] = level; + + // save modifier + m_modifierFromX[8 * group + j] |= (1u << modifierBit); + m_modifierToX.insert(std::make_pair( + 1u << modifierBit, 1u << j)); + } + } + + // handle special cases of just one keysym for the keycode + if (type->num_levels == 1) { + // if there are upper- and lowercase versions of the + // keysym then add both. + KeySym lKeysym, uKeysym; + XConvertCase(keysym, &lKeysym, &uKeysym); + if (lKeysym != uKeysym) { + if (j != -1) { + continue; + } + + item.m_sensitive |= ShiftMask | LockMask; + + KeyID lKeyID = CXWindowsUtil::mapKeySymToKeyID(lKeysym); + KeyID uKeyID = CXWindowsUtil::mapKeySymToKeyID(uKeysym); + if (lKeyID == kKeyNone || uKeyID == kKeyNone) { + continue; + } + + item.m_id = lKeyID; + item.m_required = 0; + keyMap.addKeyEntry(item); + + item.m_id = uKeyID; + item.m_required = ShiftMask; + keyMap.addKeyEntry(item); + item.m_required = LockMask; + keyMap.addKeyEntry(item); + + if (group == 0) { + m_keyCodeFromKey.insert( + std::make_pair(lKeyID, keycode)); + m_keyCodeFromKey.insert( + std::make_pair(uKeyID, keycode)); + } + continue; + } + } + + // add entry + item.m_id = CXWindowsUtil::mapKeySymToKeyID(keysym); + keyMap.addKeyEntry(item); + if (group == 0) { + m_keyCodeFromKey.insert(std::make_pair(item.m_id, keycode)); + } + } + } + } + + // change all modifier masks to synergy masks from X masks + keyMap.foreachKey(&CXWindowsKeyState::remapKeyModifiers, this); + + // allow composition across groups + keyMap.allowGroupSwitchDuringCompose(); +} +#endif + +void +CXWindowsKeyState::remapKeyModifiers(KeyID id, SInt32 group, + CKeyMap::KeyItem& item, void* vself) +{ + CXWindowsKeyState* self = reinterpret_cast(vself); + item.m_required = + self->mapModifiersFromX(XkbBuildCoreState(item.m_required, group)); + item.m_sensitive = + self->mapModifiersFromX(XkbBuildCoreState(item.m_sensitive, group)); +} + +bool +CXWindowsKeyState::hasModifiersXKB() const +{ +#if HAVE_XKB_EXTENSION + // iterate over all keycodes + for (int i = m_xkb->min_key_code; i <= m_xkb->max_key_code; ++i) { + KeyCode keycode = static_cast(i); + if (XkbKeyHasActions(m_xkb, keycode) == True) { + // iterate over all groups + int numGroups = XkbKeyNumGroups(m_xkb, keycode); + for (int group = 0; group < numGroups; ++group) { + // iterate over all shift levels for the button (including none) + XkbKeyTypePtr type = XkbKeyKeyType(m_xkb, keycode, group); + for (int j = -1; j < type->map_count; ++j) { + if (j != -1 && !type->map[j].active) { + continue; + } + int level = ((j == -1) ? 0 : type->map[j].level); + XkbAction* action = + XkbKeyActionEntry(m_xkb, keycode, level, group); + if (action->type == XkbSA_SetMods || + action->type == XkbSA_LockMods) { + return true; + } + } + } + } + } +#endif + return false; +} + +int +CXWindowsKeyState::getEffectiveGroup(KeyCode keycode, int group) const +{ + (void)keycode; +#if HAVE_XKB_EXTENSION + // get effective group for key + int numGroups = XkbKeyNumGroups(m_xkb, keycode); + if (group >= numGroups) { + unsigned char groupInfo = XkbKeyGroupInfo(m_xkb, keycode); + switch (XkbOutOfRangeGroupAction(groupInfo)) { + case XkbClampIntoRange: + group = numGroups - 1; + break; + + case XkbRedirectIntoRange: + group = XkbOutOfRangeGroupNumber(groupInfo); + if (group >= numGroups) { + group = 0; + } + break; + + default: + // wrap + group %= numGroups; + break; + } + } +#endif + return group; +} + +UInt32 +CXWindowsKeyState::getGroupFromState(unsigned int state) const +{ +#if HAVE_XKB_EXTENSION + if (m_xkb != NULL) { + return XkbGroupForCoreState(state); + } +#endif + return 0; +} diff --git a/src/lib/platform/CXWindowsKeyState.h b/src/lib/platform/CXWindowsKeyState.h new file mode 100644 index 00000000..37a5d082 --- /dev/null +++ b/src/lib/platform/CXWindowsKeyState.h @@ -0,0 +1,161 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSKEYSTATE_H +#define CXWINDOWSKEYSTATE_H + +#include "CKeyState.h" +#include "stdmap.h" +#include "stdvector.h" +#if X_DISPLAY_MISSING +# error X11 is required to build synergy +#else +# include +# if HAVE_X11_EXTENSIONS_XTEST_H +# include +# else +# error The XTest extension is required to build synergy +# endif +# if HAVE_XKB_EXTENSION +# include +# endif +#endif + +//! X Windows key state +/*! +A key state for X Windows. +*/ +class CXWindowsKeyState : public CKeyState { +public: + typedef std::vector CKeycodeList; + enum { + kGroupPoll = -1, + kGroupPollAndSet = -2 + }; + + CXWindowsKeyState(Display*, bool useXKB); + CXWindowsKeyState(Display*, bool useXKB, + IEventQueue& eventQueue, CKeyMap& keyMap); + ~CXWindowsKeyState(); + + //! @name modifiers + //@{ + + //! Set active group + /*! + Sets the active group to \p group. This is the group returned by + \c pollActiveGroup(). If \p group is \c kGroupPoll then + \c pollActiveGroup() will really poll, but that's a slow operation + on X11. If \p group is \c kGroupPollAndSet then this will poll the + active group now and use it for future calls to \c pollActiveGroup(). + */ + void setActiveGroup(SInt32 group); + + //! Set the auto-repeat state + /*! + Sets the auto-repeat state. + */ + void setAutoRepeat(const XKeyboardState&); + + //@} + //! @name accessors + //@{ + + //! Convert X modifier mask to synergy mask + /*! + Returns the synergy modifier mask corresponding to the X modifier + mask in \p state. + */ + KeyModifierMask mapModifiersFromX(unsigned int state) const; + + //! Convert synergy modifier mask to X mask + /*! + Converts the synergy modifier mask to the corresponding X modifier + mask. Returns \c true if successful and \c false if any modifier + could not be converted. + */ + bool mapModifiersToX(KeyModifierMask, unsigned int&) const; + + //! Convert synergy key to all corresponding X keycodes + /*! + Converts the synergy key \p key to all of the keycodes that map to + that key. + */ + void mapKeyToKeycodes(KeyID key, + CKeycodeList& keycodes) const; + + //@} + + // IKeyState overrides + virtual bool fakeCtrlAltDel(); + virtual KeyModifierMask + pollActiveModifiers() const; + virtual SInt32 pollActiveGroup() const; + virtual void pollPressedKeys(KeyButtonSet& pressedKeys) const; + +protected: + // CKeyState overrides + virtual void getKeyMap(CKeyMap& keyMap); + virtual void fakeKey(const Keystroke& keystroke); + +private: + void init(Display* display, bool useXKB); + void updateKeysymMap(CKeyMap&); + void updateKeysymMapXKB(CKeyMap&); + bool hasModifiersXKB() const; + int getEffectiveGroup(KeyCode, int group) const; + UInt32 getGroupFromState(unsigned int state) const; + + static void remapKeyModifiers(KeyID, SInt32, + CKeyMap::KeyItem&, void*); + +private: + struct XKBModifierInfo { + public: + unsigned char m_level; + UInt32 m_mask; + bool m_lock; + }; + + typedef std::vector KeyModifierMaskList; + typedef std::map KeyModifierToXMask; + typedef std::multimap KeyToKeyCodeMap; + typedef std::map NonXKBModifierMap; + typedef std::map XKBModifierMap; + + Display* m_display; +#if HAVE_XKB_EXTENSION + XkbDescPtr m_xkb; +#endif + SInt32 m_group; + XKBModifierMap m_lastGoodXKBModifiers; + NonXKBModifierMap m_lastGoodNonXKBModifiers; + + // X modifier (bit number) to synergy modifier (mask) mapping + KeyModifierMaskList m_modifierFromX; + + // synergy modifier (mask) to X modifier (mask) + KeyModifierToXMask m_modifierToX; + + // map KeyID to all keycodes that can synthesize that KeyID + KeyToKeyCodeMap m_keyCodeFromKey; + + // autorepeat state + XKeyboardState m_keyboardState; +}; + +#endif diff --git a/src/lib/platform/CXWindowsScreen.cpp b/src/lib/platform/CXWindowsScreen.cpp new file mode 100644 index 00000000..091ca110 --- /dev/null +++ b/src/lib/platform/CXWindowsScreen.cpp @@ -0,0 +1,2085 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsScreen.h" +#include "CXWindowsClipboard.h" +#include "CXWindowsEventQueueBuffer.h" +#include "CXWindowsKeyState.h" +#include "CXWindowsScreenSaver.h" +#include "CXWindowsUtil.h" +#include "CClipboard.h" +#include "CKeyMap.h" +#include "XScreen.h" +#include "XArch.h" +#include "CLog.h" +#include "CStopwatch.h" +#include "CStringUtil.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" +#include +#include +#if X_DISPLAY_MISSING +# error X11 is required to build synergy +#else +# include +# include +# define XK_MISCELLANY +# define XK_XKB_KEYS +# include +# if HAVE_X11_EXTENSIONS_DPMS_H + extern "C" { +# include + } +# endif +# if HAVE_X11_EXTENSIONS_XTEST_H +# include +# else +# error The XTest extension is required to build synergy +# endif +# if HAVE_X11_EXTENSIONS_XINERAMA_H + // Xinerama.h may lack extern "C" for inclusion by C++ + extern "C" { +# include + } +# endif +# if HAVE_X11_EXTENSIONS_XRANDR_H +# include +# endif +# if HAVE_XKB_EXTENSION +# include +# endif +# ifdef HAVE_XI2 +# include +# endif +#endif +#include "CArch.h" + +static int xi_opcode; + +// +// CXWindowsScreen +// + +// NOTE -- the X display is shared among several objects but is owned +// by the CXWindowsScreen. Xlib is not reentrant so we must ensure +// that no two objects can simultaneously call Xlib with the display. +// this is easy since we only make X11 calls from the main thread. +// we must also ensure that these objects do not use the display in +// their destructors or, if they do, we can tell them not to. This +// is to handle unexpected disconnection of the X display, when any +// call on the display is invalid. In that situation we discard the +// display and the X11 event queue buffer, ignore any calls that try +// to use the display, and wait to be destroyed. + +CXWindowsScreen* CXWindowsScreen::s_screen = NULL; + +CXWindowsScreen::CXWindowsScreen(const char* displayName, bool isPrimary, bool disableXInitThreads, int mouseScrollDelta, IEventQueue& eventQueue) : + m_isPrimary(isPrimary), + m_mouseScrollDelta(mouseScrollDelta), + m_display(NULL), + m_root(None), + m_window(None), + m_isOnScreen(m_isPrimary), + m_x(0), m_y(0), + m_w(0), m_h(0), + m_xCenter(0), m_yCenter(0), + m_xCursor(0), m_yCursor(0), + m_keyState(NULL), + m_lastFocus(None), + m_lastFocusRevert(RevertToNone), + m_im(NULL), + m_ic(NULL), + m_lastKeycode(0), + m_sequenceNumber(0), + m_screensaver(NULL), + m_screensaverNotify(false), + m_xtestIsXineramaUnaware(true), + m_preserveFocus(false), + m_xkb(false), + m_xi2detected(false), + m_xrandr(false), + m_eventQueue(eventQueue), + CPlatformScreen(eventQueue) +{ + assert(s_screen == NULL); + + if (mouseScrollDelta==0) m_mouseScrollDelta=120; + s_screen = this; + + if (!disableXInitThreads) { + // initializes Xlib support for concurrent threads. + if (XInitThreads() == 0) + throw XArch("XInitThreads() returned zero"); + } else { + LOG((CLOG_DEBUG "skipping XInitThreads()")); + } + + // set the X I/O error handler so we catch the display disconnecting + XSetIOErrorHandler(&CXWindowsScreen::ioErrorHandler); + + try { + m_display = openDisplay(displayName); + m_root = DefaultRootWindow(m_display); + saveShape(); + m_window = openWindow(); + m_screensaver = new CXWindowsScreenSaver(m_display, + m_window, getEventTarget(), eventQueue); + m_keyState = new CXWindowsKeyState(m_display, m_xkb, eventQueue, m_keyMap); + LOG((CLOG_DEBUG "screen shape: %d,%d %dx%d %s", m_x, m_y, m_w, m_h, m_xinerama ? "(xinerama)" : "")); + LOG((CLOG_DEBUG "window is 0x%08x", m_window)); + } + catch (...) { + if (m_display != NULL) { + XCloseDisplay(m_display); + } + throw; + } + + // primary/secondary screen only initialization + if (m_isPrimary) { + // start watching for events on other windows + selectEvents(m_root); + m_xi2detected = detectXI2(); + + if (m_xi2detected) { + selectXIRawMotion(); + } else + { + // start watching for events on other windows + selectEvents(m_root); + } + + // prepare to use input methods + openIM(); + } + else { + // become impervious to server grabs + XTestGrabControl(m_display, True); + } + + // initialize the clipboards + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + m_clipboard[id] = new CXWindowsClipboard(m_display, m_window, id); + } + + // install event handlers + m_eventQueue.adoptHandler(CEvent::kSystem, IEventQueue::getSystemTarget(), + new TMethodEventJob(this, + &CXWindowsScreen::handleSystemEvent)); + + // install the platform event queue + m_eventQueue.adoptBuffer(new CXWindowsEventQueueBuffer(m_display, m_window)); +} + +CXWindowsScreen::~CXWindowsScreen() +{ + assert(s_screen != NULL); + assert(m_display != NULL); + + m_eventQueue.adoptBuffer(NULL); + m_eventQueue.removeHandler(CEvent::kSystem, IEventQueue::getSystemTarget()); + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + delete m_clipboard[id]; + } + delete m_keyState; + delete m_screensaver; + m_keyState = NULL; + m_screensaver = NULL; + if (m_display != NULL) { + // FIXME -- is it safe to clean up the IC and IM without a display? + if (m_ic != NULL) { + XDestroyIC(m_ic); + } + if (m_im != NULL) { + XCloseIM(m_im); + } + XDestroyWindow(m_display, m_window); + XCloseDisplay(m_display); + } + XSetIOErrorHandler(NULL); + + s_screen = NULL; +} + +void +CXWindowsScreen::enable() +{ + if (!m_isPrimary) { + // get the keyboard control state + XKeyboardState keyControl; + XGetKeyboardControl(m_display, &keyControl); + m_autoRepeat = (keyControl.global_auto_repeat == AutoRepeatModeOn); + m_keyState->setAutoRepeat(keyControl); + + // move hider window under the cursor center + XMoveWindow(m_display, m_window, m_xCenter, m_yCenter); + + // raise and show the window + // FIXME -- take focus? + XMapRaised(m_display, m_window); + + // warp the mouse to the cursor center + fakeMouseMove(m_xCenter, m_yCenter); + } +} + +void +CXWindowsScreen::disable() +{ + // release input context focus + if (m_ic != NULL) { + XUnsetICFocus(m_ic); + } + + // unmap the hider/grab window. this also ungrabs the mouse and + // keyboard if they're grabbed. + XUnmapWindow(m_display, m_window); + + // restore auto-repeat state + if (!m_isPrimary && m_autoRepeat) { + //XAutoRepeatOn(m_display); + } +} + +void +CXWindowsScreen::enter() +{ + screensaver(false); + + // release input context focus + if (m_ic != NULL) { + XUnsetICFocus(m_ic); + } + + // set the input focus to what it had been when we took it + if (m_lastFocus != None) { + // the window may not exist anymore so ignore errors + CXWindowsUtil::CErrorLock lock(m_display); + XSetInputFocus(m_display, m_lastFocus, m_lastFocusRevert, CurrentTime); + } + + #if HAVE_X11_EXTENSIONS_DPMS_H + // Force the DPMS to turn screen back on since we don't + // actually cause physical hardware input to trigger it + int dummy; + CARD16 powerlevel; + BOOL enabled; + if (DPMSQueryExtension(m_display, &dummy, &dummy) && + DPMSCapable(m_display) && + DPMSInfo(m_display, &powerlevel, &enabled)) + { + if (enabled && powerlevel != DPMSModeOn) + DPMSForceLevel(m_display, DPMSModeOn); + } + #endif + + // unmap the hider/grab window. this also ungrabs the mouse and + // keyboard if they're grabbed. + XUnmapWindow(m_display, m_window); + +/* maybe call this if entering for the screensaver + // set keyboard focus to root window. the screensaver should then + // pick up key events for when the user enters a password to unlock. + XSetInputFocus(m_display, PointerRoot, PointerRoot, CurrentTime); +*/ + + if (!m_isPrimary) { + // get the keyboard control state + XKeyboardState keyControl; + XGetKeyboardControl(m_display, &keyControl); + m_autoRepeat = (keyControl.global_auto_repeat == AutoRepeatModeOn); + m_keyState->setAutoRepeat(keyControl); + + // turn off auto-repeat. we do this so fake key press events don't + // cause the local server to generate their own auto-repeats of + // those keys. + //XAutoRepeatOff(m_display); + } + + // now on screen + m_isOnScreen = true; +} + +bool +CXWindowsScreen::leave() +{ + if (!m_isPrimary) { + // restore the previous keyboard auto-repeat state. if the user + // changed the auto-repeat configuration while on the client then + // that state is lost. that's because we can't get notified by + // the X server when the auto-repeat configuration is changed so + // we can't track the desired configuration. + if (m_autoRepeat) { + //XAutoRepeatOn(m_display); + } + + // move hider window under the cursor center + XMoveWindow(m_display, m_window, m_xCenter, m_yCenter); + } + + // raise and show the window + XMapRaised(m_display, m_window); + + // grab the mouse and keyboard, if primary and possible + if (m_isPrimary && !grabMouseAndKeyboard()) { + XUnmapWindow(m_display, m_window); + return false; + } + + // save current focus + XGetInputFocus(m_display, &m_lastFocus, &m_lastFocusRevert); + + // take focus + if (m_isPrimary || !m_preserveFocus) { + XSetInputFocus(m_display, m_window, RevertToPointerRoot, CurrentTime); + } + + // now warp the mouse. we warp after showing the window so we're + // guaranteed to get the mouse leave event and to prevent the + // keyboard focus from changing under point-to-focus policies. + if (m_isPrimary) { + warpCursor(m_xCenter, m_yCenter); + } + else { + fakeMouseMove(m_xCenter, m_yCenter); + } + + // set input context focus to our window + if (m_ic != NULL) { + XmbResetIC(m_ic); + XSetICFocus(m_ic); + m_filtered.clear(); + } + + // now off screen + m_isOnScreen = false; + + return true; +} + +bool +CXWindowsScreen::setClipboard(ClipboardID id, const IClipboard* clipboard) +{ + // fail if we don't have the requested clipboard + if (m_clipboard[id] == NULL) { + return false; + } + + // get the actual time. ICCCM does not allow CurrentTime. + Time timestamp = CXWindowsUtil::getCurrentTime( + m_display, m_clipboard[id]->getWindow()); + + if (clipboard != NULL) { + // save clipboard data + return CClipboard::copy(m_clipboard[id], clipboard, timestamp); + } + else { + // assert clipboard ownership + if (!m_clipboard[id]->open(timestamp)) { + return false; + } + m_clipboard[id]->empty(); + m_clipboard[id]->close(); + return true; + } +} + +void +CXWindowsScreen::checkClipboards() +{ + // do nothing, we're always up to date +} + +void +CXWindowsScreen::openScreensaver(bool notify) +{ + m_screensaverNotify = notify; + if (!m_screensaverNotify) { + m_screensaver->disable(); + } +} + +void +CXWindowsScreen::closeScreensaver() +{ + if (!m_screensaverNotify) { + m_screensaver->enable(); + } +} + +void +CXWindowsScreen::screensaver(bool activate) +{ + if (activate) { + m_screensaver->activate(); + } + else { + m_screensaver->deactivate(); + } +} + +void +CXWindowsScreen::resetOptions() +{ + m_xtestIsXineramaUnaware = true; + m_preserveFocus = false; +} + +void +CXWindowsScreen::setOptions(const COptionsList& options) +{ + for (UInt32 i = 0, n = options.size(); i < n; i += 2) { + if (options[i] == kOptionXTestXineramaUnaware) { + m_xtestIsXineramaUnaware = (options[i + 1] != 0); + LOG((CLOG_DEBUG1 "XTest is Xinerama unaware %s", m_xtestIsXineramaUnaware ? "true" : "false")); + } + else if (options[i] == kOptionScreenPreserveFocus) { + m_preserveFocus = (options[i + 1] != 0); + LOG((CLOG_DEBUG1 "Preserve Focus = %s", m_preserveFocus ? "true" : "false")); + } + } +} + +void +CXWindowsScreen::setSequenceNumber(UInt32 seqNum) +{ + m_sequenceNumber = seqNum; +} + +bool +CXWindowsScreen::isPrimary() const +{ + return m_isPrimary; +} + +void* +CXWindowsScreen::getEventTarget() const +{ + return const_cast(this); +} + +bool +CXWindowsScreen::getClipboard(ClipboardID id, IClipboard* clipboard) const +{ + assert(clipboard != NULL); + + // fail if we don't have the requested clipboard + if (m_clipboard[id] == NULL) { + return false; + } + + // get the actual time. ICCCM does not allow CurrentTime. + Time timestamp = CXWindowsUtil::getCurrentTime( + m_display, m_clipboard[id]->getWindow()); + + // copy the clipboard + return CClipboard::copy(clipboard, m_clipboard[id], timestamp); +} + +void +CXWindowsScreen::getShape(SInt32& x, SInt32& y, SInt32& w, SInt32& h) const +{ + x = m_x; + y = m_y; + w = m_w; + h = m_h; +} + +void +CXWindowsScreen::getCursorPos(SInt32& x, SInt32& y) const +{ + Window root, window; + int mx, my, xWindow, yWindow; + unsigned int mask; + if (XQueryPointer(m_display, m_root, &root, &window, + &mx, &my, &xWindow, &yWindow, &mask)) { + x = mx; + y = my; + } + else { + x = m_xCenter; + y = m_yCenter; + } +} + +void +CXWindowsScreen::reconfigure(UInt32) +{ + // do nothing +} + +void +CXWindowsScreen::warpCursor(SInt32 x, SInt32 y) +{ + // warp mouse + warpCursorNoFlush(x, y); + + // remove all input events before and including warp + XEvent event; + while (XCheckMaskEvent(m_display, PointerMotionMask | + ButtonPressMask | ButtonReleaseMask | + KeyPressMask | KeyReleaseMask | + KeymapStateMask, + &event)) { + // do nothing + } + + // save position as last position + m_xCursor = x; + m_yCursor = y; +} + +UInt32 +CXWindowsScreen::registerHotKey(KeyID key, KeyModifierMask mask) +{ + // only allow certain modifiers + if ((mask & ~(KeyModifierShift | KeyModifierControl | + KeyModifierAlt | KeyModifierSuper)) != 0) { + LOG((CLOG_DEBUG "could not map hotkey id=%04x mask=%04x", key, mask)); + return 0; + } + + // fail if no keys + if (key == kKeyNone && mask == 0) { + return 0; + } + + // convert to X + unsigned int modifiers; + if (!m_keyState->mapModifiersToX(mask, modifiers)) { + // can't map all modifiers + LOG((CLOG_DEBUG "could not map hotkey id=%04x mask=%04x", key, mask)); + return 0; + } + CXWindowsKeyState::CKeycodeList keycodes; + m_keyState->mapKeyToKeycodes(key, keycodes); + if (key != kKeyNone && keycodes.empty()) { + // can't map key + LOG((CLOG_DEBUG "could not map hotkey id=%04x mask=%04x", key, mask)); + return 0; + } + + // choose hotkey id + UInt32 id; + if (!m_oldHotKeyIDs.empty()) { + id = m_oldHotKeyIDs.back(); + m_oldHotKeyIDs.pop_back(); + } + else { + id = m_hotKeys.size() + 1; + } + HotKeyList& hotKeys = m_hotKeys[id]; + + // all modifier hotkey must be treated specially. for each modifier + // we need to grab the modifier key in combination with all the other + // requested modifiers. + bool err = false; + { + CXWindowsUtil::CErrorLock lock(m_display, &err); + if (key == kKeyNone) { + static const KeyModifierMask s_hotKeyModifiers[] = { + KeyModifierShift, + KeyModifierControl, + KeyModifierAlt, + KeyModifierMeta, + KeyModifierSuper + }; + + XModifierKeymap* modKeymap = XGetModifierMapping(m_display); + for (size_t j = 0; j < sizeof(s_hotKeyModifiers) / + sizeof(s_hotKeyModifiers[0]) && !err; ++j) { + // skip modifier if not in mask + if ((mask & s_hotKeyModifiers[j]) == 0) { + continue; + } + + // skip with error if we can't map remaining modifiers + unsigned int modifiers2; + KeyModifierMask mask2 = (mask & ~s_hotKeyModifiers[j]); + if (!m_keyState->mapModifiersToX(mask2, modifiers2)) { + err = true; + continue; + } + + // compute modifier index for modifier. there should be + // exactly one X modifier missing + int index; + switch (modifiers ^ modifiers2) { + case ShiftMask: + index = ShiftMapIndex; + break; + + case LockMask: + index = LockMapIndex; + break; + + case ControlMask: + index = ControlMapIndex; + break; + + case Mod1Mask: + index = Mod1MapIndex; + break; + + case Mod2Mask: + index = Mod2MapIndex; + break; + + case Mod3Mask: + index = Mod3MapIndex; + break; + + case Mod4Mask: + index = Mod4MapIndex; + break; + + case Mod5Mask: + index = Mod5MapIndex; + break; + + default: + err = true; + continue; + } + + // grab each key for the modifier + const KeyCode* modifiermap = + modKeymap->modifiermap + index * modKeymap->max_keypermod; + for (int k = 0; k < modKeymap->max_keypermod && !err; ++k) { + KeyCode code = modifiermap[k]; + if (modifiermap[k] != 0) { + XGrabKey(m_display, code, modifiers2, m_root, + False, GrabModeAsync, GrabModeAsync); + if (!err) { + hotKeys.push_back(std::make_pair(code, modifiers2)); + m_hotKeyToIDMap[CHotKeyItem(code, modifiers2)] = id; + } + } + } + } + XFreeModifiermap(modKeymap); + } + + // a non-modifier key must be insensitive to CapsLock, NumLock and + // ScrollLock, so we have to grab the key with every combination of + // those. + else { + // collect available toggle modifiers + unsigned int modifier; + unsigned int toggleModifiers[3]; + size_t numToggleModifiers = 0; + if (m_keyState->mapModifiersToX(KeyModifierCapsLock, modifier)) { + toggleModifiers[numToggleModifiers++] = modifier; + } + if (m_keyState->mapModifiersToX(KeyModifierNumLock, modifier)) { + toggleModifiers[numToggleModifiers++] = modifier; + } + if (m_keyState->mapModifiersToX(KeyModifierScrollLock, modifier)) { + toggleModifiers[numToggleModifiers++] = modifier; + } + + + for (CXWindowsKeyState::CKeycodeList::iterator j = keycodes.begin(); + j != keycodes.end() && !err; ++j) { + for (size_t i = 0; i < (1u << numToggleModifiers); ++i) { + // add toggle modifiers for index i + unsigned int tmpModifiers = modifiers; + if ((i & 1) != 0) { + tmpModifiers |= toggleModifiers[0]; + } + if ((i & 2) != 0) { + tmpModifiers |= toggleModifiers[1]; + } + if ((i & 4) != 0) { + tmpModifiers |= toggleModifiers[2]; + } + + // add grab + XGrabKey(m_display, *j, tmpModifiers, m_root, + False, GrabModeAsync, GrabModeAsync); + if (!err) { + hotKeys.push_back(std::make_pair(*j, tmpModifiers)); + m_hotKeyToIDMap[CHotKeyItem(*j, tmpModifiers)] = id; + } + } + } + } + } + + if (err) { + // if any failed then unregister any we did get + for (HotKeyList::iterator j = hotKeys.begin(); + j != hotKeys.end(); ++j) { + XUngrabKey(m_display, j->first, j->second, m_root); + m_hotKeyToIDMap.erase(CHotKeyItem(j->first, j->second)); + } + + m_oldHotKeyIDs.push_back(id); + m_hotKeys.erase(id); + LOG((CLOG_WARN "failed to register hotkey %s (id=%04x mask=%04x)", CKeyMap::formatKey(key, mask).c_str(), key, mask)); + return 0; + } + + LOG((CLOG_DEBUG "registered hotkey %s (id=%04x mask=%04x) as id=%d", CKeyMap::formatKey(key, mask).c_str(), key, mask, id)); + return id; +} + +void +CXWindowsScreen::unregisterHotKey(UInt32 id) +{ + // look up hotkey + HotKeyMap::iterator i = m_hotKeys.find(id); + if (i == m_hotKeys.end()) { + return; + } + + // unregister with OS + bool err = false; + { + CXWindowsUtil::CErrorLock lock(m_display, &err); + HotKeyList& hotKeys = i->second; + for (HotKeyList::iterator j = hotKeys.begin(); + j != hotKeys.end(); ++j) { + XUngrabKey(m_display, j->first, j->second, m_root); + m_hotKeyToIDMap.erase(CHotKeyItem(j->first, j->second)); + } + } + if (err) { + LOG((CLOG_WARN "failed to unregister hotkey id=%d", id)); + } + else { + LOG((CLOG_DEBUG "unregistered hotkey id=%d", id)); + } + + // discard hot key from map and record old id for reuse + m_hotKeys.erase(i); + m_oldHotKeyIDs.push_back(id); +} + +void +CXWindowsScreen::fakeInputBegin() +{ + // FIXME -- not implemented +} + +void +CXWindowsScreen::fakeInputEnd() +{ + // FIXME -- not implemented +} + +SInt32 +CXWindowsScreen::getJumpZoneSize() const +{ + return 1; +} + +bool +CXWindowsScreen::isAnyMouseButtonDown() const +{ + // query the pointer to get the button state + Window root, window; + int xRoot, yRoot, xWindow, yWindow; + unsigned int state; + if (XQueryPointer(m_display, m_root, &root, &window, + &xRoot, &yRoot, &xWindow, &yWindow, &state)) { + return ((state & (Button1Mask | Button2Mask | Button3Mask | + Button4Mask | Button5Mask)) != 0); + } + + return false; +} + +void +CXWindowsScreen::getCursorCenter(SInt32& x, SInt32& y) const +{ + x = m_xCenter; + y = m_yCenter; +} + +void +CXWindowsScreen::fakeMouseButton(ButtonID button, bool press) +{ + const unsigned int xButton = mapButtonToX(button); + if (xButton != 0) { + XTestFakeButtonEvent(m_display, xButton, + press ? True : False, CurrentTime); + XFlush(m_display); + } +} + +void +CXWindowsScreen::fakeMouseMove(SInt32 x, SInt32 y) const +{ + if (m_xinerama && m_xtestIsXineramaUnaware) { + XWarpPointer(m_display, None, m_root, 0, 0, 0, 0, x, y); + } + else { + XTestFakeMotionEvent(m_display, DefaultScreen(m_display), + x, y, CurrentTime); + } + XFlush(m_display); +} + +void +CXWindowsScreen::fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const +{ + // FIXME -- ignore xinerama for now + if (false && m_xinerama && m_xtestIsXineramaUnaware) { +// XWarpPointer(m_display, None, m_root, 0, 0, 0, 0, x, y); + } + else { + XTestFakeRelativeMotionEvent(m_display, dx, dy, CurrentTime); + } + XFlush(m_display); +} + +void +CXWindowsScreen::fakeMouseWheel(SInt32, SInt32 yDelta) const +{ + // XXX -- support x-axis scrolling + if (yDelta == 0) { + return; + } + + // choose button depending on rotation direction + const unsigned int xButton = mapButtonToX(static_cast( + (yDelta >= 0) ? -1 : -2)); + if (xButton == 0) { + // If we get here, then the XServer does not support the scroll + // wheel buttons, so send PageUp/PageDown keystrokes instead. + // Patch by Tom Chadwick. + KeyCode keycode = 0; + if (yDelta >= 0) { + keycode = XKeysymToKeycode(m_display, XK_Page_Up); + } + else { + keycode = XKeysymToKeycode(m_display, XK_Page_Down); + } + if (keycode != 0) { + XTestFakeKeyEvent(m_display, keycode, True, CurrentTime); + XTestFakeKeyEvent(m_display, keycode, False, CurrentTime); + } + return; + } + + // now use absolute value of delta + if (yDelta < 0) { + yDelta = -yDelta; + } + + if (yDelta < m_mouseScrollDelta) { + LOG((CLOG_WARN "Wheel scroll delta (%d) smaller than threshold (%d)", yDelta, m_mouseScrollDelta)); + } + + // send as many clicks as necessary + for (; yDelta >= m_mouseScrollDelta; yDelta -= m_mouseScrollDelta) { + XTestFakeButtonEvent(m_display, xButton, True, CurrentTime); + XTestFakeButtonEvent(m_display, xButton, False, CurrentTime); + } + XFlush(m_display); +} + +Display* +CXWindowsScreen::openDisplay(const char* displayName) +{ + // get the DISPLAY + if (displayName == NULL) { + displayName = getenv("DISPLAY"); + if (displayName == NULL) { + displayName = ":0.0"; + } + } + + // open the display + LOG((CLOG_DEBUG "XOpenDisplay(\"%s\")", displayName)); + Display* display = XOpenDisplay(displayName); + if (display == NULL) { + throw XScreenUnavailable(60.0); + } + + // verify the availability of the XTest extension + if (!m_isPrimary) { + int majorOpcode, firstEvent, firstError; + if (!XQueryExtension(display, XTestExtensionName, + &majorOpcode, &firstEvent, &firstError)) { + LOG((CLOG_ERR "XTEST extension not available")); + XCloseDisplay(display); + throw XScreenOpenFailure(); + } + } + +#if HAVE_XKB_EXTENSION + { + m_xkb = false; + int major = XkbMajorVersion, minor = XkbMinorVersion; + if (XkbLibraryVersion(&major, &minor)) { + int opcode, firstError; + if (XkbQueryExtension(display, &opcode, &m_xkbEventBase, + &firstError, &major, &minor)) { + m_xkb = true; + XkbSelectEvents(display, XkbUseCoreKbd, + XkbMapNotifyMask, XkbMapNotifyMask); + XkbSelectEventDetails(display, XkbUseCoreKbd, + XkbStateNotifyMask, + XkbGroupStateMask, XkbGroupStateMask); + } + } + } +#endif + +#if HAVE_X11_EXTENSIONS_XRANDR_H + // query for XRandR extension + int dummyError; + m_xrandr = XRRQueryExtension(display, &m_xrandrEventBase, &dummyError); + if (m_xrandr) { + // enable XRRScreenChangeNotifyEvent + XRRSelectInput(display, DefaultRootWindow(display), RRScreenChangeNotifyMask | RRCrtcChangeNotifyMask); + } +#endif + + return display; +} + +void +CXWindowsScreen::saveShape() +{ + // get shape of default screen + m_x = 0; + m_y = 0; + + m_w = WidthOfScreen(DefaultScreenOfDisplay(m_display)); + m_h = HeightOfScreen(DefaultScreenOfDisplay(m_display)); + +#if HAVE_X11_EXTENSIONS_XRANDR_H + if (m_xrandr){ + int numSizes; + XRRScreenSize* xrrs; + Rotation rotation; + xrrs = XRRSizes(m_display, DefaultScreen(m_display), &numSizes); + XRRRotations(m_display, DefaultScreen(m_display), &rotation); + if (xrrs != NULL) { + if (rotation & (RR_Rotate_90|RR_Rotate_270) ){ + m_w = xrrs->height; + m_h = xrrs->width; + } + } + } +#endif + + // get center of default screen + m_xCenter = m_x + (m_w >> 1); + m_yCenter = m_y + (m_h >> 1); + + // check if xinerama is enabled and there is more than one screen. + // get center of first Xinerama screen. Xinerama appears to have + // a bug when XWarpPointer() is used in combination with + // XGrabPointer(). in that case, the warp is successful but the + // next pointer motion warps the pointer again, apparently to + // constrain it to some unknown region, possibly the region from + // 0,0 to Wm,Hm where Wm (Hm) is the minimum width (height) over + // all physical screens. this warp only seems to happen if the + // pointer wasn't in that region before the XWarpPointer(). the + // second (unexpected) warp causes synergy to think the pointer + // has been moved when it hasn't. to work around the problem, + // we warp the pointer to the center of the first physical + // screen instead of the logical screen. + m_xinerama = false; +#if HAVE_X11_EXTENSIONS_XINERAMA_H + int eventBase, errorBase; + if (XineramaQueryExtension(m_display, &eventBase, &errorBase) && + XineramaIsActive(m_display)) { + int numScreens; + XineramaScreenInfo* screens; + screens = XineramaQueryScreens(m_display, &numScreens); + if (screens != NULL) { + if (numScreens > 1) { + m_xinerama = true; + m_xCenter = screens[0].x_org + (screens[0].width >> 1); + m_yCenter = screens[0].y_org + (screens[0].height >> 1); + } + XFree(screens); + } + } +#endif +} + +Window +CXWindowsScreen::openWindow() const +{ + // default window attributes. we don't want the window manager + // messing with our window and we don't want the cursor to be + // visible inside the window. + XSetWindowAttributes attr; + attr.do_not_propagate_mask = 0; + attr.override_redirect = True; + attr.cursor = createBlankCursor(); + + // adjust attributes and get size and shape + SInt32 x, y, w, h; + if (m_isPrimary) { + // grab window attributes. this window is used to capture user + // input when the user is focused on another client. it covers + // the whole screen. + attr.event_mask = PointerMotionMask | + ButtonPressMask | ButtonReleaseMask | + KeyPressMask | KeyReleaseMask | + KeymapStateMask | PropertyChangeMask; + x = m_x; + y = m_y; + w = m_w; + h = m_h; + } + else { + // cursor hider window attributes. this window is used to hide the + // cursor when it's not on the screen. the window is hidden as soon + // as the cursor enters the screen or the display's real mouse is + // moved. we'll reposition the window as necessary so its + // position here doesn't matter. it only needs to be 1x1 because + // it only needs to contain the cursor's hotspot. + attr.event_mask = LeaveWindowMask; + x = 0; + y = 0; + w = 1; + h = 1; + } + + // create and return the window + Window window = XCreateWindow(m_display, m_root, x, y, w, h, 0, 0, + InputOnly, CopyFromParent, + CWDontPropagate | CWEventMask | + CWOverrideRedirect | CWCursor, + &attr); + if (window == None) { + throw XScreenOpenFailure(); + } + return window; +} + +void +CXWindowsScreen::openIM() +{ + // open the input methods + XIM im = XOpenIM(m_display, NULL, NULL, NULL); + if (im == NULL) { + LOG((CLOG_INFO "no support for IM")); + return; + } + + // find the appropriate style. synergy supports XIMPreeditNothing + // only at the moment. + XIMStyles* styles; + if (XGetIMValues(im, XNQueryInputStyle, &styles, NULL) != NULL || + styles == NULL) { + LOG((CLOG_WARN "cannot get IM styles")); + XCloseIM(im); + return; + } + XIMStyle style = 0; + for (unsigned short i = 0; i < styles->count_styles; ++i) { + style = styles->supported_styles[i]; + if ((style & XIMPreeditNothing) != 0) { + if ((style & (XIMStatusNothing | XIMStatusNone)) != 0) { + break; + } + } + } + XFree(styles); + if (style == 0) { + LOG((CLOG_INFO "no supported IM styles")); + XCloseIM(im); + return; + } + + // create an input context for the style and tell it about our window + XIC ic = XCreateIC(im, XNInputStyle, style, XNClientWindow, m_window, NULL); + if (ic == NULL) { + LOG((CLOG_WARN "cannot create IC")); + XCloseIM(im); + return; + } + + // find out the events we must select for and do so + unsigned long mask; + if (XGetICValues(ic, XNFilterEvents, &mask, NULL) != NULL) { + LOG((CLOG_WARN "cannot get IC filter events")); + XDestroyIC(ic); + XCloseIM(im); + return; + } + + // we have IM + m_im = im; + m_ic = ic; + m_lastKeycode = 0; + + // select events on our window that IM requires + XWindowAttributes attr; + XGetWindowAttributes(m_display, m_window, &attr); + XSelectInput(m_display, m_window, attr.your_event_mask | mask); +} + +void +CXWindowsScreen::sendEvent(CEvent::Type type, void* data) +{ + m_eventQueue.addEvent(CEvent(type, getEventTarget(), data)); +} + +void +CXWindowsScreen::sendClipboardEvent(CEvent::Type type, ClipboardID id) +{ + CClipboardInfo* info = (CClipboardInfo*)malloc(sizeof(CClipboardInfo)); + info->m_id = id; + info->m_sequenceNumber = m_sequenceNumber; + sendEvent(type, info); +} + +IKeyState* +CXWindowsScreen::getKeyState() const +{ + return m_keyState; +} + +Bool +CXWindowsScreen::findKeyEvent(Display*, XEvent* xevent, XPointer arg) +{ + CKeyEventFilter* filter = reinterpret_cast(arg); + return (xevent->type == filter->m_event && + xevent->xkey.window == filter->m_window && + xevent->xkey.time == filter->m_time && + xevent->xkey.keycode == filter->m_keycode) ? True : False; +} + +void +CXWindowsScreen::handleSystemEvent(const CEvent& event, void*) +{ + XEvent* xevent = reinterpret_cast(event.getData()); + assert(xevent != NULL); + + // update key state + bool isRepeat = false; + if (m_isPrimary) { + if (xevent->type == KeyRelease) { + // check if this is a key repeat by getting the next + // KeyPress event that has the same key and time as + // this release event, if any. first prepare the + // filter info. + CKeyEventFilter filter; + filter.m_event = KeyPress; + filter.m_window = xevent->xkey.window; + filter.m_time = xevent->xkey.time; + filter.m_keycode = xevent->xkey.keycode; + XEvent xevent2; + isRepeat = (XCheckIfEvent(m_display, &xevent2, + &CXWindowsScreen::findKeyEvent, + (XPointer)&filter) == True); + } + + if (xevent->type == KeyPress || xevent->type == KeyRelease) { + if (xevent->xkey.window == m_root) { + // this is a hot key + onHotKey(xevent->xkey, isRepeat); + return; + } + else if (!m_isOnScreen) { + // this might be a hot key + if (onHotKey(xevent->xkey, isRepeat)) { + return; + } + } + + bool down = (isRepeat || xevent->type == KeyPress); + KeyModifierMask state = + m_keyState->mapModifiersFromX(xevent->xkey.state); + m_keyState->onKey(xevent->xkey.keycode, down, state); + } + } + + // let input methods try to handle event first + if (m_ic != NULL) { + // XFilterEvent() may eat the event and generate a new KeyPress + // event with a keycode of 0 because there isn't an actual key + // associated with the keysym. but the KeyRelease may pass + // through XFilterEvent() and keep its keycode. this means + // there's a mismatch between KeyPress and KeyRelease keycodes. + // since we use the keycode on the client to detect when a key + // is released this won't do. so we remember the keycode on + // the most recent KeyPress (and clear it on a matching + // KeyRelease) so we have a keycode for a synthesized KeyPress. + if (xevent->type == KeyPress && xevent->xkey.keycode != 0) { + m_lastKeycode = xevent->xkey.keycode; + } + else if (xevent->type == KeyRelease && + xevent->xkey.keycode == m_lastKeycode) { + m_lastKeycode = 0; + } + + // now filter the event + if (XFilterEvent(xevent, None)) { + if (xevent->type == KeyPress) { + // add filtered presses to the filtered list + m_filtered.insert(m_lastKeycode); + } + return; + } + + // discard matching key releases for key presses that were + // filtered and remove them from our filtered list. + else if (xevent->type == KeyRelease && + m_filtered.count(xevent->xkey.keycode) > 0) { + m_filtered.erase(xevent->xkey.keycode); + return; + } + } + + // let screen saver have a go + if (m_screensaver->handleXEvent(xevent)) { + // screen saver handled it + return; + } + + if (m_xi2detected) { + // Process RawMotion + XGenericEventCookie *cookie = (XGenericEventCookie*)&xevent->xcookie; + if (XGetEventData(m_display, cookie) && + cookie->type == GenericEvent && + cookie->extension == xi_opcode) { + if (cookie->evtype == XI_RawMotion) { + // Get current pointer's position + Window root, child; + XMotionEvent xmotion; + xmotion.type = MotionNotify; + xmotion.send_event = False; // Raw motion + xmotion.display = m_display; + xmotion.window = m_window; + /* xmotion's time, state and is_hint are not used */ + unsigned int msk; + xmotion.same_screen = XQueryPointer( + m_display, m_root, &xmotion.root, &xmotion.subwindow, + &xmotion.x_root, + &xmotion.y_root, + &xmotion.x, + &xmotion.y, + &msk); + onMouseMove(xmotion); + XFreeEventData(m_display, cookie); + return; + } + XFreeEventData(m_display, cookie); + } + } + + // handle the event ourself + switch (xevent->type) { + case CreateNotify: + if (m_isPrimary) { + // select events on new window + selectEvents(xevent->xcreatewindow.window); + } + break; + + case MappingNotify: + refreshKeyboard(xevent); + break; + + case LeaveNotify: + if (!m_isPrimary) { + // mouse moved out of hider window somehow. hide the window. + XUnmapWindow(m_display, m_window); + } + break; + + case SelectionClear: + { + // we just lost the selection. that means someone else + // grabbed the selection so this screen is now the + // selection owner. report that to the receiver. + ClipboardID id = getClipboardID(xevent->xselectionclear.selection); + if (id != kClipboardEnd) { + LOG((CLOG_DEBUG "lost clipboard %d ownership at time %d", id, xevent->xselectionclear.time)); + m_clipboard[id]->lost(xevent->xselectionclear.time); + sendClipboardEvent(getClipboardGrabbedEvent(), id); + return; + } + } + break; + + case SelectionNotify: + // notification of selection transferred. we shouldn't + // get this here because we handle them in the selection + // retrieval methods. we'll just delete the property + // with the data (satisfying the usual ICCCM protocol). + if (xevent->xselection.property != None) { + XDeleteProperty(m_display, + xevent->xselection.requestor, + xevent->xselection.property); + } + break; + + case SelectionRequest: + { + // somebody is asking for clipboard data + ClipboardID id = getClipboardID( + xevent->xselectionrequest.selection); + if (id != kClipboardEnd) { + m_clipboard[id]->addRequest( + xevent->xselectionrequest.owner, + xevent->xselectionrequest.requestor, + xevent->xselectionrequest.target, + xevent->xselectionrequest.time, + xevent->xselectionrequest.property); + return; + } + } + break; + + case PropertyNotify: + // property delete may be part of a selection conversion + if (xevent->xproperty.state == PropertyDelete) { + processClipboardRequest(xevent->xproperty.window, + xevent->xproperty.time, + xevent->xproperty.atom); + } + break; + + case DestroyNotify: + // looks like one of the windows that requested a clipboard + // transfer has gone bye-bye. + destroyClipboardRequest(xevent->xdestroywindow.window); + break; + + case KeyPress: + if (m_isPrimary) { + onKeyPress(xevent->xkey); + } + return; + + case KeyRelease: + if (m_isPrimary) { + onKeyRelease(xevent->xkey, isRepeat); + } + return; + + case ButtonPress: + if (m_isPrimary) { + onMousePress(xevent->xbutton); + } + return; + + case ButtonRelease: + if (m_isPrimary) { + onMouseRelease(xevent->xbutton); + } + return; + + case MotionNotify: + if (m_isPrimary) { + onMouseMove(xevent->xmotion); + } + return; + + default: +#if HAVE_XKB_EXTENSION + if (m_xkb && xevent->type == m_xkbEventBase) { + XkbEvent* xkbEvent = reinterpret_cast(xevent); + switch (xkbEvent->any.xkb_type) { + case XkbMapNotify: + refreshKeyboard(xevent); + return; + + case XkbStateNotify: + LOG((CLOG_INFO "group change: %d", xkbEvent->state.group)); + m_keyState->setActiveGroup((SInt32)xkbEvent->state.group); + return; + } + } +#endif + +#if HAVE_X11_EXTENSIONS_XRANDR_H + if (m_xrandr) { + if (xevent->type == m_xrandrEventBase + RRScreenChangeNotify + || xevent->type == m_xrandrEventBase + RRNotify + && reinterpret_cast(xevent)->subtype == RRNotify_CrtcChange) { + LOG((CLOG_INFO "XRRScreenChangeNotifyEvent or RRNotify_CrtcChange received")); + + // we're required to call back into XLib so XLib can update its internal state + XRRUpdateConfiguration(xevent); + + // requery/recalculate the screen shape + saveShape(); + + // we need to resize m_window, otherwise we'll get a weird problem where moving + // off the server onto the client causes the pointer to warp to the + // center of the server (so you can't move the pointer off the server) + if (m_isPrimary) { + XMoveWindow(m_display, m_window, m_x, m_y); + XResizeWindow(m_display, m_window, m_w, m_h); + } + } + } +#endif + + break; + } +} + +void +CXWindowsScreen::onKeyPress(XKeyEvent& xkey) +{ + LOG((CLOG_DEBUG1 "event: KeyPress code=%d, state=0x%04x", xkey.keycode, xkey.state)); + const KeyModifierMask mask = m_keyState->mapModifiersFromX(xkey.state); + KeyID key = mapKeyFromX(&xkey); + if (key != kKeyNone) { + // check for ctrl+alt+del emulation + if ((key == kKeyPause || key == kKeyBreak) && + (mask & (KeyModifierControl | KeyModifierAlt)) == + (KeyModifierControl | KeyModifierAlt)) { + // pretend it's ctrl+alt+del + LOG((CLOG_DEBUG "emulate ctrl+alt+del")); + key = kKeyDelete; + } + + // get which button. see call to XFilterEvent() in onEvent() + // for more info. + bool isFake = false; + KeyButton keycode = static_cast(xkey.keycode); + if (keycode == 0) { + isFake = true; + keycode = static_cast(m_lastKeycode); + if (keycode == 0) { + // no keycode + return; + } + } + + // handle key + m_keyState->sendKeyEvent(getEventTarget(), + true, false, key, mask, 1, keycode); + + // do fake release if this is a fake press + if (isFake) { + m_keyState->sendKeyEvent(getEventTarget(), + false, false, key, mask, 1, keycode); + } + } +} + +void +CXWindowsScreen::onKeyRelease(XKeyEvent& xkey, bool isRepeat) +{ + const KeyModifierMask mask = m_keyState->mapModifiersFromX(xkey.state); + KeyID key = mapKeyFromX(&xkey); + if (key != kKeyNone) { + // check for ctrl+alt+del emulation + if ((key == kKeyPause || key == kKeyBreak) && + (mask & (KeyModifierControl | KeyModifierAlt)) == + (KeyModifierControl | KeyModifierAlt)) { + // pretend it's ctrl+alt+del and ignore autorepeat + LOG((CLOG_DEBUG "emulate ctrl+alt+del")); + key = kKeyDelete; + isRepeat = false; + } + + KeyButton keycode = static_cast(xkey.keycode); + if (!isRepeat) { + // no press event follows so it's a plain release + LOG((CLOG_DEBUG1 "event: KeyRelease code=%d, state=0x%04x", keycode, xkey.state)); + m_keyState->sendKeyEvent(getEventTarget(), + false, false, key, mask, 1, keycode); + } + else { + // found a press event following so it's a repeat. + // we could attempt to count the already queued + // repeats but we'll just send a repeat of 1. + // note that we discard the press event. + LOG((CLOG_DEBUG1 "event: repeat code=%d, state=0x%04x", keycode, xkey.state)); + m_keyState->sendKeyEvent(getEventTarget(), + false, true, key, mask, 1, keycode); + } + } +} + +bool +CXWindowsScreen::onHotKey(XKeyEvent& xkey, bool isRepeat) +{ + // find the hot key id + HotKeyToIDMap::const_iterator i = + m_hotKeyToIDMap.find(CHotKeyItem(xkey.keycode, xkey.state)); + if (i == m_hotKeyToIDMap.end()) { + return false; + } + + // find what kind of event + CEvent::Type type; + if (xkey.type == KeyPress) { + type = getHotKeyDownEvent(); + } + else if (xkey.type == KeyRelease) { + type = getHotKeyUpEvent(); + } + else { + return false; + } + + // generate event (ignore key repeats) + if (!isRepeat) { + m_eventQueue.addEvent(CEvent(type, getEventTarget(), + CHotKeyInfo::alloc(i->second))); + } + return true; +} + +void +CXWindowsScreen::onMousePress(const XButtonEvent& xbutton) +{ + LOG((CLOG_DEBUG1 "event: ButtonPress button=%d", xbutton.button)); + ButtonID button = mapButtonFromX(&xbutton); + KeyModifierMask mask = m_keyState->mapModifiersFromX(xbutton.state); + if (button != kButtonNone) { + sendEvent(getButtonDownEvent(), CButtonInfo::alloc(button, mask)); + } +} + +void +CXWindowsScreen::onMouseRelease(const XButtonEvent& xbutton) +{ + LOG((CLOG_DEBUG1 "event: ButtonRelease button=%d", xbutton.button)); + ButtonID button = mapButtonFromX(&xbutton); + KeyModifierMask mask = m_keyState->mapModifiersFromX(xbutton.state); + if (button != kButtonNone) { + sendEvent(getButtonUpEvent(), CButtonInfo::alloc(button, mask)); + } + else if (xbutton.button == 4) { + // wheel forward (away from user) + sendEvent(getWheelEvent(), CWheelInfo::alloc(0, 120)); + } + else if (xbutton.button == 5) { + // wheel backward (toward user) + sendEvent(getWheelEvent(), CWheelInfo::alloc(0, -120)); + } + // XXX -- support x-axis scrolling +} + +void +CXWindowsScreen::onMouseMove(const XMotionEvent& xmotion) +{ + LOG((CLOG_DEBUG2 "event: MotionNotify %d,%d", xmotion.x_root, xmotion.y_root)); + + // compute motion delta (relative to the last known + // mouse position) + SInt32 x = xmotion.x_root - m_xCursor; + SInt32 y = xmotion.y_root - m_yCursor; + + // save position to compute delta of next motion + m_xCursor = xmotion.x_root; + m_yCursor = xmotion.y_root; + + if (xmotion.send_event) { + // we warped the mouse. discard events until we + // find the matching sent event. see + // warpCursorNoFlush() for where the events are + // sent. we discard the matching sent event and + // can be sure we've skipped the warp event. + XEvent xevent; + char cntr = 0; + do { + XMaskEvent(m_display, PointerMotionMask, &xevent); + if (cntr++ > 10) { + LOG((CLOG_WARN "too many discarded events! %d", cntr)); + break; + } + } while (!xevent.xany.send_event); + cntr = 0; + } + else if (m_isOnScreen) { + // motion on primary screen + sendEvent(getMotionOnPrimaryEvent(), + CMotionInfo::alloc(m_xCursor, m_yCursor)); + } + else { + // motion on secondary screen. warp mouse back to + // center. + // + // my lombard (powerbook g3) running linux and + // using the adbmouse driver has two problems: + // first, the driver only sends motions of +/-2 + // pixels and, second, it seems to discard some + // physical input after a warp. the former isn't a + // big deal (we're just limited to every other + // pixel) but the latter is a PITA. to work around + // it we only warp when the mouse has moved more + // than s_size pixels from the center. + static const SInt32 s_size = 32; + if (xmotion.x_root - m_xCenter < -s_size || + xmotion.x_root - m_xCenter > s_size || + xmotion.y_root - m_yCenter < -s_size || + xmotion.y_root - m_yCenter > s_size) { + warpCursorNoFlush(m_xCenter, m_yCenter); + } + + // send event if mouse moved. do this after warping + // back to center in case the motion takes us onto + // the primary screen. if we sent the event first + // in that case then the warp would happen after + // warping to the primary screen's enter position, + // effectively overriding it. + if (x != 0 || y != 0) { + sendEvent(getMotionOnSecondaryEvent(), CMotionInfo::alloc(x, y)); + } + } +} + +Cursor +CXWindowsScreen::createBlankCursor() const +{ + // this seems just a bit more complicated than really necessary + + // get the closet cursor size to 1x1 + unsigned int w, h; + XQueryBestCursor(m_display, m_root, 1, 1, &w, &h); + + // make bitmap data for cursor of closet size. since the cursor + // is blank we can use the same bitmap for shape and mask: all + // zeros. + const int size = ((w + 7) >> 3) * h; + char* data = new char[size]; + memset(data, 0, size); + + // make bitmap + Pixmap bitmap = XCreateBitmapFromData(m_display, m_root, data, w, h); + + // need an arbitrary color for the cursor + XColor color; + color.pixel = 0; + color.red = color.green = color.blue = 0; + color.flags = DoRed | DoGreen | DoBlue; + + // make cursor from bitmap + Cursor cursor = XCreatePixmapCursor(m_display, bitmap, bitmap, + &color, &color, 0, 0); + + // don't need bitmap or the data anymore + delete[] data; + XFreePixmap(m_display, bitmap); + + return cursor; +} + +ClipboardID +CXWindowsScreen::getClipboardID(Atom selection) const +{ + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + if (m_clipboard[id] != NULL && + m_clipboard[id]->getSelection() == selection) { + return id; + } + } + return kClipboardEnd; +} + +void +CXWindowsScreen::processClipboardRequest(Window requestor, + Time time, Atom property) +{ + // check every clipboard until one returns success + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + if (m_clipboard[id] != NULL && + m_clipboard[id]->processRequest(requestor, time, property)) { + break; + } + } +} + +void +CXWindowsScreen::destroyClipboardRequest(Window requestor) +{ + // check every clipboard until one returns success + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + if (m_clipboard[id] != NULL && + m_clipboard[id]->destroyRequest(requestor)) { + break; + } + } +} + +void +CXWindowsScreen::onError() +{ + // prevent further access to the X display + m_eventQueue.adoptBuffer(NULL); + m_screensaver->destroy(); + m_screensaver = NULL; + m_display = NULL; + + // notify of failure + sendEvent(getErrorEvent(), NULL); + + // FIXME -- should ensure that we ignore operations that involve + // m_display from now on. however, Xlib will simply exit the + // application in response to the X I/O error so there's no + // point in trying to really handle the error. if we did want + // to handle the error, it'd probably be easiest to delegate to + // one of two objects. one object would take the implementation + // from this class. the other object would be stub methods that + // don't use X11. on error, we'd switch to the latter. +} + +int +CXWindowsScreen::ioErrorHandler(Display*) +{ + // the display has disconnected, probably because X is shutting + // down. X forces us to exit at this point which is annoying. + // we'll pretend as if we won't exit so we try to make sure we + // don't access the display anymore. + LOG((CLOG_CRIT "X display has unexpectedly disconnected")); + s_screen->onError(); + return 0; +} + +void +CXWindowsScreen::selectEvents(Window w) const +{ + // ignore errors while we adjust event masks. windows could be + // destroyed at any time after the XQueryTree() in doSelectEvents() + // so we must ignore BadWindow errors. + CXWindowsUtil::CErrorLock lock(m_display); + + // adjust event masks + doSelectEvents(w); +} + +void +CXWindowsScreen::doSelectEvents(Window w) const +{ + // we want to track the mouse everywhere on the display. to achieve + // that we select PointerMotionMask on every window. we also select + // SubstructureNotifyMask in order to get CreateNotify events so we + // select events on new windows too. + // + // note that this can break certain clients due a design flaw of X. + // X will deliver a PointerMotion event to the deepest window in the + // hierarchy that contains the pointer and has PointerMotionMask + // selected by *any* client. if another client doesn't select + // motion events in a subwindow so the parent window will get them + // then by selecting for motion events on the subwindow we break + // that client because the parent will no longer get the events. + + // FIXME -- should provide some workaround for event selection + // design flaw. perhaps only select for motion events on windows + // that already do or are top-level windows or don't propagate + // pointer events. or maybe an option to simply poll the mouse. + + // we don't want to adjust our grab window + if (w == m_window) { + return; + } + + // select events of interest. do this before querying the tree so + // we'll get notifications of children created after the XQueryTree() + // so we won't miss them. + XSelectInput(m_display, w, PointerMotionMask | SubstructureNotifyMask); + + // recurse on child windows + Window rw, pw, *cw; + unsigned int nc; + if (XQueryTree(m_display, w, &rw, &pw, &cw, &nc)) { + for (unsigned int i = 0; i < nc; ++i) { + doSelectEvents(cw[i]); + } + XFree(cw); + } +} + +KeyID +CXWindowsScreen::mapKeyFromX(XKeyEvent* event) const +{ + // convert to a keysym + KeySym keysym; + if (event->type == KeyPress && m_ic != NULL) { + // do multibyte lookup. can only call XmbLookupString with a + // key press event and a valid XIC so we checked those above. + char scratch[32]; + int n = sizeof(scratch) / sizeof(scratch[0]); + char* buffer = scratch; + int status; + n = XmbLookupString(m_ic, event, buffer, n, &keysym, &status); + if (status == XBufferOverflow) { + // not enough space. grow buffer and try again. + buffer = new char[n]; + n = XmbLookupString(m_ic, event, buffer, n, &keysym, &status); + delete[] buffer; + } + + // see what we got. since we don't care about the string + // we'll just look for a keysym. + switch (status) { + default: + case XLookupNone: + case XLookupChars: + keysym = 0; + break; + + case XLookupKeySym: + case XLookupBoth: + break; + } + } + else { + // plain old lookup + char dummy[1]; + XLookupString(event, dummy, 0, &keysym, NULL); + } + + // convert key + return CXWindowsUtil::mapKeySymToKeyID(keysym); +} + +ButtonID +CXWindowsScreen::mapButtonFromX(const XButtonEvent* event) const +{ + unsigned int button = event->button; + + // first three buttons map to 1, 2, 3 (kButtonLeft, Middle, Right) + if (button >= 1 && button <= 3) { + return static_cast(button); + } + + // buttons 4 and 5 are ignored here. they're used for the wheel. + // buttons 6, 7, etc and up map to 4, 5, etc. + else if (button >= 6) { + return static_cast(button - 2); + } + + // unknown button + else { + return kButtonNone; + } +} + +unsigned int +CXWindowsScreen::mapButtonToX(ButtonID id) const +{ + // map button -1 to button 4 (+wheel) + if (id == static_cast(-1)) { + id = 4; + } + + // map button -2 to button 5 (-wheel) + else if (id == static_cast(-2)) { + id = 5; + } + + // map buttons 4, 5, etc. to 6, 7, etc. to make room for buttons + // 4 and 5 used to simulate the mouse wheel. + else if (id >= 4) { + id += 2; + } + + // check button is in legal range + if (id < 1 || id > m_buttons.size()) { + // out of range + return 0; + } + + // map button + return static_cast(id); +} + +void +CXWindowsScreen::warpCursorNoFlush(SInt32 x, SInt32 y) +{ + assert(m_window != None); + + // send an event that we can recognize before the mouse warp + XEvent eventBefore; + eventBefore.type = MotionNotify; + eventBefore.xmotion.display = m_display; + eventBefore.xmotion.window = m_window; + eventBefore.xmotion.root = m_root; + eventBefore.xmotion.subwindow = m_window; + eventBefore.xmotion.time = CurrentTime; + eventBefore.xmotion.x = x; + eventBefore.xmotion.y = y; + eventBefore.xmotion.x_root = x; + eventBefore.xmotion.y_root = y; + eventBefore.xmotion.state = 0; + eventBefore.xmotion.is_hint = NotifyNormal; + eventBefore.xmotion.same_screen = True; + XEvent eventAfter = eventBefore; + XSendEvent(m_display, m_window, False, 0, &eventBefore); + + // warp mouse + XWarpPointer(m_display, None, m_root, 0, 0, 0, 0, x, y); + + // send an event that we can recognize after the mouse warp + XSendEvent(m_display, m_window, False, 0, &eventAfter); + XSync(m_display, False); + + LOG((CLOG_DEBUG2 "warped to %d,%d", x, y)); +} + +void +CXWindowsScreen::updateButtons() +{ + // query the button mapping + UInt32 numButtons = XGetPointerMapping(m_display, NULL, 0); + unsigned char* tmpButtons = new unsigned char[numButtons]; + XGetPointerMapping(m_display, tmpButtons, numButtons); + + // find the largest logical button id + unsigned char maxButton = 0; + for (UInt32 i = 0; i < numButtons; ++i) { + if (tmpButtons[i] > maxButton) { + maxButton = tmpButtons[i]; + } + } + + // allocate button array + m_buttons.resize(maxButton); + + // fill in button array values. m_buttons[i] is the physical + // button number for logical button i+1. + for (UInt32 i = 0; i < numButtons; ++i) { + m_buttons[i] = 0; + } + for (UInt32 i = 0; i < numButtons; ++i) { + m_buttons[tmpButtons[i] - 1] = i + 1; + } + + // clean up + delete[] tmpButtons; +} + +bool +CXWindowsScreen::grabMouseAndKeyboard() +{ + unsigned int event_mask = ButtonPressMask | ButtonReleaseMask | EnterWindowMask | LeaveWindowMask | PointerMotionMask; + + // grab the mouse and keyboard. keep trying until we get them. + // if we can't grab one after grabbing the other then ungrab + // and wait before retrying. give up after s_timeout seconds. + static const double s_timeout = 1.0; + int result; + CStopwatch timer; + do { + // keyboard first + do { + result = XGrabKeyboard(m_display, m_window, True, + GrabModeAsync, GrabModeAsync, CurrentTime); + assert(result != GrabNotViewable); + if (result != GrabSuccess) { + LOG((CLOG_DEBUG2 "waiting to grab keyboard")); + ARCH->sleep(0.05); + if (timer.getTime() >= s_timeout) { + LOG((CLOG_DEBUG2 "grab keyboard timed out")); + return false; + } + } + } while (result != GrabSuccess); + LOG((CLOG_DEBUG2 "grabbed keyboard")); + + // now the mouse --- use event_mask to get EnterNotify, LeaveNotify events + result = XGrabPointer(m_display, m_window, False, event_mask, + GrabModeAsync, GrabModeAsync, + m_window, None, CurrentTime); + assert(result != GrabNotViewable); + if (result != GrabSuccess) { + // back off to avoid grab deadlock + XUngrabKeyboard(m_display, CurrentTime); + LOG((CLOG_DEBUG2 "ungrabbed keyboard, waiting to grab pointer")); + ARCH->sleep(0.05); + if (timer.getTime() >= s_timeout) { + LOG((CLOG_DEBUG2 "grab pointer timed out")); + return false; + } + } + } while (result != GrabSuccess); + + LOG((CLOG_DEBUG1 "grabbed pointer and keyboard")); + return true; +} + +void +CXWindowsScreen::refreshKeyboard(XEvent* event) +{ + if (XPending(m_display) > 0) { + XEvent tmpEvent; + XPeekEvent(m_display, &tmpEvent); + if (tmpEvent.type == MappingNotify) { + // discard this event since another follows. + // we tend to get a bunch of these in a row. + return; + } + } + + // keyboard mapping changed +#if HAVE_XKB_EXTENSION + if (m_xkb && event->type == m_xkbEventBase) { + XkbRefreshKeyboardMapping((XkbMapNotifyEvent*)event); + } + else +#else + { + XRefreshKeyboardMapping(&event->xmapping); + } +#endif + m_keyState->updateKeyMap(); + m_keyState->updateKeyState(); +} + + +// +// CXWindowsScreen::CHotKeyItem +// + +CXWindowsScreen::CHotKeyItem::CHotKeyItem(int keycode, unsigned int mask) : + m_keycode(keycode), + m_mask(mask) +{ + // do nothing +} + +bool +CXWindowsScreen::CHotKeyItem::operator<(const CHotKeyItem& x) const +{ + return (m_keycode < x.m_keycode || + (m_keycode == x.m_keycode && m_mask < x.m_mask)); +} + +bool +CXWindowsScreen::detectXI2() +{ + int event, error; + return XQueryExtension(m_display, + "XInputExtension", &xi_opcode, &event, &error); +} + +void +CXWindowsScreen::selectXIRawMotion() +{ + XIEventMask mask; + + mask.deviceid = XIAllDevices; + mask.mask_len = XIMaskLen(XI_RawMotion); + mask.mask = (unsigned char*)calloc(mask.mask_len, sizeof(char)); + mask.deviceid = XIAllMasterDevices; + memset(mask.mask, 0, 2); + XISetMask(mask.mask, XI_RawKeyRelease); + XISetMask(mask.mask, XI_RawMotion); + XISelectEvents(m_display, DefaultRootWindow(m_display), &mask, 1); + free(mask.mask); +} diff --git a/src/lib/platform/CXWindowsScreen.h b/src/lib/platform/CXWindowsScreen.h new file mode 100644 index 00000000..f17fdb18 --- /dev/null +++ b/src/lib/platform/CXWindowsScreen.h @@ -0,0 +1,255 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSSCREEN_H +#define CXWINDOWSSCREEN_H + +#include "CPlatformScreen.h" +#include "stdset.h" +#include "stdvector.h" +#if X_DISPLAY_MISSING +# error X11 is required to build synergy +#else +# include +#endif +#include "CKeyMap.h" + +class CXWindowsClipboard; +class CXWindowsKeyState; +class CXWindowsScreenSaver; + +//! Implementation of IPlatformScreen for X11 +class CXWindowsScreen : public CPlatformScreen { +public: + CXWindowsScreen(const char* displayName, bool isPrimary, bool disableXInitThreads, int mouseScrollDelta, IEventQueue& eventQueue); + virtual ~CXWindowsScreen(); + + //! @name manipulators + //@{ + + //@} + + // IScreen overrides + virtual void* getEventTarget() const; + virtual bool getClipboard(ClipboardID id, IClipboard*) const; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const; + virtual void getCursorPos(SInt32& x, SInt32& y) const; + + // IPrimaryScreen overrides + virtual void reconfigure(UInt32 activeSides); + virtual void warpCursor(SInt32 x, SInt32 y); + virtual UInt32 registerHotKey(KeyID key, KeyModifierMask mask); + virtual void unregisterHotKey(UInt32 id); + virtual void fakeInputBegin(); + virtual void fakeInputEnd(); + virtual SInt32 getJumpZoneSize() const; + virtual bool isAnyMouseButtonDown() const; + virtual void getCursorCenter(SInt32& x, SInt32& y) const; + virtual void gameDeviceTimingResp(UInt16 freq) { } + + // ISecondaryScreen overrides + virtual void fakeMouseButton(ButtonID id, bool press); + virtual void fakeMouseMove(SInt32 x, SInt32 y) const; + virtual void fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const; + virtual void fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const; + virtual void fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const { } + virtual void fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const { } + virtual void fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const { } + virtual void queueGameDeviceTimingReq() const { } + + // IPlatformScreen overrides + virtual void enable(); + virtual void disable(); + virtual void enter(); + virtual bool leave(); + virtual bool setClipboard(ClipboardID, const IClipboard*); + virtual void checkClipboards(); + virtual void openScreensaver(bool notify); + virtual void closeScreensaver(); + virtual void screensaver(bool activate); + virtual void resetOptions(); + virtual void setOptions(const COptionsList& options); + virtual void setSequenceNumber(UInt32); + virtual bool isPrimary() const; + virtual void gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2) { } + +protected: + // IPlatformScreen overrides + virtual void handleSystemEvent(const CEvent&, void*); + virtual void updateButtons(); + virtual IKeyState* getKeyState() const; + +private: + // event sending + void sendEvent(CEvent::Type, void* = NULL); + void sendClipboardEvent(CEvent::Type, ClipboardID); + + // create the transparent cursor + Cursor createBlankCursor() const; + + // determine the clipboard from the X selection. returns + // kClipboardEnd if no such clipboard. + ClipboardID getClipboardID(Atom selection) const; + + // continue processing a selection request + void processClipboardRequest(Window window, + Time time, Atom property); + + // terminate a selection request + void destroyClipboardRequest(Window window); + + // X I/O error handler + void onError(); + static int ioErrorHandler(Display*); + +private: + class CKeyEventFilter { + public: + int m_event; + Window m_window; + Time m_time; + KeyCode m_keycode; + }; + + Display* openDisplay(const char* displayName); + void saveShape(); + Window openWindow() const; + void openIM(); + + bool grabMouseAndKeyboard(); + void onKeyPress(XKeyEvent&); + void onKeyRelease(XKeyEvent&, bool isRepeat); + bool onHotKey(XKeyEvent&, bool isRepeat); + void onMousePress(const XButtonEvent&); + void onMouseRelease(const XButtonEvent&); + void onMouseMove(const XMotionEvent&); + + bool detectXI2(); + void selectXIRawMotion(); + void selectEvents(Window) const; + void doSelectEvents(Window) const; + + KeyID mapKeyFromX(XKeyEvent*) const; + ButtonID mapButtonFromX(const XButtonEvent*) const; + unsigned int mapButtonToX(ButtonID id) const; + + void warpCursorNoFlush(SInt32 x, SInt32 y); + + void refreshKeyboard(XEvent*); + + static Bool findKeyEvent(Display*, XEvent* xevent, XPointer arg); + +private: + struct CHotKeyItem { + public: + CHotKeyItem(int, unsigned int); + + bool operator<(const CHotKeyItem&) const; + + private: + int m_keycode; + unsigned int m_mask; + }; + typedef std::set CFilteredKeycodes; + typedef std::vector > HotKeyList; + typedef std::map HotKeyMap; + typedef std::vector HotKeyIDList; + typedef std::map HotKeyToIDMap; + + // true if screen is being used as a primary screen, false otherwise + bool m_isPrimary; + int m_mouseScrollDelta; + + Display* m_display; + Window m_root; + Window m_window; + + // true if mouse has entered the screen + bool m_isOnScreen; + + // screen shape stuff + SInt32 m_x, m_y; + SInt32 m_w, m_h; + SInt32 m_xCenter, m_yCenter; + + // last mouse position + SInt32 m_xCursor, m_yCursor; + + // keyboard stuff + CXWindowsKeyState* m_keyState; + + // hot key stuff + HotKeyMap m_hotKeys; + HotKeyIDList m_oldHotKeyIDs; + HotKeyToIDMap m_hotKeyToIDMap; + + // input focus stuff + Window m_lastFocus; + int m_lastFocusRevert; + + // input method stuff + XIM m_im; + XIC m_ic; + KeyCode m_lastKeycode; + CFilteredKeycodes m_filtered; + + // clipboards + CXWindowsClipboard* m_clipboard[kClipboardEnd]; + UInt32 m_sequenceNumber; + + // screen saver stuff + CXWindowsScreenSaver* m_screensaver; + bool m_screensaverNotify; + + // logical to physical button mapping. m_buttons[i] gives the + // physical button for logical button i+1. + std::vector m_buttons; + + // true if global auto-repeat was enabled before we turned it off + bool m_autoRepeat; + + // stuff to workaround xtest being xinerama unaware. attempting + // to fake a mouse motion under xinerama may behave strangely, + // especially if screen 0 is not at 0,0 or if faking a motion on + // a screen other than screen 0. + bool m_xtestIsXineramaUnaware; + bool m_xinerama; + + // stuff to work around lost focus issues on certain systems + // (ie: a MythTV front-end). + bool m_preserveFocus; + + // XKB extension stuff + bool m_xkb; + int m_xkbEventBase; + + bool m_xi2detected; + + // XRandR extension stuff + bool m_xrandr; + int m_xrandrEventBase; + + IEventQueue& m_eventQueue; + CKeyMap m_keyMap; + + // pointer to (singleton) screen. this is only needed by + // ioErrorHandler(). + static CXWindowsScreen* s_screen; +}; + +#endif diff --git a/src/lib/platform/CXWindowsScreenSaver.cpp b/src/lib/platform/CXWindowsScreenSaver.cpp new file mode 100644 index 00000000..56ace48a --- /dev/null +++ b/src/lib/platform/CXWindowsScreenSaver.cpp @@ -0,0 +1,602 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsScreenSaver.h" +#include "CXWindowsUtil.h" +#include "IPlatformScreen.h" +#include "CLog.h" +#include "CEvent.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" +#include +#if HAVE_X11_EXTENSIONS_XTEST_H +# include +#else +# error The XTest extension is required to build synergy +#endif +#if HAVE_X11_EXTENSIONS_DPMS_H +extern "C" { +# include +# include +# if !HAVE_DPMS_PROTOTYPES +# undef DPMSModeOn +# undef DPMSModeStandby +# undef DPMSModeSuspend +# undef DPMSModeOff +# define DPMSModeOn 0 +# define DPMSModeStandby 1 +# define DPMSModeSuspend 2 +# define DPMSModeOff 3 +extern Bool DPMSQueryExtension(Display *, int *, int *); +extern Bool DPMSCapable(Display *); +extern Status DPMSEnable(Display *); +extern Status DPMSDisable(Display *); +extern Status DPMSForceLevel(Display *, CARD16); +extern Status DPMSInfo(Display *, CARD16 *, BOOL *); +# endif +} +#endif + +// +// CXWindowsScreenSaver +// + +CXWindowsScreenSaver::CXWindowsScreenSaver( + Display* display, Window window, void* eventTarget, IEventQueue& eventQueue) : + m_display(display), + m_xscreensaverSink(window), + m_eventTarget(eventTarget), + m_xscreensaver(None), + m_xscreensaverActive(false), + m_dpms(false), + m_disabled(false), + m_suppressDisable(false), + m_disableTimer(NULL), + m_disablePos(0), + m_eventQueue(eventQueue) +{ + // get atoms + m_atomScreenSaver = XInternAtom(m_display, + "SCREENSAVER", False); + m_atomScreenSaverVersion = XInternAtom(m_display, + "_SCREENSAVER_VERSION", False); + m_atomScreenSaverActivate = XInternAtom(m_display, + "ACTIVATE", False); + m_atomScreenSaverDeactivate = XInternAtom(m_display, + "DEACTIVATE", False); + + // check for DPMS extension. this is an alternative screen saver + // that powers down the display. +#if HAVE_X11_EXTENSIONS_DPMS_H + int eventBase, errorBase; + if (DPMSQueryExtension(m_display, &eventBase, &errorBase)) { + if (DPMSCapable(m_display)) { + // we have DPMS + m_dpms = true; + } + } +#endif + + // watch top-level windows for changes + bool error = false; + { + CXWindowsUtil::CErrorLock lock(m_display, &error); + Window root = DefaultRootWindow(m_display); + XWindowAttributes attr; + XGetWindowAttributes(m_display, root, &attr); + m_rootEventMask = attr.your_event_mask; + XSelectInput(m_display, root, m_rootEventMask | SubstructureNotifyMask); + } + if (error) { + LOG((CLOG_DEBUG "didn't set root event mask")); + m_rootEventMask = 0; + } + + // get the built-in settings + XGetScreenSaver(m_display, &m_timeout, &m_interval, + &m_preferBlanking, &m_allowExposures); + + // get the DPMS settings + m_dpmsEnabled = isDPMSEnabled(); + + // get the xscreensaver window, if any + if (!findXScreenSaver()) { + setXScreenSaver(None); + } + + // install disable timer event handler + m_eventQueue.adoptHandler(CEvent::kTimer, this, + new TMethodEventJob(this, + &CXWindowsScreenSaver::handleDisableTimer)); +} + +CXWindowsScreenSaver::~CXWindowsScreenSaver() +{ + // done with disable job + if (m_disableTimer != NULL) { + m_eventQueue.deleteTimer(m_disableTimer); + } + m_eventQueue.removeHandler(CEvent::kTimer, this); + + if (m_display != NULL) { + enableDPMS(m_dpmsEnabled); + XSetScreenSaver(m_display, m_timeout, m_interval, + m_preferBlanking, m_allowExposures); + clearWatchForXScreenSaver(); + CXWindowsUtil::CErrorLock lock(m_display); + XSelectInput(m_display, DefaultRootWindow(m_display), m_rootEventMask); + } +} + +void +CXWindowsScreenSaver::destroy() +{ + m_display = NULL; + delete this; +} + +bool +CXWindowsScreenSaver::handleXEvent(const XEvent* xevent) +{ + switch (xevent->type) { + case CreateNotify: + if (m_xscreensaver == None) { + if (isXScreenSaver(xevent->xcreatewindow.window)) { + // found the xscreensaver + setXScreenSaver(xevent->xcreatewindow.window); + } + else { + // another window to watch. to detect the xscreensaver + // window we look for a property but that property may + // not yet exist by the time we get this event so we + // have to watch the window for property changes. + // this would be so much easier if xscreensaver did the + // smart thing and stored its window in a property on + // the root window. + addWatchXScreenSaver(xevent->xcreatewindow.window); + } + } + break; + + case DestroyNotify: + if (xevent->xdestroywindow.window == m_xscreensaver) { + // xscreensaver is gone + LOG((CLOG_DEBUG "xscreensaver died")); + setXScreenSaver(None); + return true; + } + break; + + case PropertyNotify: + if (xevent->xproperty.state == PropertyNewValue) { + if (isXScreenSaver(xevent->xproperty.window)) { + // found the xscreensaver + setXScreenSaver(xevent->xcreatewindow.window); + } + } + break; + + case MapNotify: + if (xevent->xmap.window == m_xscreensaver) { + // xscreensaver has activated + setXScreenSaverActive(true); + return true; + } + break; + + case UnmapNotify: + if (xevent->xunmap.window == m_xscreensaver) { + // xscreensaver has deactivated + setXScreenSaverActive(false); + return true; + } + break; + } + + return false; +} + +void +CXWindowsScreenSaver::enable() +{ + // for xscreensaver + m_disabled = false; + updateDisableTimer(); + + // for built-in X screen saver + XSetScreenSaver(m_display, m_timeout, m_interval, + m_preferBlanking, m_allowExposures); + + // for DPMS + enableDPMS(m_dpmsEnabled); +} + +void +CXWindowsScreenSaver::disable() +{ + // for xscreensaver + m_disabled = true; + updateDisableTimer(); + + // use built-in X screen saver + XGetScreenSaver(m_display, &m_timeout, &m_interval, + &m_preferBlanking, &m_allowExposures); + XSetScreenSaver(m_display, 0, m_interval, + m_preferBlanking, m_allowExposures); + + // for DPMS + m_dpmsEnabled = isDPMSEnabled(); + enableDPMS(false); + + // FIXME -- now deactivate? +} + +void +CXWindowsScreenSaver::activate() +{ + // remove disable job timer + m_suppressDisable = true; + updateDisableTimer(); + + // enable DPMS if it was enabled + enableDPMS(m_dpmsEnabled); + + // try xscreensaver + findXScreenSaver(); + if (m_xscreensaver != None) { + sendXScreenSaverCommand(m_atomScreenSaverActivate); + return; + } + + // try built-in X screen saver + if (m_timeout != 0) { + XForceScreenSaver(m_display, ScreenSaverActive); + } + + // try DPMS + activateDPMS(true); +} + +void +CXWindowsScreenSaver::deactivate() +{ + // reinstall disable job timer + m_suppressDisable = false; + updateDisableTimer(); + + // try DPMS + activateDPMS(false); + + // disable DPMS if screen saver is disabled + if (m_disabled) { + enableDPMS(false); + } + + // try xscreensaver + findXScreenSaver(); + if (m_xscreensaver != None) { + sendXScreenSaverCommand(m_atomScreenSaverDeactivate); + return; + } + + // use built-in X screen saver + XForceScreenSaver(m_display, ScreenSaverReset); +} + +bool +CXWindowsScreenSaver::isActive() const +{ + // check xscreensaver + if (m_xscreensaver != None) { + return m_xscreensaverActive; + } + + // check DPMS + if (isDPMSActivated()) { + return true; + } + + // can't check built-in X screen saver activity + return false; +} + +bool +CXWindowsScreenSaver::findXScreenSaver() +{ + // do nothing if we've already got the xscreensaver window + if (m_xscreensaver == None) { + // find top-level window xscreensaver window + Window root = DefaultRootWindow(m_display); + Window rw, pw, *cw; + unsigned int nc; + if (XQueryTree(m_display, root, &rw, &pw, &cw, &nc)) { + for (unsigned int i = 0; i < nc; ++i) { + if (isXScreenSaver(cw[i])) { + setXScreenSaver(cw[i]); + break; + } + } + XFree(cw); + } + } + + return (m_xscreensaver != None); +} + +void +CXWindowsScreenSaver::setXScreenSaver(Window window) +{ + LOG((CLOG_DEBUG "xscreensaver window: 0x%08x", window)); + + // save window + m_xscreensaver = window; + + if (m_xscreensaver != None) { + // clear old watch list + clearWatchForXScreenSaver(); + + // see if xscreensaver is active + bool error = false; + XWindowAttributes attr; + { + CXWindowsUtil::CErrorLock lock(m_display, &error); + XGetWindowAttributes(m_display, m_xscreensaver, &attr); + } + setXScreenSaverActive(!error && attr.map_state != IsUnmapped); + + // save current DPMS state; xscreensaver may have changed it. + m_dpmsEnabled = isDPMSEnabled(); + } + else { + // screen saver can't be active if it doesn't exist + setXScreenSaverActive(false); + + // start watching for xscreensaver + watchForXScreenSaver(); + } +} + +bool +CXWindowsScreenSaver::isXScreenSaver(Window w) const +{ + // check for m_atomScreenSaverVersion string property + Atom type; + return (CXWindowsUtil::getWindowProperty(m_display, w, + m_atomScreenSaverVersion, + NULL, &type, NULL, False) && + type == XA_STRING); +} + +void +CXWindowsScreenSaver::setXScreenSaverActive(bool activated) +{ + if (m_xscreensaverActive != activated) { + LOG((CLOG_DEBUG "xscreensaver %s on window 0x%08x", activated ? "activated" : "deactivated", m_xscreensaver)); + m_xscreensaverActive = activated; + + // if screen saver was activated forcefully (i.e. against + // our will) then just accept it. don't try to keep it + // from activating since that'll just pop up the password + // dialog if locking is enabled. + m_suppressDisable = activated; + updateDisableTimer(); + + if (activated) { + m_eventQueue.addEvent(CEvent( + IPlatformScreen::getScreensaverActivatedEvent(), + m_eventTarget)); + } + else { + m_eventQueue.addEvent(CEvent( + IPlatformScreen::getScreensaverDeactivatedEvent(), + m_eventTarget)); + } + } +} + +void +CXWindowsScreenSaver::sendXScreenSaverCommand(Atom cmd, long arg1, long arg2) +{ + XEvent event; + event.xclient.type = ClientMessage; + event.xclient.display = m_display; + event.xclient.window = m_xscreensaverSink; + event.xclient.message_type = m_atomScreenSaver; + event.xclient.format = 32; + event.xclient.data.l[0] = static_cast(cmd); + event.xclient.data.l[1] = arg1; + event.xclient.data.l[2] = arg2; + event.xclient.data.l[3] = 0; + event.xclient.data.l[4] = 0; + + LOG((CLOG_DEBUG "send xscreensaver command: %d %d %d", (long)cmd, arg1, arg2)); + bool error = false; + { + CXWindowsUtil::CErrorLock lock(m_display, &error); + XSendEvent(m_display, m_xscreensaver, False, 0, &event); + } + if (error) { + findXScreenSaver(); + } +} + +void +CXWindowsScreenSaver::watchForXScreenSaver() +{ + // clear old watch list + clearWatchForXScreenSaver(); + + // add every child of the root to the list of windows to watch + Window root = DefaultRootWindow(m_display); + Window rw, pw, *cw; + unsigned int nc; + if (XQueryTree(m_display, root, &rw, &pw, &cw, &nc)) { + for (unsigned int i = 0; i < nc; ++i) { + addWatchXScreenSaver(cw[i]); + } + XFree(cw); + } + + // now check for xscreensaver window in case it set the property + // before we could request property change events. + if (findXScreenSaver()) { + // found it so clear out our watch list + clearWatchForXScreenSaver(); + } +} + +void +CXWindowsScreenSaver::clearWatchForXScreenSaver() +{ + // stop watching all windows + CXWindowsUtil::CErrorLock lock(m_display); + for (CWatchList::iterator index = m_watchWindows.begin(); + index != m_watchWindows.end(); ++index) { + XSelectInput(m_display, index->first, index->second); + } + m_watchWindows.clear(); +} + +void +CXWindowsScreenSaver::addWatchXScreenSaver(Window window) +{ + // get window attributes + bool error = false; + XWindowAttributes attr; + { + CXWindowsUtil::CErrorLock lock(m_display, &error); + XGetWindowAttributes(m_display, window, &attr); + } + + // if successful and window uses override_redirect (like xscreensaver + // does) then watch it for property changes. + if (!error && attr.override_redirect == True) { + error = false; + { + CXWindowsUtil::CErrorLock lock(m_display, &error); + XSelectInput(m_display, window, + attr.your_event_mask | PropertyChangeMask); + } + if (!error) { + // if successful then add the window to our list + m_watchWindows.insert(std::make_pair(window, attr.your_event_mask)); + } + } +} + +void +CXWindowsScreenSaver::updateDisableTimer() +{ + if (m_disabled && !m_suppressDisable && m_disableTimer == NULL) { + // 5 seconds should be plenty often to suppress the screen saver + m_disableTimer = m_eventQueue.newTimer(5.0, this); + } + else if ((!m_disabled || m_suppressDisable) && m_disableTimer != NULL) { + m_eventQueue.deleteTimer(m_disableTimer); + m_disableTimer = NULL; + } +} + +void +CXWindowsScreenSaver::handleDisableTimer(const CEvent&, void*) +{ + // send fake mouse motion directly to xscreensaver + if (m_xscreensaver != None) { + XEvent event; + event.xmotion.type = MotionNotify; + event.xmotion.display = m_display; + event.xmotion.window = m_xscreensaver; + event.xmotion.root = DefaultRootWindow(m_display); + event.xmotion.subwindow = None; + event.xmotion.time = CurrentTime; + event.xmotion.x = m_disablePos; + event.xmotion.y = 0; + event.xmotion.x_root = m_disablePos; + event.xmotion.y_root = 0; + event.xmotion.state = 0; + event.xmotion.is_hint = NotifyNormal; + event.xmotion.same_screen = True; + + CXWindowsUtil::CErrorLock lock(m_display); + XSendEvent(m_display, m_xscreensaver, False, 0, &event); + + m_disablePos = 20 - m_disablePos; + } +} + +void +CXWindowsScreenSaver::activateDPMS(bool activate) +{ +#if HAVE_X11_EXTENSIONS_DPMS_H + if (m_dpms) { + // DPMSForceLevel will generate a BadMatch if DPMS is disabled + CXWindowsUtil::CErrorLock lock(m_display); + DPMSForceLevel(m_display, activate ? DPMSModeStandby : DPMSModeOn); + } +#endif +} + +void +CXWindowsScreenSaver::enableDPMS(bool enable) +{ +#if HAVE_X11_EXTENSIONS_DPMS_H + if (m_dpms) { + if (enable) { + DPMSEnable(m_display); + } + else { + DPMSDisable(m_display); + } + } +#endif +} + +bool +CXWindowsScreenSaver::isDPMSEnabled() const +{ +#if HAVE_X11_EXTENSIONS_DPMS_H + if (m_dpms) { + CARD16 level; + BOOL state; + DPMSInfo(m_display, &level, &state); + return (state != False); + } + else { + return false; + } +#else + return false; +#endif +} + +bool +CXWindowsScreenSaver::isDPMSActivated() const +{ +#if HAVE_X11_EXTENSIONS_DPMS_H + if (m_dpms) { + CARD16 level; + BOOL state; + DPMSInfo(m_display, &level, &state); + return (level != DPMSModeOn); + } + else { + return false; + } +#else + return false; +#endif +} diff --git a/src/lib/platform/CXWindowsScreenSaver.h b/src/lib/platform/CXWindowsScreenSaver.h new file mode 100644 index 00000000..3326f243 --- /dev/null +++ b/src/lib/platform/CXWindowsScreenSaver.h @@ -0,0 +1,170 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSSCREENSAVER_H +#define CXWINDOWSSCREENSAVER_H + +#include "IScreenSaver.h" +#include "stdmap.h" +#if X_DISPLAY_MISSING +# error X11 is required to build synergy +#else +# include +#endif +#include "IEventQueue.h" + +class CEvent; +class CEventQueueTimer; + +//! X11 screen saver implementation +class CXWindowsScreenSaver : public IScreenSaver { +public: + CXWindowsScreenSaver(Display*, Window, void* eventTarget, IEventQueue& eventQueue); + virtual ~CXWindowsScreenSaver(); + + //! @name manipulators + //@{ + + //! Event filtering + /*! + Should be called for each system event before event translation and + dispatch. Returns true to skip translation and dispatch. + */ + bool handleXEvent(const XEvent*); + + //! Destroy without the display + /*! + Tells this object to delete itself without using the X11 display. + It may leak some resources as a result. + */ + void destroy(); + + //@} + + // IScreenSaver overrides + virtual void enable(); + virtual void disable(); + virtual void activate(); + virtual void deactivate(); + virtual bool isActive() const; + +private: + // find and set the running xscreensaver's window. returns true iff + // found. + bool findXScreenSaver(); + + // set the xscreensaver's window, updating the activation state flag + void setXScreenSaver(Window); + + // returns true if the window appears to be the xscreensaver window + bool isXScreenSaver(Window) const; + + // set xscreensaver's activation state flag. sends notification + // if the state has changed. + void setXScreenSaverActive(bool activated); + + // send a command to xscreensaver + void sendXScreenSaverCommand(Atom, long = 0, long = 0); + + // watch all windows that could potentially be the xscreensaver for + // the events that will confirm it. + void watchForXScreenSaver(); + + // stop watching all watched windows + void clearWatchForXScreenSaver(); + + // add window to the watch list + void addWatchXScreenSaver(Window window); + + // install/uninstall the job used to suppress the screensaver + void updateDisableTimer(); + + // called periodically to prevent the screen saver from starting + void handleDisableTimer(const CEvent&, void*); + + // force DPMS to activate or deactivate + void activateDPMS(bool activate); + + // enable/disable DPMS screen saver + void enableDPMS(bool); + + // check if DPMS is enabled + bool isDPMSEnabled() const; + + // check if DPMS is activate + bool isDPMSActivated() const; + +private: + typedef std::map CWatchList; + + // the X display + Display* m_display; + + // window to receive xscreensaver repsonses + Window m_xscreensaverSink; + + // the target for the events we generate + void* m_eventTarget; + + // xscreensaver's window + Window m_xscreensaver; + + // xscreensaver activation state + bool m_xscreensaverActive; + + // old event mask on root window + long m_rootEventMask; + + // potential xscreensaver windows being watched + CWatchList m_watchWindows; + + // atoms used to communicate with xscreensaver's window + Atom m_atomScreenSaver; + Atom m_atomScreenSaverVersion; + Atom m_atomScreenSaverActivate; + Atom m_atomScreenSaverDeactivate; + + // built-in screen saver settings + int m_timeout; + int m_interval; + int m_preferBlanking; + int m_allowExposures; + + // DPMS screen saver settings + bool m_dpms; + bool m_dpmsEnabled; + + // true iff the client wants the screen saver suppressed + bool m_disabled; + + // true iff we're ignoring m_disabled. this is true, for example, + // when the client has called activate() and so presumably wants + // to activate the screen saver even if disabled. + bool m_suppressDisable; + + // the disable timer (NULL if not installed) + CEventQueueTimer* m_disableTimer; + + // fake mouse motion position for suppressing the screen saver. + // xscreensaver since 2.21 requires the mouse to move more than 10 + // pixels to be considered significant. + SInt32 m_disablePos; + + IEventQueue& m_eventQueue; +}; + +#endif diff --git a/src/lib/platform/CXWindowsUtil.cpp b/src/lib/platform/CXWindowsUtil.cpp new file mode 100644 index 00000000..eb8bdcd1 --- /dev/null +++ b/src/lib/platform/CXWindowsUtil.cpp @@ -0,0 +1,1779 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CXWindowsUtil.h" +#include "KeyTypes.h" +#include "CThread.h" +#include "CLog.h" +#include "CStringUtil.h" +#include +#define XK_APL +#define XK_ARABIC +#define XK_ARMENIAN +#define XK_CAUCASUS +#define XK_CURRENCY +#define XK_CYRILLIC +#define XK_GEORGIAN +#define XK_GREEK +#define XK_HEBREW +#define XK_KATAKANA +#define XK_KOREAN +#define XK_LATIN1 +#define XK_LATIN2 +#define XK_LATIN3 +#define XK_LATIN4 +#define XK_LATIN8 +#define XK_LATIN9 +#define XK_MISCELLANY +#define XK_PUBLISHING +#define XK_SPECIAL +#define XK_TECHNICAL +#define XK_THAI +#define XK_VIETNAMESE +#define XK_XKB_KEYS +#include + +#if !defined(XK_OE) +#define XK_OE 0x13bc +#endif +#if !defined(XK_oe) +#define XK_oe 0x13bd +#endif +#if !defined(XK_Ydiaeresis) +#define XK_Ydiaeresis 0x13be +#endif + +/* + * This table maps keysym values into the corresponding ISO 10646 + * (UCS, Unicode) values. + * + * The array keysymtab[] contains pairs of X11 keysym values for graphical + * characters and the corresponding Unicode value. + * + * Author: Markus G. Kuhn , + * University of Cambridge, April 2001 + * + * Special thanks to Richard Verhoeven for preparing + * an initial draft of the mapping table. + * + * This software is in the public domain. Share and enjoy! + */ + +struct codepair { + KeySym keysym; + UInt32 ucs4; +} s_keymap[] = { +{ XK_Aogonek, 0x0104 }, /* LATIN CAPITAL LETTER A WITH OGONEK */ +{ XK_breve, 0x02d8 }, /* BREVE */ +{ XK_Lstroke, 0x0141 }, /* LATIN CAPITAL LETTER L WITH STROKE */ +{ XK_Lcaron, 0x013d }, /* LATIN CAPITAL LETTER L WITH CARON */ +{ XK_Sacute, 0x015a }, /* LATIN CAPITAL LETTER S WITH ACUTE */ +{ XK_Scaron, 0x0160 }, /* LATIN CAPITAL LETTER S WITH CARON */ +{ XK_Scedilla, 0x015e }, /* LATIN CAPITAL LETTER S WITH CEDILLA */ +{ XK_Tcaron, 0x0164 }, /* LATIN CAPITAL LETTER T WITH CARON */ +{ XK_Zacute, 0x0179 }, /* LATIN CAPITAL LETTER Z WITH ACUTE */ +{ XK_Zcaron, 0x017d }, /* LATIN CAPITAL LETTER Z WITH CARON */ +{ XK_Zabovedot, 0x017b }, /* LATIN CAPITAL LETTER Z WITH DOT ABOVE */ +{ XK_aogonek, 0x0105 }, /* LATIN SMALL LETTER A WITH OGONEK */ +{ XK_ogonek, 0x02db }, /* OGONEK */ +{ XK_lstroke, 0x0142 }, /* LATIN SMALL LETTER L WITH STROKE */ +{ XK_lcaron, 0x013e }, /* LATIN SMALL LETTER L WITH CARON */ +{ XK_sacute, 0x015b }, /* LATIN SMALL LETTER S WITH ACUTE */ +{ XK_caron, 0x02c7 }, /* CARON */ +{ XK_scaron, 0x0161 }, /* LATIN SMALL LETTER S WITH CARON */ +{ XK_scedilla, 0x015f }, /* LATIN SMALL LETTER S WITH CEDILLA */ +{ XK_tcaron, 0x0165 }, /* LATIN SMALL LETTER T WITH CARON */ +{ XK_zacute, 0x017a }, /* LATIN SMALL LETTER Z WITH ACUTE */ +{ XK_doubleacute, 0x02dd }, /* DOUBLE ACUTE ACCENT */ +{ XK_zcaron, 0x017e }, /* LATIN SMALL LETTER Z WITH CARON */ +{ XK_zabovedot, 0x017c }, /* LATIN SMALL LETTER Z WITH DOT ABOVE */ +{ XK_Racute, 0x0154 }, /* LATIN CAPITAL LETTER R WITH ACUTE */ +{ XK_Abreve, 0x0102 }, /* LATIN CAPITAL LETTER A WITH BREVE */ +{ XK_Lacute, 0x0139 }, /* LATIN CAPITAL LETTER L WITH ACUTE */ +{ XK_Cacute, 0x0106 }, /* LATIN CAPITAL LETTER C WITH ACUTE */ +{ XK_Ccaron, 0x010c }, /* LATIN CAPITAL LETTER C WITH CARON */ +{ XK_Eogonek, 0x0118 }, /* LATIN CAPITAL LETTER E WITH OGONEK */ +{ XK_Ecaron, 0x011a }, /* LATIN CAPITAL LETTER E WITH CARON */ +{ XK_Dcaron, 0x010e }, /* LATIN CAPITAL LETTER D WITH CARON */ +{ XK_Dstroke, 0x0110 }, /* LATIN CAPITAL LETTER D WITH STROKE */ +{ XK_Nacute, 0x0143 }, /* LATIN CAPITAL LETTER N WITH ACUTE */ +{ XK_Ncaron, 0x0147 }, /* LATIN CAPITAL LETTER N WITH CARON */ +{ XK_Odoubleacute, 0x0150 }, /* LATIN CAPITAL LETTER O WITH DOUBLE ACUTE */ +{ XK_Rcaron, 0x0158 }, /* LATIN CAPITAL LETTER R WITH CARON */ +{ XK_Uring, 0x016e }, /* LATIN CAPITAL LETTER U WITH RING ABOVE */ +{ XK_Udoubleacute, 0x0170 }, /* LATIN CAPITAL LETTER U WITH DOUBLE ACUTE */ +{ XK_Tcedilla, 0x0162 }, /* LATIN CAPITAL LETTER T WITH CEDILLA */ +{ XK_racute, 0x0155 }, /* LATIN SMALL LETTER R WITH ACUTE */ +{ XK_abreve, 0x0103 }, /* LATIN SMALL LETTER A WITH BREVE */ +{ XK_lacute, 0x013a }, /* LATIN SMALL LETTER L WITH ACUTE */ +{ XK_cacute, 0x0107 }, /* LATIN SMALL LETTER C WITH ACUTE */ +{ XK_ccaron, 0x010d }, /* LATIN SMALL LETTER C WITH CARON */ +{ XK_eogonek, 0x0119 }, /* LATIN SMALL LETTER E WITH OGONEK */ +{ XK_ecaron, 0x011b }, /* LATIN SMALL LETTER E WITH CARON */ +{ XK_dcaron, 0x010f }, /* LATIN SMALL LETTER D WITH CARON */ +{ XK_dstroke, 0x0111 }, /* LATIN SMALL LETTER D WITH STROKE */ +{ XK_nacute, 0x0144 }, /* LATIN SMALL LETTER N WITH ACUTE */ +{ XK_ncaron, 0x0148 }, /* LATIN SMALL LETTER N WITH CARON */ +{ XK_odoubleacute, 0x0151 }, /* LATIN SMALL LETTER O WITH DOUBLE ACUTE */ +{ XK_rcaron, 0x0159 }, /* LATIN SMALL LETTER R WITH CARON */ +{ XK_uring, 0x016f }, /* LATIN SMALL LETTER U WITH RING ABOVE */ +{ XK_udoubleacute, 0x0171 }, /* LATIN SMALL LETTER U WITH DOUBLE ACUTE */ +{ XK_tcedilla, 0x0163 }, /* LATIN SMALL LETTER T WITH CEDILLA */ +{ XK_abovedot, 0x02d9 }, /* DOT ABOVE */ +{ XK_Hstroke, 0x0126 }, /* LATIN CAPITAL LETTER H WITH STROKE */ +{ XK_Hcircumflex, 0x0124 }, /* LATIN CAPITAL LETTER H WITH CIRCUMFLEX */ +{ XK_Iabovedot, 0x0130 }, /* LATIN CAPITAL LETTER I WITH DOT ABOVE */ +{ XK_Gbreve, 0x011e }, /* LATIN CAPITAL LETTER G WITH BREVE */ +{ XK_Jcircumflex, 0x0134 }, /* LATIN CAPITAL LETTER J WITH CIRCUMFLEX */ +{ XK_hstroke, 0x0127 }, /* LATIN SMALL LETTER H WITH STROKE */ +{ XK_hcircumflex, 0x0125 }, /* LATIN SMALL LETTER H WITH CIRCUMFLEX */ +{ XK_idotless, 0x0131 }, /* LATIN SMALL LETTER DOTLESS I */ +{ XK_gbreve, 0x011f }, /* LATIN SMALL LETTER G WITH BREVE */ +{ XK_jcircumflex, 0x0135 }, /* LATIN SMALL LETTER J WITH CIRCUMFLEX */ +{ XK_Cabovedot, 0x010a }, /* LATIN CAPITAL LETTER C WITH DOT ABOVE */ +{ XK_Ccircumflex, 0x0108 }, /* LATIN CAPITAL LETTER C WITH CIRCUMFLEX */ +{ XK_Gabovedot, 0x0120 }, /* LATIN CAPITAL LETTER G WITH DOT ABOVE */ +{ XK_Gcircumflex, 0x011c }, /* LATIN CAPITAL LETTER G WITH CIRCUMFLEX */ +{ XK_Ubreve, 0x016c }, /* LATIN CAPITAL LETTER U WITH BREVE */ +{ XK_Scircumflex, 0x015c }, /* LATIN CAPITAL LETTER S WITH CIRCUMFLEX */ +{ XK_cabovedot, 0x010b }, /* LATIN SMALL LETTER C WITH DOT ABOVE */ +{ XK_ccircumflex, 0x0109 }, /* LATIN SMALL LETTER C WITH CIRCUMFLEX */ +{ XK_gabovedot, 0x0121 }, /* LATIN SMALL LETTER G WITH DOT ABOVE */ +{ XK_gcircumflex, 0x011d }, /* LATIN SMALL LETTER G WITH CIRCUMFLEX */ +{ XK_ubreve, 0x016d }, /* LATIN SMALL LETTER U WITH BREVE */ +{ XK_scircumflex, 0x015d }, /* LATIN SMALL LETTER S WITH CIRCUMFLEX */ +{ XK_kra, 0x0138 }, /* LATIN SMALL LETTER KRA */ +{ XK_Rcedilla, 0x0156 }, /* LATIN CAPITAL LETTER R WITH CEDILLA */ +{ XK_Itilde, 0x0128 }, /* LATIN CAPITAL LETTER I WITH TILDE */ +{ XK_Lcedilla, 0x013b }, /* LATIN CAPITAL LETTER L WITH CEDILLA */ +{ XK_Emacron, 0x0112 }, /* LATIN CAPITAL LETTER E WITH MACRON */ +{ XK_Gcedilla, 0x0122 }, /* LATIN CAPITAL LETTER G WITH CEDILLA */ +{ XK_Tslash, 0x0166 }, /* LATIN CAPITAL LETTER T WITH STROKE */ +{ XK_rcedilla, 0x0157 }, /* LATIN SMALL LETTER R WITH CEDILLA */ +{ XK_itilde, 0x0129 }, /* LATIN SMALL LETTER I WITH TILDE */ +{ XK_lcedilla, 0x013c }, /* LATIN SMALL LETTER L WITH CEDILLA */ +{ XK_emacron, 0x0113 }, /* LATIN SMALL LETTER E WITH MACRON */ +{ XK_gcedilla, 0x0123 }, /* LATIN SMALL LETTER G WITH CEDILLA */ +{ XK_tslash, 0x0167 }, /* LATIN SMALL LETTER T WITH STROKE */ +{ XK_ENG, 0x014a }, /* LATIN CAPITAL LETTER ENG */ +{ XK_eng, 0x014b }, /* LATIN SMALL LETTER ENG */ +{ XK_Amacron, 0x0100 }, /* LATIN CAPITAL LETTER A WITH MACRON */ +{ XK_Iogonek, 0x012e }, /* LATIN CAPITAL LETTER I WITH OGONEK */ +{ XK_Eabovedot, 0x0116 }, /* LATIN CAPITAL LETTER E WITH DOT ABOVE */ +{ XK_Imacron, 0x012a }, /* LATIN CAPITAL LETTER I WITH MACRON */ +{ XK_Ncedilla, 0x0145 }, /* LATIN CAPITAL LETTER N WITH CEDILLA */ +{ XK_Omacron, 0x014c }, /* LATIN CAPITAL LETTER O WITH MACRON */ +{ XK_Kcedilla, 0x0136 }, /* LATIN CAPITAL LETTER K WITH CEDILLA */ +{ XK_Uogonek, 0x0172 }, /* LATIN CAPITAL LETTER U WITH OGONEK */ +{ XK_Utilde, 0x0168 }, /* LATIN CAPITAL LETTER U WITH TILDE */ +{ XK_Umacron, 0x016a }, /* LATIN CAPITAL LETTER U WITH MACRON */ +{ XK_amacron, 0x0101 }, /* LATIN SMALL LETTER A WITH MACRON */ +{ XK_iogonek, 0x012f }, /* LATIN SMALL LETTER I WITH OGONEK */ +{ XK_eabovedot, 0x0117 }, /* LATIN SMALL LETTER E WITH DOT ABOVE */ +{ XK_imacron, 0x012b }, /* LATIN SMALL LETTER I WITH MACRON */ +{ XK_ncedilla, 0x0146 }, /* LATIN SMALL LETTER N WITH CEDILLA */ +{ XK_omacron, 0x014d }, /* LATIN SMALL LETTER O WITH MACRON */ +{ XK_kcedilla, 0x0137 }, /* LATIN SMALL LETTER K WITH CEDILLA */ +{ XK_uogonek, 0x0173 }, /* LATIN SMALL LETTER U WITH OGONEK */ +{ XK_utilde, 0x0169 }, /* LATIN SMALL LETTER U WITH TILDE */ +{ XK_umacron, 0x016b }, /* LATIN SMALL LETTER U WITH MACRON */ +#if defined(XK_Babovedot) +{ XK_Babovedot, 0x1e02 }, /* LATIN CAPITAL LETTER B WITH DOT ABOVE */ +{ XK_babovedot, 0x1e03 }, /* LATIN SMALL LETTER B WITH DOT ABOVE */ +{ XK_Dabovedot, 0x1e0a }, /* LATIN CAPITAL LETTER D WITH DOT ABOVE */ +{ XK_Wgrave, 0x1e80 }, /* LATIN CAPITAL LETTER W WITH GRAVE */ +{ XK_Wacute, 0x1e82 }, /* LATIN CAPITAL LETTER W WITH ACUTE */ +{ XK_dabovedot, 0x1e0b }, /* LATIN SMALL LETTER D WITH DOT ABOVE */ +{ XK_Ygrave, 0x1ef2 }, /* LATIN CAPITAL LETTER Y WITH GRAVE */ +{ XK_Fabovedot, 0x1e1e }, /* LATIN CAPITAL LETTER F WITH DOT ABOVE */ +{ XK_fabovedot, 0x1e1f }, /* LATIN SMALL LETTER F WITH DOT ABOVE */ +{ XK_Mabovedot, 0x1e40 }, /* LATIN CAPITAL LETTER M WITH DOT ABOVE */ +{ XK_mabovedot, 0x1e41 }, /* LATIN SMALL LETTER M WITH DOT ABOVE */ +{ XK_Pabovedot, 0x1e56 }, /* LATIN CAPITAL LETTER P WITH DOT ABOVE */ +{ XK_wgrave, 0x1e81 }, /* LATIN SMALL LETTER W WITH GRAVE */ +{ XK_pabovedot, 0x1e57 }, /* LATIN SMALL LETTER P WITH DOT ABOVE */ +{ XK_wacute, 0x1e83 }, /* LATIN SMALL LETTER W WITH ACUTE */ +{ XK_Sabovedot, 0x1e60 }, /* LATIN CAPITAL LETTER S WITH DOT ABOVE */ +{ XK_ygrave, 0x1ef3 }, /* LATIN SMALL LETTER Y WITH GRAVE */ +{ XK_Wdiaeresis, 0x1e84 }, /* LATIN CAPITAL LETTER W WITH DIAERESIS */ +{ XK_wdiaeresis, 0x1e85 }, /* LATIN SMALL LETTER W WITH DIAERESIS */ +{ XK_sabovedot, 0x1e61 }, /* LATIN SMALL LETTER S WITH DOT ABOVE */ +{ XK_Wcircumflex, 0x0174 }, /* LATIN CAPITAL LETTER W WITH CIRCUMFLEX */ +{ XK_Tabovedot, 0x1e6a }, /* LATIN CAPITAL LETTER T WITH DOT ABOVE */ +{ XK_Ycircumflex, 0x0176 }, /* LATIN CAPITAL LETTER Y WITH CIRCUMFLEX */ +{ XK_wcircumflex, 0x0175 }, /* LATIN SMALL LETTER W WITH CIRCUMFLEX */ +{ XK_tabovedot, 0x1e6b }, /* LATIN SMALL LETTER T WITH DOT ABOVE */ +{ XK_ycircumflex, 0x0177 }, /* LATIN SMALL LETTER Y WITH CIRCUMFLEX */ +#endif // defined(XK_Babovedot) +#if defined(XK_overline) +{ XK_overline, 0x203e }, /* OVERLINE */ +{ XK_kana_fullstop, 0x3002 }, /* IDEOGRAPHIC FULL STOP */ +{ XK_kana_openingbracket, 0x300c }, /* LEFT CORNER BRACKET */ +{ XK_kana_closingbracket, 0x300d }, /* RIGHT CORNER BRACKET */ +{ XK_kana_comma, 0x3001 }, /* IDEOGRAPHIC COMMA */ +{ XK_kana_conjunctive, 0x30fb }, /* KATAKANA MIDDLE DOT */ +{ XK_kana_WO, 0x30f2 }, /* KATAKANA LETTER WO */ +{ XK_kana_a, 0x30a1 }, /* KATAKANA LETTER SMALL A */ +{ XK_kana_i, 0x30a3 }, /* KATAKANA LETTER SMALL I */ +{ XK_kana_u, 0x30a5 }, /* KATAKANA LETTER SMALL U */ +{ XK_kana_e, 0x30a7 }, /* KATAKANA LETTER SMALL E */ +{ XK_kana_o, 0x30a9 }, /* KATAKANA LETTER SMALL O */ +{ XK_kana_ya, 0x30e3 }, /* KATAKANA LETTER SMALL YA */ +{ XK_kana_yu, 0x30e5 }, /* KATAKANA LETTER SMALL YU */ +{ XK_kana_yo, 0x30e7 }, /* KATAKANA LETTER SMALL YO */ +{ XK_kana_tsu, 0x30c3 }, /* KATAKANA LETTER SMALL TU */ +{ XK_prolongedsound, 0x30fc }, /* KATAKANA-HIRAGANA PROLONGED SOUND MARK */ +{ XK_kana_A, 0x30a2 }, /* KATAKANA LETTER A */ +{ XK_kana_I, 0x30a4 }, /* KATAKANA LETTER I */ +{ XK_kana_U, 0x30a6 }, /* KATAKANA LETTER U */ +{ XK_kana_E, 0x30a8 }, /* KATAKANA LETTER E */ +{ XK_kana_O, 0x30aa }, /* KATAKANA LETTER O */ +{ XK_kana_KA, 0x30ab }, /* KATAKANA LETTER KA */ +{ XK_kana_KI, 0x30ad }, /* KATAKANA LETTER KI */ +{ XK_kana_KU, 0x30af }, /* KATAKANA LETTER KU */ +{ XK_kana_KE, 0x30b1 }, /* KATAKANA LETTER KE */ +{ XK_kana_KO, 0x30b3 }, /* KATAKANA LETTER KO */ +{ XK_kana_SA, 0x30b5 }, /* KATAKANA LETTER SA */ +{ XK_kana_SHI, 0x30b7 }, /* KATAKANA LETTER SI */ +{ XK_kana_SU, 0x30b9 }, /* KATAKANA LETTER SU */ +{ XK_kana_SE, 0x30bb }, /* KATAKANA LETTER SE */ +{ XK_kana_SO, 0x30bd }, /* KATAKANA LETTER SO */ +{ XK_kana_TA, 0x30bf }, /* KATAKANA LETTER TA */ +{ XK_kana_CHI, 0x30c1 }, /* KATAKANA LETTER TI */ +{ XK_kana_TSU, 0x30c4 }, /* KATAKANA LETTER TU */ +{ XK_kana_TE, 0x30c6 }, /* KATAKANA LETTER TE */ +{ XK_kana_TO, 0x30c8 }, /* KATAKANA LETTER TO */ +{ XK_kana_NA, 0x30ca }, /* KATAKANA LETTER NA */ +{ XK_kana_NI, 0x30cb }, /* KATAKANA LETTER NI */ +{ XK_kana_NU, 0x30cc }, /* KATAKANA LETTER NU */ +{ XK_kana_NE, 0x30cd }, /* KATAKANA LETTER NE */ +{ XK_kana_NO, 0x30ce }, /* KATAKANA LETTER NO */ +{ XK_kana_HA, 0x30cf }, /* KATAKANA LETTER HA */ +{ XK_kana_HI, 0x30d2 }, /* KATAKANA LETTER HI */ +{ XK_kana_FU, 0x30d5 }, /* KATAKANA LETTER HU */ +{ XK_kana_HE, 0x30d8 }, /* KATAKANA LETTER HE */ +{ XK_kana_HO, 0x30db }, /* KATAKANA LETTER HO */ +{ XK_kana_MA, 0x30de }, /* KATAKANA LETTER MA */ +{ XK_kana_MI, 0x30df }, /* KATAKANA LETTER MI */ +{ XK_kana_MU, 0x30e0 }, /* KATAKANA LETTER MU */ +{ XK_kana_ME, 0x30e1 }, /* KATAKANA LETTER ME */ +{ XK_kana_MO, 0x30e2 }, /* KATAKANA LETTER MO */ +{ XK_kana_YA, 0x30e4 }, /* KATAKANA LETTER YA */ +{ XK_kana_YU, 0x30e6 }, /* KATAKANA LETTER YU */ +{ XK_kana_YO, 0x30e8 }, /* KATAKANA LETTER YO */ +{ XK_kana_RA, 0x30e9 }, /* KATAKANA LETTER RA */ +{ XK_kana_RI, 0x30ea }, /* KATAKANA LETTER RI */ +{ XK_kana_RU, 0x30eb }, /* KATAKANA LETTER RU */ +{ XK_kana_RE, 0x30ec }, /* KATAKANA LETTER RE */ +{ XK_kana_RO, 0x30ed }, /* KATAKANA LETTER RO */ +{ XK_kana_WA, 0x30ef }, /* KATAKANA LETTER WA */ +{ XK_kana_N, 0x30f3 }, /* KATAKANA LETTER N */ +{ XK_voicedsound, 0x309b }, /* KATAKANA-HIRAGANA VOICED SOUND MARK */ +{ XK_semivoicedsound, 0x309c }, /* KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK */ +#endif // defined(XK_overline) +#if defined(XK_Farsi_0) +{ XK_Farsi_0, 0x06f0 }, /* EXTENDED ARABIC-INDIC DIGIT 0 */ +{ XK_Farsi_1, 0x06f1 }, /* EXTENDED ARABIC-INDIC DIGIT 1 */ +{ XK_Farsi_2, 0x06f2 }, /* EXTENDED ARABIC-INDIC DIGIT 2 */ +{ XK_Farsi_3, 0x06f3 }, /* EXTENDED ARABIC-INDIC DIGIT 3 */ +{ XK_Farsi_4, 0x06f4 }, /* EXTENDED ARABIC-INDIC DIGIT 4 */ +{ XK_Farsi_5, 0x06f5 }, /* EXTENDED ARABIC-INDIC DIGIT 5 */ +{ XK_Farsi_6, 0x06f6 }, /* EXTENDED ARABIC-INDIC DIGIT 6 */ +{ XK_Farsi_7, 0x06f7 }, /* EXTENDED ARABIC-INDIC DIGIT 7 */ +{ XK_Farsi_8, 0x06f8 }, /* EXTENDED ARABIC-INDIC DIGIT 8 */ +{ XK_Farsi_9, 0x06f9 }, /* EXTENDED ARABIC-INDIC DIGIT 9 */ +{ XK_Arabic_percent, 0x066a }, /* ARABIC PERCENT */ +{ XK_Arabic_superscript_alef, 0x0670 }, /* ARABIC LETTER SUPERSCRIPT ALEF */ +{ XK_Arabic_tteh, 0x0679 }, /* ARABIC LETTER TTEH */ +{ XK_Arabic_peh, 0x067e }, /* ARABIC LETTER PEH */ +{ XK_Arabic_tcheh, 0x0686 }, /* ARABIC LETTER TCHEH */ +{ XK_Arabic_ddal, 0x0688 }, /* ARABIC LETTER DDAL */ +{ XK_Arabic_rreh, 0x0691 }, /* ARABIC LETTER RREH */ +{ XK_Arabic_comma, 0x060c }, /* ARABIC COMMA */ +{ XK_Arabic_fullstop, 0x06d4 }, /* ARABIC FULLSTOP */ +{ XK_Arabic_semicolon, 0x061b }, /* ARABIC SEMICOLON */ +{ XK_Arabic_0, 0x0660 }, /* ARABIC 0 */ +{ XK_Arabic_1, 0x0661 }, /* ARABIC 1 */ +{ XK_Arabic_2, 0x0662 }, /* ARABIC 2 */ +{ XK_Arabic_3, 0x0663 }, /* ARABIC 3 */ +{ XK_Arabic_4, 0x0664 }, /* ARABIC 4 */ +{ XK_Arabic_5, 0x0665 }, /* ARABIC 5 */ +{ XK_Arabic_6, 0x0666 }, /* ARABIC 6 */ +{ XK_Arabic_7, 0x0667 }, /* ARABIC 7 */ +{ XK_Arabic_8, 0x0668 }, /* ARABIC 8 */ +{ XK_Arabic_9, 0x0669 }, /* ARABIC 9 */ +{ XK_Arabic_question_mark, 0x061f }, /* ARABIC QUESTION MARK */ +{ XK_Arabic_hamza, 0x0621 }, /* ARABIC LETTER HAMZA */ +{ XK_Arabic_maddaonalef, 0x0622 }, /* ARABIC LETTER ALEF WITH MADDA ABOVE */ +{ XK_Arabic_hamzaonalef, 0x0623 }, /* ARABIC LETTER ALEF WITH HAMZA ABOVE */ +{ XK_Arabic_hamzaonwaw, 0x0624 }, /* ARABIC LETTER WAW WITH HAMZA ABOVE */ +{ XK_Arabic_hamzaunderalef, 0x0625 }, /* ARABIC LETTER ALEF WITH HAMZA BELOW */ +{ XK_Arabic_hamzaonyeh, 0x0626 }, /* ARABIC LETTER YEH WITH HAMZA ABOVE */ +{ XK_Arabic_alef, 0x0627 }, /* ARABIC LETTER ALEF */ +{ XK_Arabic_beh, 0x0628 }, /* ARABIC LETTER BEH */ +{ XK_Arabic_tehmarbuta, 0x0629 }, /* ARABIC LETTER TEH MARBUTA */ +{ XK_Arabic_teh, 0x062a }, /* ARABIC LETTER TEH */ +{ XK_Arabic_theh, 0x062b }, /* ARABIC LETTER THEH */ +{ XK_Arabic_jeem, 0x062c }, /* ARABIC LETTER JEEM */ +{ XK_Arabic_hah, 0x062d }, /* ARABIC LETTER HAH */ +{ XK_Arabic_khah, 0x062e }, /* ARABIC LETTER KHAH */ +{ XK_Arabic_dal, 0x062f }, /* ARABIC LETTER DAL */ +{ XK_Arabic_thal, 0x0630 }, /* ARABIC LETTER THAL */ +{ XK_Arabic_ra, 0x0631 }, /* ARABIC LETTER REH */ +{ XK_Arabic_zain, 0x0632 }, /* ARABIC LETTER ZAIN */ +{ XK_Arabic_seen, 0x0633 }, /* ARABIC LETTER SEEN */ +{ XK_Arabic_sheen, 0x0634 }, /* ARABIC LETTER SHEEN */ +{ XK_Arabic_sad, 0x0635 }, /* ARABIC LETTER SAD */ +{ XK_Arabic_dad, 0x0636 }, /* ARABIC LETTER DAD */ +{ XK_Arabic_tah, 0x0637 }, /* ARABIC LETTER TAH */ +{ XK_Arabic_zah, 0x0638 }, /* ARABIC LETTER ZAH */ +{ XK_Arabic_ain, 0x0639 }, /* ARABIC LETTER AIN */ +{ XK_Arabic_ghain, 0x063a }, /* ARABIC LETTER GHAIN */ +{ XK_Arabic_tatweel, 0x0640 }, /* ARABIC TATWEEL */ +{ XK_Arabic_feh, 0x0641 }, /* ARABIC LETTER FEH */ +{ XK_Arabic_qaf, 0x0642 }, /* ARABIC LETTER QAF */ +{ XK_Arabic_kaf, 0x0643 }, /* ARABIC LETTER KAF */ +{ XK_Arabic_lam, 0x0644 }, /* ARABIC LETTER LAM */ +{ XK_Arabic_meem, 0x0645 }, /* ARABIC LETTER MEEM */ +{ XK_Arabic_noon, 0x0646 }, /* ARABIC LETTER NOON */ +{ XK_Arabic_ha, 0x0647 }, /* ARABIC LETTER HEH */ +{ XK_Arabic_waw, 0x0648 }, /* ARABIC LETTER WAW */ +{ XK_Arabic_alefmaksura, 0x0649 }, /* ARABIC LETTER ALEF MAKSURA */ +{ XK_Arabic_yeh, 0x064a }, /* ARABIC LETTER YEH */ +{ XK_Arabic_fathatan, 0x064b }, /* ARABIC FATHATAN */ +{ XK_Arabic_dammatan, 0x064c }, /* ARABIC DAMMATAN */ +{ XK_Arabic_kasratan, 0x064d }, /* ARABIC KASRATAN */ +{ XK_Arabic_fatha, 0x064e }, /* ARABIC FATHA */ +{ XK_Arabic_damma, 0x064f }, /* ARABIC DAMMA */ +{ XK_Arabic_kasra, 0x0650 }, /* ARABIC KASRA */ +{ XK_Arabic_shadda, 0x0651 }, /* ARABIC SHADDA */ +{ XK_Arabic_sukun, 0x0652 }, /* ARABIC SUKUN */ +{ XK_Arabic_madda_above, 0x0653 }, /* ARABIC MADDA ABOVE */ +{ XK_Arabic_hamza_above, 0x0654 }, /* ARABIC HAMZA ABOVE */ +{ XK_Arabic_hamza_below, 0x0655 }, /* ARABIC HAMZA BELOW */ +{ XK_Arabic_jeh, 0x0698 }, /* ARABIC LETTER JEH */ +{ XK_Arabic_veh, 0x06a4 }, /* ARABIC LETTER VEH */ +{ XK_Arabic_keheh, 0x06a9 }, /* ARABIC LETTER KEHEH */ +{ XK_Arabic_gaf, 0x06af }, /* ARABIC LETTER GAF */ +{ XK_Arabic_noon_ghunna, 0x06ba }, /* ARABIC LETTER NOON GHUNNA */ +{ XK_Arabic_heh_doachashmee, 0x06be }, /* ARABIC LETTER HEH DOACHASHMEE */ +{ XK_Arabic_farsi_yeh, 0x06cc }, /* ARABIC LETTER FARSI YEH */ +{ XK_Arabic_yeh_baree, 0x06d2 }, /* ARABIC LETTER YEH BAREE */ +{ XK_Arabic_heh_goal, 0x06c1 }, /* ARABIC LETTER HEH GOAL */ +#endif // defined(XK_Farsi_0) +#if defined(XK_Serbian_dje) +{ XK_Serbian_dje, 0x0452 }, /* CYRILLIC SMALL LETTER DJE */ +{ XK_Macedonia_gje, 0x0453 }, /* CYRILLIC SMALL LETTER GJE */ +{ XK_Cyrillic_io, 0x0451 }, /* CYRILLIC SMALL LETTER IO */ +{ XK_Ukrainian_ie, 0x0454 }, /* CYRILLIC SMALL LETTER UKRAINIAN IE */ +{ XK_Macedonia_dse, 0x0455 }, /* CYRILLIC SMALL LETTER DZE */ +{ XK_Ukrainian_i, 0x0456 }, /* CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I */ +{ XK_Ukrainian_yi, 0x0457 }, /* CYRILLIC SMALL LETTER YI */ +{ XK_Cyrillic_je, 0x0458 }, /* CYRILLIC SMALL LETTER JE */ +{ XK_Cyrillic_lje, 0x0459 }, /* CYRILLIC SMALL LETTER LJE */ +{ XK_Cyrillic_nje, 0x045a }, /* CYRILLIC SMALL LETTER NJE */ +{ XK_Serbian_tshe, 0x045b }, /* CYRILLIC SMALL LETTER TSHE */ +{ XK_Macedonia_kje, 0x045c }, /* CYRILLIC SMALL LETTER KJE */ +#if defined(XK_Ukrainian_ghe_with_upturn) +{ XK_Ukrainian_ghe_with_upturn, 0x0491 }, /* CYRILLIC SMALL LETTER GHE WITH UPTURN */ +#endif +{ XK_Byelorussian_shortu, 0x045e }, /* CYRILLIC SMALL LETTER SHORT U */ +{ XK_Cyrillic_dzhe, 0x045f }, /* CYRILLIC SMALL LETTER DZHE */ +{ XK_numerosign, 0x2116 }, /* NUMERO SIGN */ +{ XK_Serbian_DJE, 0x0402 }, /* CYRILLIC CAPITAL LETTER DJE */ +{ XK_Macedonia_GJE, 0x0403 }, /* CYRILLIC CAPITAL LETTER GJE */ +{ XK_Cyrillic_IO, 0x0401 }, /* CYRILLIC CAPITAL LETTER IO */ +{ XK_Ukrainian_IE, 0x0404 }, /* CYRILLIC CAPITAL LETTER UKRAINIAN IE */ +{ XK_Macedonia_DSE, 0x0405 }, /* CYRILLIC CAPITAL LETTER DZE */ +{ XK_Ukrainian_I, 0x0406 }, /* CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I */ +{ XK_Ukrainian_YI, 0x0407 }, /* CYRILLIC CAPITAL LETTER YI */ +{ XK_Cyrillic_JE, 0x0408 }, /* CYRILLIC CAPITAL LETTER JE */ +{ XK_Cyrillic_LJE, 0x0409 }, /* CYRILLIC CAPITAL LETTER LJE */ +{ XK_Cyrillic_NJE, 0x040a }, /* CYRILLIC CAPITAL LETTER NJE */ +{ XK_Serbian_TSHE, 0x040b }, /* CYRILLIC CAPITAL LETTER TSHE */ +{ XK_Macedonia_KJE, 0x040c }, /* CYRILLIC CAPITAL LETTER KJE */ +#if defined(XK_Ukrainian_GHE_WITH_UPTURN) +{ XK_Ukrainian_GHE_WITH_UPTURN, 0x0490 }, /* CYRILLIC CAPITAL LETTER GHE WITH UPTURN */ +#endif +{ XK_Byelorussian_SHORTU, 0x040e }, /* CYRILLIC CAPITAL LETTER SHORT U */ +{ XK_Cyrillic_DZHE, 0x040f }, /* CYRILLIC CAPITAL LETTER DZHE */ +{ XK_Cyrillic_yu, 0x044e }, /* CYRILLIC SMALL LETTER YU */ +{ XK_Cyrillic_a, 0x0430 }, /* CYRILLIC SMALL LETTER A */ +{ XK_Cyrillic_be, 0x0431 }, /* CYRILLIC SMALL LETTER BE */ +{ XK_Cyrillic_tse, 0x0446 }, /* CYRILLIC SMALL LETTER TSE */ +{ XK_Cyrillic_de, 0x0434 }, /* CYRILLIC SMALL LETTER DE */ +{ XK_Cyrillic_ie, 0x0435 }, /* CYRILLIC SMALL LETTER IE */ +{ XK_Cyrillic_ef, 0x0444 }, /* CYRILLIC SMALL LETTER EF */ +{ XK_Cyrillic_ghe, 0x0433 }, /* CYRILLIC SMALL LETTER GHE */ +{ XK_Cyrillic_ha, 0x0445 }, /* CYRILLIC SMALL LETTER HA */ +{ XK_Cyrillic_i, 0x0438 }, /* CYRILLIC SMALL LETTER I */ +{ XK_Cyrillic_shorti, 0x0439 }, /* CYRILLIC SMALL LETTER SHORT I */ +{ XK_Cyrillic_ka, 0x043a }, /* CYRILLIC SMALL LETTER KA */ +{ XK_Cyrillic_el, 0x043b }, /* CYRILLIC SMALL LETTER EL */ +{ XK_Cyrillic_em, 0x043c }, /* CYRILLIC SMALL LETTER EM */ +{ XK_Cyrillic_en, 0x043d }, /* CYRILLIC SMALL LETTER EN */ +{ XK_Cyrillic_o, 0x043e }, /* CYRILLIC SMALL LETTER O */ +{ XK_Cyrillic_pe, 0x043f }, /* CYRILLIC SMALL LETTER PE */ +{ XK_Cyrillic_ya, 0x044f }, /* CYRILLIC SMALL LETTER YA */ +{ XK_Cyrillic_er, 0x0440 }, /* CYRILLIC SMALL LETTER ER */ +{ XK_Cyrillic_es, 0x0441 }, /* CYRILLIC SMALL LETTER ES */ +{ XK_Cyrillic_te, 0x0442 }, /* CYRILLIC SMALL LETTER TE */ +{ XK_Cyrillic_u, 0x0443 }, /* CYRILLIC SMALL LETTER U */ +{ XK_Cyrillic_zhe, 0x0436 }, /* CYRILLIC SMALL LETTER ZHE */ +{ XK_Cyrillic_ve, 0x0432 }, /* CYRILLIC SMALL LETTER VE */ +{ XK_Cyrillic_softsign, 0x044c }, /* CYRILLIC SMALL LETTER SOFT SIGN */ +{ XK_Cyrillic_yeru, 0x044b }, /* CYRILLIC SMALL LETTER YERU */ +{ XK_Cyrillic_ze, 0x0437 }, /* CYRILLIC SMALL LETTER ZE */ +{ XK_Cyrillic_sha, 0x0448 }, /* CYRILLIC SMALL LETTER SHA */ +{ XK_Cyrillic_e, 0x044d }, /* CYRILLIC SMALL LETTER E */ +{ XK_Cyrillic_shcha, 0x0449 }, /* CYRILLIC SMALL LETTER SHCHA */ +{ XK_Cyrillic_che, 0x0447 }, /* CYRILLIC SMALL LETTER CHE */ +{ XK_Cyrillic_hardsign, 0x044a }, /* CYRILLIC SMALL LETTER HARD SIGN */ +{ XK_Cyrillic_YU, 0x042e }, /* CYRILLIC CAPITAL LETTER YU */ +{ XK_Cyrillic_A, 0x0410 }, /* CYRILLIC CAPITAL LETTER A */ +{ XK_Cyrillic_BE, 0x0411 }, /* CYRILLIC CAPITAL LETTER BE */ +{ XK_Cyrillic_TSE, 0x0426 }, /* CYRILLIC CAPITAL LETTER TSE */ +{ XK_Cyrillic_DE, 0x0414 }, /* CYRILLIC CAPITAL LETTER DE */ +{ XK_Cyrillic_IE, 0x0415 }, /* CYRILLIC CAPITAL LETTER IE */ +{ XK_Cyrillic_EF, 0x0424 }, /* CYRILLIC CAPITAL LETTER EF */ +{ XK_Cyrillic_GHE, 0x0413 }, /* CYRILLIC CAPITAL LETTER GHE */ +{ XK_Cyrillic_HA, 0x0425 }, /* CYRILLIC CAPITAL LETTER HA */ +{ XK_Cyrillic_I, 0x0418 }, /* CYRILLIC CAPITAL LETTER I */ +{ XK_Cyrillic_SHORTI, 0x0419 }, /* CYRILLIC CAPITAL LETTER SHORT I */ +{ XK_Cyrillic_KA, 0x041a }, /* CYRILLIC CAPITAL LETTER KA */ +{ XK_Cyrillic_EL, 0x041b }, /* CYRILLIC CAPITAL LETTER EL */ +{ XK_Cyrillic_EM, 0x041c }, /* CYRILLIC CAPITAL LETTER EM */ +{ XK_Cyrillic_EN, 0x041d }, /* CYRILLIC CAPITAL LETTER EN */ +{ XK_Cyrillic_O, 0x041e }, /* CYRILLIC CAPITAL LETTER O */ +{ XK_Cyrillic_PE, 0x041f }, /* CYRILLIC CAPITAL LETTER PE */ +{ XK_Cyrillic_YA, 0x042f }, /* CYRILLIC CAPITAL LETTER YA */ +{ XK_Cyrillic_ER, 0x0420 }, /* CYRILLIC CAPITAL LETTER ER */ +{ XK_Cyrillic_ES, 0x0421 }, /* CYRILLIC CAPITAL LETTER ES */ +{ XK_Cyrillic_TE, 0x0422 }, /* CYRILLIC CAPITAL LETTER TE */ +{ XK_Cyrillic_U, 0x0423 }, /* CYRILLIC CAPITAL LETTER U */ +{ XK_Cyrillic_ZHE, 0x0416 }, /* CYRILLIC CAPITAL LETTER ZHE */ +{ XK_Cyrillic_VE, 0x0412 }, /* CYRILLIC CAPITAL LETTER VE */ +{ XK_Cyrillic_SOFTSIGN, 0x042c }, /* CYRILLIC CAPITAL LETTER SOFT SIGN */ +{ XK_Cyrillic_YERU, 0x042b }, /* CYRILLIC CAPITAL LETTER YERU */ +{ XK_Cyrillic_ZE, 0x0417 }, /* CYRILLIC CAPITAL LETTER ZE */ +{ XK_Cyrillic_SHA, 0x0428 }, /* CYRILLIC CAPITAL LETTER SHA */ +{ XK_Cyrillic_E, 0x042d }, /* CYRILLIC CAPITAL LETTER E */ +{ XK_Cyrillic_SHCHA, 0x0429 }, /* CYRILLIC CAPITAL LETTER SHCHA */ +{ XK_Cyrillic_CHE, 0x0427 }, /* CYRILLIC CAPITAL LETTER CHE */ +{ XK_Cyrillic_HARDSIGN, 0x042a }, /* CYRILLIC CAPITAL LETTER HARD SIGN */ +#endif // defined(XK_Serbian_dje) +#if defined(XK_Greek_ALPHAaccent) +{ XK_Greek_ALPHAaccent, 0x0386 }, /* GREEK CAPITAL LETTER ALPHA WITH TONOS */ +{ XK_Greek_EPSILONaccent, 0x0388 }, /* GREEK CAPITAL LETTER EPSILON WITH TONOS */ +{ XK_Greek_ETAaccent, 0x0389 }, /* GREEK CAPITAL LETTER ETA WITH TONOS */ +{ XK_Greek_IOTAaccent, 0x038a }, /* GREEK CAPITAL LETTER IOTA WITH TONOS */ +{ XK_Greek_IOTAdiaeresis, 0x03aa }, /* GREEK CAPITAL LETTER IOTA WITH DIALYTIKA */ +{ XK_Greek_OMICRONaccent, 0x038c }, /* GREEK CAPITAL LETTER OMICRON WITH TONOS */ +{ XK_Greek_UPSILONaccent, 0x038e }, /* GREEK CAPITAL LETTER UPSILON WITH TONOS */ +{ XK_Greek_UPSILONdieresis, 0x03ab }, /* GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA */ +{ XK_Greek_OMEGAaccent, 0x038f }, /* GREEK CAPITAL LETTER OMEGA WITH TONOS */ +{ XK_Greek_accentdieresis, 0x0385 }, /* GREEK DIALYTIKA TONOS */ +{ XK_Greek_horizbar, 0x2015 }, /* HORIZONTAL BAR */ +{ XK_Greek_alphaaccent, 0x03ac }, /* GREEK SMALL LETTER ALPHA WITH TONOS */ +{ XK_Greek_epsilonaccent, 0x03ad }, /* GREEK SMALL LETTER EPSILON WITH TONOS */ +{ XK_Greek_etaaccent, 0x03ae }, /* GREEK SMALL LETTER ETA WITH TONOS */ +{ XK_Greek_iotaaccent, 0x03af }, /* GREEK SMALL LETTER IOTA WITH TONOS */ +{ XK_Greek_iotadieresis, 0x03ca }, /* GREEK SMALL LETTER IOTA WITH DIALYTIKA */ +{ XK_Greek_iotaaccentdieresis, 0x0390 }, /* GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS */ +{ XK_Greek_omicronaccent, 0x03cc }, /* GREEK SMALL LETTER OMICRON WITH TONOS */ +{ XK_Greek_upsilonaccent, 0x03cd }, /* GREEK SMALL LETTER UPSILON WITH TONOS */ +{ XK_Greek_upsilondieresis, 0x03cb }, /* GREEK SMALL LETTER UPSILON WITH DIALYTIKA */ +{ XK_Greek_upsilonaccentdieresis, 0x03b0 }, /* GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS */ +{ XK_Greek_omegaaccent, 0x03ce }, /* GREEK SMALL LETTER OMEGA WITH TONOS */ +{ XK_Greek_ALPHA, 0x0391 }, /* GREEK CAPITAL LETTER ALPHA */ +{ XK_Greek_BETA, 0x0392 }, /* GREEK CAPITAL LETTER BETA */ +{ XK_Greek_GAMMA, 0x0393 }, /* GREEK CAPITAL LETTER GAMMA */ +{ XK_Greek_DELTA, 0x0394 }, /* GREEK CAPITAL LETTER DELTA */ +{ XK_Greek_EPSILON, 0x0395 }, /* GREEK CAPITAL LETTER EPSILON */ +{ XK_Greek_ZETA, 0x0396 }, /* GREEK CAPITAL LETTER ZETA */ +{ XK_Greek_ETA, 0x0397 }, /* GREEK CAPITAL LETTER ETA */ +{ XK_Greek_THETA, 0x0398 }, /* GREEK CAPITAL LETTER THETA */ +{ XK_Greek_IOTA, 0x0399 }, /* GREEK CAPITAL LETTER IOTA */ +{ XK_Greek_KAPPA, 0x039a }, /* GREEK CAPITAL LETTER KAPPA */ +{ XK_Greek_LAMBDA, 0x039b }, /* GREEK CAPITAL LETTER LAMDA */ +{ XK_Greek_MU, 0x039c }, /* GREEK CAPITAL LETTER MU */ +{ XK_Greek_NU, 0x039d }, /* GREEK CAPITAL LETTER NU */ +{ XK_Greek_XI, 0x039e }, /* GREEK CAPITAL LETTER XI */ +{ XK_Greek_OMICRON, 0x039f }, /* GREEK CAPITAL LETTER OMICRON */ +{ XK_Greek_PI, 0x03a0 }, /* GREEK CAPITAL LETTER PI */ +{ XK_Greek_RHO, 0x03a1 }, /* GREEK CAPITAL LETTER RHO */ +{ XK_Greek_SIGMA, 0x03a3 }, /* GREEK CAPITAL LETTER SIGMA */ +{ XK_Greek_TAU, 0x03a4 }, /* GREEK CAPITAL LETTER TAU */ +{ XK_Greek_UPSILON, 0x03a5 }, /* GREEK CAPITAL LETTER UPSILON */ +{ XK_Greek_PHI, 0x03a6 }, /* GREEK CAPITAL LETTER PHI */ +{ XK_Greek_CHI, 0x03a7 }, /* GREEK CAPITAL LETTER CHI */ +{ XK_Greek_PSI, 0x03a8 }, /* GREEK CAPITAL LETTER PSI */ +{ XK_Greek_OMEGA, 0x03a9 }, /* GREEK CAPITAL LETTER OMEGA */ +{ XK_Greek_alpha, 0x03b1 }, /* GREEK SMALL LETTER ALPHA */ +{ XK_Greek_beta, 0x03b2 }, /* GREEK SMALL LETTER BETA */ +{ XK_Greek_gamma, 0x03b3 }, /* GREEK SMALL LETTER GAMMA */ +{ XK_Greek_delta, 0x03b4 }, /* GREEK SMALL LETTER DELTA */ +{ XK_Greek_epsilon, 0x03b5 }, /* GREEK SMALL LETTER EPSILON */ +{ XK_Greek_zeta, 0x03b6 }, /* GREEK SMALL LETTER ZETA */ +{ XK_Greek_eta, 0x03b7 }, /* GREEK SMALL LETTER ETA */ +{ XK_Greek_theta, 0x03b8 }, /* GREEK SMALL LETTER THETA */ +{ XK_Greek_iota, 0x03b9 }, /* GREEK SMALL LETTER IOTA */ +{ XK_Greek_kappa, 0x03ba }, /* GREEK SMALL LETTER KAPPA */ +{ XK_Greek_lambda, 0x03bb }, /* GREEK SMALL LETTER LAMDA */ +{ XK_Greek_mu, 0x03bc }, /* GREEK SMALL LETTER MU */ +{ XK_Greek_nu, 0x03bd }, /* GREEK SMALL LETTER NU */ +{ XK_Greek_xi, 0x03be }, /* GREEK SMALL LETTER XI */ +{ XK_Greek_omicron, 0x03bf }, /* GREEK SMALL LETTER OMICRON */ +{ XK_Greek_pi, 0x03c0 }, /* GREEK SMALL LETTER PI */ +{ XK_Greek_rho, 0x03c1 }, /* GREEK SMALL LETTER RHO */ +{ XK_Greek_sigma, 0x03c3 }, /* GREEK SMALL LETTER SIGMA */ +{ XK_Greek_finalsmallsigma, 0x03c2 }, /* GREEK SMALL LETTER FINAL SIGMA */ +{ XK_Greek_tau, 0x03c4 }, /* GREEK SMALL LETTER TAU */ +{ XK_Greek_upsilon, 0x03c5 }, /* GREEK SMALL LETTER UPSILON */ +{ XK_Greek_phi, 0x03c6 }, /* GREEK SMALL LETTER PHI */ +{ XK_Greek_chi, 0x03c7 }, /* GREEK SMALL LETTER CHI */ +{ XK_Greek_psi, 0x03c8 }, /* GREEK SMALL LETTER PSI */ +{ XK_Greek_omega, 0x03c9 }, /* GREEK SMALL LETTER OMEGA */ +#endif // defined(XK_Greek_ALPHAaccent) +{ XK_leftradical, 0x23b7 }, /* ??? */ +{ XK_topleftradical, 0x250c }, /* BOX DRAWINGS LIGHT DOWN AND RIGHT */ +{ XK_horizconnector, 0x2500 }, /* BOX DRAWINGS LIGHT HORIZONTAL */ +{ XK_topintegral, 0x2320 }, /* TOP HALF INTEGRAL */ +{ XK_botintegral, 0x2321 }, /* BOTTOM HALF INTEGRAL */ +{ XK_vertconnector, 0x2502 }, /* BOX DRAWINGS LIGHT VERTICAL */ +{ XK_topleftsqbracket, 0x23a1 }, /* ??? */ +{ XK_botleftsqbracket, 0x23a3 }, /* ??? */ +{ XK_toprightsqbracket, 0x23a4 }, /* ??? */ +{ XK_botrightsqbracket, 0x23a6 }, /* ??? */ +{ XK_topleftparens, 0x239b }, /* ??? */ +{ XK_botleftparens, 0x239d }, /* ??? */ +{ XK_toprightparens, 0x239e }, /* ??? */ +{ XK_botrightparens, 0x23a0 }, /* ??? */ +{ XK_leftmiddlecurlybrace, 0x23a8 }, /* ??? */ +{ XK_rightmiddlecurlybrace, 0x23ac }, /* ??? */ +{ XK_lessthanequal, 0x2264 }, /* LESS-THAN OR EQUAL TO */ +{ XK_notequal, 0x2260 }, /* NOT EQUAL TO */ +{ XK_greaterthanequal, 0x2265 }, /* GREATER-THAN OR EQUAL TO */ +{ XK_integral, 0x222b }, /* INTEGRAL */ +{ XK_therefore, 0x2234 }, /* THEREFORE */ +{ XK_variation, 0x221d }, /* PROPORTIONAL TO */ +{ XK_infinity, 0x221e }, /* INFINITY */ +{ XK_nabla, 0x2207 }, /* NABLA */ +{ XK_approximate, 0x223c }, /* TILDE OPERATOR */ +{ XK_similarequal, 0x2243 }, /* ASYMPTOTICALLY EQUAL TO */ +{ XK_ifonlyif, 0x21d4 }, /* LEFT RIGHT DOUBLE ARROW */ +{ XK_implies, 0x21d2 }, /* RIGHTWARDS DOUBLE ARROW */ +{ XK_identical, 0x2261 }, /* IDENTICAL TO */ +{ XK_radical, 0x221a }, /* SQUARE ROOT */ +{ XK_includedin, 0x2282 }, /* SUBSET OF */ +{ XK_includes, 0x2283 }, /* SUPERSET OF */ +{ XK_intersection, 0x2229 }, /* INTERSECTION */ +{ XK_union, 0x222a }, /* UNION */ +{ XK_logicaland, 0x2227 }, /* LOGICAL AND */ +{ XK_logicalor, 0x2228 }, /* LOGICAL OR */ +{ XK_partialderivative, 0x2202 }, /* PARTIAL DIFFERENTIAL */ +{ XK_function, 0x0192 }, /* LATIN SMALL LETTER F WITH HOOK */ +{ XK_leftarrow, 0x2190 }, /* LEFTWARDS ARROW */ +{ XK_uparrow, 0x2191 }, /* UPWARDS ARROW */ +{ XK_rightarrow, 0x2192 }, /* RIGHTWARDS ARROW */ +{ XK_downarrow, 0x2193 }, /* DOWNWARDS ARROW */ +/*{ XK_blank, ??? }, */ +{ XK_soliddiamond, 0x25c6 }, /* BLACK DIAMOND */ +{ XK_checkerboard, 0x2592 }, /* MEDIUM SHADE */ +{ XK_ht, 0x2409 }, /* SYMBOL FOR HORIZONTAL TABULATION */ +{ XK_ff, 0x240c }, /* SYMBOL FOR FORM FEED */ +{ XK_cr, 0x240d }, /* SYMBOL FOR CARRIAGE RETURN */ +{ XK_lf, 0x240a }, /* SYMBOL FOR LINE FEED */ +{ XK_nl, 0x2424 }, /* SYMBOL FOR NEWLINE */ +{ XK_vt, 0x240b }, /* SYMBOL FOR VERTICAL TABULATION */ +{ XK_lowrightcorner, 0x2518 }, /* BOX DRAWINGS LIGHT UP AND LEFT */ +{ XK_uprightcorner, 0x2510 }, /* BOX DRAWINGS LIGHT DOWN AND LEFT */ +{ XK_upleftcorner, 0x250c }, /* BOX DRAWINGS LIGHT DOWN AND RIGHT */ +{ XK_lowleftcorner, 0x2514 }, /* BOX DRAWINGS LIGHT UP AND RIGHT */ +{ XK_crossinglines, 0x253c }, /* BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL */ +{ XK_horizlinescan1, 0x23ba }, /* HORIZONTAL SCAN LINE-1 (Unicode 3.2 draft) */ +{ XK_horizlinescan3, 0x23bb }, /* HORIZONTAL SCAN LINE-3 (Unicode 3.2 draft) */ +{ XK_horizlinescan5, 0x2500 }, /* BOX DRAWINGS LIGHT HORIZONTAL */ +{ XK_horizlinescan7, 0x23bc }, /* HORIZONTAL SCAN LINE-7 (Unicode 3.2 draft) */ +{ XK_horizlinescan9, 0x23bd }, /* HORIZONTAL SCAN LINE-9 (Unicode 3.2 draft) */ +{ XK_leftt, 0x251c }, /* BOX DRAWINGS LIGHT VERTICAL AND RIGHT */ +{ XK_rightt, 0x2524 }, /* BOX DRAWINGS LIGHT VERTICAL AND LEFT */ +{ XK_bott, 0x2534 }, /* BOX DRAWINGS LIGHT UP AND HORIZONTAL */ +{ XK_topt, 0x252c }, /* BOX DRAWINGS LIGHT DOWN AND HORIZONTAL */ +{ XK_vertbar, 0x2502 }, /* BOX DRAWINGS LIGHT VERTICAL */ +{ XK_emspace, 0x2003 }, /* EM SPACE */ +{ XK_enspace, 0x2002 }, /* EN SPACE */ +{ XK_em3space, 0x2004 }, /* THREE-PER-EM SPACE */ +{ XK_em4space, 0x2005 }, /* FOUR-PER-EM SPACE */ +{ XK_digitspace, 0x2007 }, /* FIGURE SPACE */ +{ XK_punctspace, 0x2008 }, /* PUNCTUATION SPACE */ +{ XK_thinspace, 0x2009 }, /* THIN SPACE */ +{ XK_hairspace, 0x200a }, /* HAIR SPACE */ +{ XK_emdash, 0x2014 }, /* EM DASH */ +{ XK_endash, 0x2013 }, /* EN DASH */ +/*{ XK_signifblank, ??? }, */ +{ XK_ellipsis, 0x2026 }, /* HORIZONTAL ELLIPSIS */ +{ XK_doubbaselinedot, 0x2025 }, /* TWO DOT LEADER */ +{ XK_onethird, 0x2153 }, /* VULGAR FRACTION ONE THIRD */ +{ XK_twothirds, 0x2154 }, /* VULGAR FRACTION TWO THIRDS */ +{ XK_onefifth, 0x2155 }, /* VULGAR FRACTION ONE FIFTH */ +{ XK_twofifths, 0x2156 }, /* VULGAR FRACTION TWO FIFTHS */ +{ XK_threefifths, 0x2157 }, /* VULGAR FRACTION THREE FIFTHS */ +{ XK_fourfifths, 0x2158 }, /* VULGAR FRACTION FOUR FIFTHS */ +{ XK_onesixth, 0x2159 }, /* VULGAR FRACTION ONE SIXTH */ +{ XK_fivesixths, 0x215a }, /* VULGAR FRACTION FIVE SIXTHS */ +{ XK_careof, 0x2105 }, /* CARE OF */ +{ XK_figdash, 0x2012 }, /* FIGURE DASH */ +{ XK_leftanglebracket, 0x2329 }, /* LEFT-POINTING ANGLE BRACKET */ +/*{ XK_decimalpoint, ??? }, */ +{ XK_rightanglebracket, 0x232a }, /* RIGHT-POINTING ANGLE BRACKET */ +/*{ XK_marker, ??? }, */ +{ XK_oneeighth, 0x215b }, /* VULGAR FRACTION ONE EIGHTH */ +{ XK_threeeighths, 0x215c }, /* VULGAR FRACTION THREE EIGHTHS */ +{ XK_fiveeighths, 0x215d }, /* VULGAR FRACTION FIVE EIGHTHS */ +{ XK_seveneighths, 0x215e }, /* VULGAR FRACTION SEVEN EIGHTHS */ +{ XK_trademark, 0x2122 }, /* TRADE MARK SIGN */ +{ XK_signaturemark, 0x2613 }, /* SALTIRE */ +/*{ XK_trademarkincircle, ??? }, */ +{ XK_leftopentriangle, 0x25c1 }, /* WHITE LEFT-POINTING TRIANGLE */ +{ XK_rightopentriangle, 0x25b7 }, /* WHITE RIGHT-POINTING TRIANGLE */ +{ XK_emopencircle, 0x25cb }, /* WHITE CIRCLE */ +{ XK_emopenrectangle, 0x25af }, /* WHITE VERTICAL RECTANGLE */ +{ XK_leftsinglequotemark, 0x2018 }, /* LEFT SINGLE QUOTATION MARK */ +{ XK_rightsinglequotemark, 0x2019 }, /* RIGHT SINGLE QUOTATION MARK */ +{ XK_leftdoublequotemark, 0x201c }, /* LEFT DOUBLE QUOTATION MARK */ +{ XK_rightdoublequotemark, 0x201d }, /* RIGHT DOUBLE QUOTATION MARK */ +{ XK_prescription, 0x211e }, /* PRESCRIPTION TAKE */ +{ XK_minutes, 0x2032 }, /* PRIME */ +{ XK_seconds, 0x2033 }, /* DOUBLE PRIME */ +{ XK_latincross, 0x271d }, /* LATIN CROSS */ +/*{ XK_hexagram, ??? }, */ +{ XK_filledrectbullet, 0x25ac }, /* BLACK RECTANGLE */ +{ XK_filledlefttribullet, 0x25c0 }, /* BLACK LEFT-POINTING TRIANGLE */ +{ XK_filledrighttribullet, 0x25b6 }, /* BLACK RIGHT-POINTING TRIANGLE */ +{ XK_emfilledcircle, 0x25cf }, /* BLACK CIRCLE */ +{ XK_emfilledrect, 0x25ae }, /* BLACK VERTICAL RECTANGLE */ +{ XK_enopencircbullet, 0x25e6 }, /* WHITE BULLET */ +{ XK_enopensquarebullet, 0x25ab }, /* WHITE SMALL SQUARE */ +{ XK_openrectbullet, 0x25ad }, /* WHITE RECTANGLE */ +{ XK_opentribulletup, 0x25b3 }, /* WHITE UP-POINTING TRIANGLE */ +{ XK_opentribulletdown, 0x25bd }, /* WHITE DOWN-POINTING TRIANGLE */ +{ XK_openstar, 0x2606 }, /* WHITE STAR */ +{ XK_enfilledcircbullet, 0x2022 }, /* BULLET */ +{ XK_enfilledsqbullet, 0x25aa }, /* BLACK SMALL SQUARE */ +{ XK_filledtribulletup, 0x25b2 }, /* BLACK UP-POINTING TRIANGLE */ +{ XK_filledtribulletdown, 0x25bc }, /* BLACK DOWN-POINTING TRIANGLE */ +{ XK_leftpointer, 0x261c }, /* WHITE LEFT POINTING INDEX */ +{ XK_rightpointer, 0x261e }, /* WHITE RIGHT POINTING INDEX */ +{ XK_club, 0x2663 }, /* BLACK CLUB SUIT */ +{ XK_diamond, 0x2666 }, /* BLACK DIAMOND SUIT */ +{ XK_heart, 0x2665 }, /* BLACK HEART SUIT */ +{ XK_maltesecross, 0x2720 }, /* MALTESE CROSS */ +{ XK_dagger, 0x2020 }, /* DAGGER */ +{ XK_doubledagger, 0x2021 }, /* DOUBLE DAGGER */ +{ XK_checkmark, 0x2713 }, /* CHECK MARK */ +{ XK_ballotcross, 0x2717 }, /* BALLOT X */ +{ XK_musicalsharp, 0x266f }, /* MUSIC SHARP SIGN */ +{ XK_musicalflat, 0x266d }, /* MUSIC FLAT SIGN */ +{ XK_malesymbol, 0x2642 }, /* MALE SIGN */ +{ XK_femalesymbol, 0x2640 }, /* FEMALE SIGN */ +{ XK_telephone, 0x260e }, /* BLACK TELEPHONE */ +{ XK_telephonerecorder, 0x2315 }, /* TELEPHONE RECORDER */ +{ XK_phonographcopyright, 0x2117 }, /* SOUND RECORDING COPYRIGHT */ +{ XK_caret, 0x2038 }, /* CARET */ +{ XK_singlelowquotemark, 0x201a }, /* SINGLE LOW-9 QUOTATION MARK */ +{ XK_doublelowquotemark, 0x201e }, /* DOUBLE LOW-9 QUOTATION MARK */ +/*{ XK_cursor, ??? }, */ +{ XK_leftcaret, 0x003c }, /* LESS-THAN SIGN */ +{ XK_rightcaret, 0x003e }, /* GREATER-THAN SIGN */ +{ XK_downcaret, 0x2228 }, /* LOGICAL OR */ +{ XK_upcaret, 0x2227 }, /* LOGICAL AND */ +{ XK_overbar, 0x00af }, /* MACRON */ +{ XK_downtack, 0x22a5 }, /* UP TACK */ +{ XK_upshoe, 0x2229 }, /* INTERSECTION */ +{ XK_downstile, 0x230a }, /* LEFT FLOOR */ +{ XK_underbar, 0x005f }, /* LOW LINE */ +{ XK_jot, 0x2218 }, /* RING OPERATOR */ +{ XK_quad, 0x2395 }, /* APL FUNCTIONAL SYMBOL QUAD */ +{ XK_uptack, 0x22a4 }, /* DOWN TACK */ +{ XK_circle, 0x25cb }, /* WHITE CIRCLE */ +{ XK_upstile, 0x2308 }, /* LEFT CEILING */ +{ XK_downshoe, 0x222a }, /* UNION */ +{ XK_rightshoe, 0x2283 }, /* SUPERSET OF */ +{ XK_leftshoe, 0x2282 }, /* SUBSET OF */ +{ XK_lefttack, 0x22a2 }, /* RIGHT TACK */ +{ XK_righttack, 0x22a3 }, /* LEFT TACK */ +#if defined(XK_hebrew_doublelowline) +{ XK_hebrew_doublelowline, 0x2017 }, /* DOUBLE LOW LINE */ +{ XK_hebrew_aleph, 0x05d0 }, /* HEBREW LETTER ALEF */ +{ XK_hebrew_bet, 0x05d1 }, /* HEBREW LETTER BET */ +{ XK_hebrew_gimel, 0x05d2 }, /* HEBREW LETTER GIMEL */ +{ XK_hebrew_dalet, 0x05d3 }, /* HEBREW LETTER DALET */ +{ XK_hebrew_he, 0x05d4 }, /* HEBREW LETTER HE */ +{ XK_hebrew_waw, 0x05d5 }, /* HEBREW LETTER VAV */ +{ XK_hebrew_zain, 0x05d6 }, /* HEBREW LETTER ZAYIN */ +{ XK_hebrew_chet, 0x05d7 }, /* HEBREW LETTER HET */ +{ XK_hebrew_tet, 0x05d8 }, /* HEBREW LETTER TET */ +{ XK_hebrew_yod, 0x05d9 }, /* HEBREW LETTER YOD */ +{ XK_hebrew_finalkaph, 0x05da }, /* HEBREW LETTER FINAL KAF */ +{ XK_hebrew_kaph, 0x05db }, /* HEBREW LETTER KAF */ +{ XK_hebrew_lamed, 0x05dc }, /* HEBREW LETTER LAMED */ +{ XK_hebrew_finalmem, 0x05dd }, /* HEBREW LETTER FINAL MEM */ +{ XK_hebrew_mem, 0x05de }, /* HEBREW LETTER MEM */ +{ XK_hebrew_finalnun, 0x05df }, /* HEBREW LETTER FINAL NUN */ +{ XK_hebrew_nun, 0x05e0 }, /* HEBREW LETTER NUN */ +{ XK_hebrew_samech, 0x05e1 }, /* HEBREW LETTER SAMEKH */ +{ XK_hebrew_ayin, 0x05e2 }, /* HEBREW LETTER AYIN */ +{ XK_hebrew_finalpe, 0x05e3 }, /* HEBREW LETTER FINAL PE */ +{ XK_hebrew_pe, 0x05e4 }, /* HEBREW LETTER PE */ +{ XK_hebrew_finalzade, 0x05e5 }, /* HEBREW LETTER FINAL TSADI */ +{ XK_hebrew_zade, 0x05e6 }, /* HEBREW LETTER TSADI */ +{ XK_hebrew_qoph, 0x05e7 }, /* HEBREW LETTER QOF */ +{ XK_hebrew_resh, 0x05e8 }, /* HEBREW LETTER RESH */ +{ XK_hebrew_shin, 0x05e9 }, /* HEBREW LETTER SHIN */ +{ XK_hebrew_taw, 0x05ea }, /* HEBREW LETTER TAV */ +#endif // defined(XK_hebrew_doublelowline) +#if defined(XK_Thai_kokai) +{ XK_Thai_kokai, 0x0e01 }, /* THAI CHARACTER KO KAI */ +{ XK_Thai_khokhai, 0x0e02 }, /* THAI CHARACTER KHO KHAI */ +{ XK_Thai_khokhuat, 0x0e03 }, /* THAI CHARACTER KHO KHUAT */ +{ XK_Thai_khokhwai, 0x0e04 }, /* THAI CHARACTER KHO KHWAI */ +{ XK_Thai_khokhon, 0x0e05 }, /* THAI CHARACTER KHO KHON */ +{ XK_Thai_khorakhang, 0x0e06 }, /* THAI CHARACTER KHO RAKHANG */ +{ XK_Thai_ngongu, 0x0e07 }, /* THAI CHARACTER NGO NGU */ +{ XK_Thai_chochan, 0x0e08 }, /* THAI CHARACTER CHO CHAN */ +{ XK_Thai_choching, 0x0e09 }, /* THAI CHARACTER CHO CHING */ +{ XK_Thai_chochang, 0x0e0a }, /* THAI CHARACTER CHO CHANG */ +{ XK_Thai_soso, 0x0e0b }, /* THAI CHARACTER SO SO */ +{ XK_Thai_chochoe, 0x0e0c }, /* THAI CHARACTER CHO CHOE */ +{ XK_Thai_yoying, 0x0e0d }, /* THAI CHARACTER YO YING */ +{ XK_Thai_dochada, 0x0e0e }, /* THAI CHARACTER DO CHADA */ +{ XK_Thai_topatak, 0x0e0f }, /* THAI CHARACTER TO PATAK */ +{ XK_Thai_thothan, 0x0e10 }, /* THAI CHARACTER THO THAN */ +{ XK_Thai_thonangmontho, 0x0e11 }, /* THAI CHARACTER THO NANGMONTHO */ +{ XK_Thai_thophuthao, 0x0e12 }, /* THAI CHARACTER THO PHUTHAO */ +{ XK_Thai_nonen, 0x0e13 }, /* THAI CHARACTER NO NEN */ +{ XK_Thai_dodek, 0x0e14 }, /* THAI CHARACTER DO DEK */ +{ XK_Thai_totao, 0x0e15 }, /* THAI CHARACTER TO TAO */ +{ XK_Thai_thothung, 0x0e16 }, /* THAI CHARACTER THO THUNG */ +{ XK_Thai_thothahan, 0x0e17 }, /* THAI CHARACTER THO THAHAN */ +{ XK_Thai_thothong, 0x0e18 }, /* THAI CHARACTER THO THONG */ +{ XK_Thai_nonu, 0x0e19 }, /* THAI CHARACTER NO NU */ +{ XK_Thai_bobaimai, 0x0e1a }, /* THAI CHARACTER BO BAIMAI */ +{ XK_Thai_popla, 0x0e1b }, /* THAI CHARACTER PO PLA */ +{ XK_Thai_phophung, 0x0e1c }, /* THAI CHARACTER PHO PHUNG */ +{ XK_Thai_fofa, 0x0e1d }, /* THAI CHARACTER FO FA */ +{ XK_Thai_phophan, 0x0e1e }, /* THAI CHARACTER PHO PHAN */ +{ XK_Thai_fofan, 0x0e1f }, /* THAI CHARACTER FO FAN */ +{ XK_Thai_phosamphao, 0x0e20 }, /* THAI CHARACTER PHO SAMPHAO */ +{ XK_Thai_moma, 0x0e21 }, /* THAI CHARACTER MO MA */ +{ XK_Thai_yoyak, 0x0e22 }, /* THAI CHARACTER YO YAK */ +{ XK_Thai_rorua, 0x0e23 }, /* THAI CHARACTER RO RUA */ +{ XK_Thai_ru, 0x0e24 }, /* THAI CHARACTER RU */ +{ XK_Thai_loling, 0x0e25 }, /* THAI CHARACTER LO LING */ +{ XK_Thai_lu, 0x0e26 }, /* THAI CHARACTER LU */ +{ XK_Thai_wowaen, 0x0e27 }, /* THAI CHARACTER WO WAEN */ +{ XK_Thai_sosala, 0x0e28 }, /* THAI CHARACTER SO SALA */ +{ XK_Thai_sorusi, 0x0e29 }, /* THAI CHARACTER SO RUSI */ +{ XK_Thai_sosua, 0x0e2a }, /* THAI CHARACTER SO SUA */ +{ XK_Thai_hohip, 0x0e2b }, /* THAI CHARACTER HO HIP */ +{ XK_Thai_lochula, 0x0e2c }, /* THAI CHARACTER LO CHULA */ +{ XK_Thai_oang, 0x0e2d }, /* THAI CHARACTER O ANG */ +{ XK_Thai_honokhuk, 0x0e2e }, /* THAI CHARACTER HO NOKHUK */ +{ XK_Thai_paiyannoi, 0x0e2f }, /* THAI CHARACTER PAIYANNOI */ +{ XK_Thai_saraa, 0x0e30 }, /* THAI CHARACTER SARA A */ +{ XK_Thai_maihanakat, 0x0e31 }, /* THAI CHARACTER MAI HAN-AKAT */ +{ XK_Thai_saraaa, 0x0e32 }, /* THAI CHARACTER SARA AA */ +{ XK_Thai_saraam, 0x0e33 }, /* THAI CHARACTER SARA AM */ +{ XK_Thai_sarai, 0x0e34 }, /* THAI CHARACTER SARA I */ +{ XK_Thai_saraii, 0x0e35 }, /* THAI CHARACTER SARA II */ +{ XK_Thai_saraue, 0x0e36 }, /* THAI CHARACTER SARA UE */ +{ XK_Thai_sarauee, 0x0e37 }, /* THAI CHARACTER SARA UEE */ +{ XK_Thai_sarau, 0x0e38 }, /* THAI CHARACTER SARA U */ +{ XK_Thai_sarauu, 0x0e39 }, /* THAI CHARACTER SARA UU */ +{ XK_Thai_phinthu, 0x0e3a }, /* THAI CHARACTER PHINTHU */ +/*{ XK_Thai_maihanakat_maitho, ??? }, */ +{ XK_Thai_baht, 0x0e3f }, /* THAI CURRENCY SYMBOL BAHT */ +{ XK_Thai_sarae, 0x0e40 }, /* THAI CHARACTER SARA E */ +{ XK_Thai_saraae, 0x0e41 }, /* THAI CHARACTER SARA AE */ +{ XK_Thai_sarao, 0x0e42 }, /* THAI CHARACTER SARA O */ +{ XK_Thai_saraaimaimuan, 0x0e43 }, /* THAI CHARACTER SARA AI MAIMUAN */ +{ XK_Thai_saraaimaimalai, 0x0e44 }, /* THAI CHARACTER SARA AI MAIMALAI */ +{ XK_Thai_lakkhangyao, 0x0e45 }, /* THAI CHARACTER LAKKHANGYAO */ +{ XK_Thai_maiyamok, 0x0e46 }, /* THAI CHARACTER MAIYAMOK */ +{ XK_Thai_maitaikhu, 0x0e47 }, /* THAI CHARACTER MAITAIKHU */ +{ XK_Thai_maiek, 0x0e48 }, /* THAI CHARACTER MAI EK */ +{ XK_Thai_maitho, 0x0e49 }, /* THAI CHARACTER MAI THO */ +{ XK_Thai_maitri, 0x0e4a }, /* THAI CHARACTER MAI TRI */ +{ XK_Thai_maichattawa, 0x0e4b }, /* THAI CHARACTER MAI CHATTAWA */ +{ XK_Thai_thanthakhat, 0x0e4c }, /* THAI CHARACTER THANTHAKHAT */ +{ XK_Thai_nikhahit, 0x0e4d }, /* THAI CHARACTER NIKHAHIT */ +{ XK_Thai_leksun, 0x0e50 }, /* THAI DIGIT ZERO */ +{ XK_Thai_leknung, 0x0e51 }, /* THAI DIGIT ONE */ +{ XK_Thai_leksong, 0x0e52 }, /* THAI DIGIT TWO */ +{ XK_Thai_leksam, 0x0e53 }, /* THAI DIGIT THREE */ +{ XK_Thai_leksi, 0x0e54 }, /* THAI DIGIT FOUR */ +{ XK_Thai_lekha, 0x0e55 }, /* THAI DIGIT FIVE */ +{ XK_Thai_lekhok, 0x0e56 }, /* THAI DIGIT SIX */ +{ XK_Thai_lekchet, 0x0e57 }, /* THAI DIGIT SEVEN */ +{ XK_Thai_lekpaet, 0x0e58 }, /* THAI DIGIT EIGHT */ +{ XK_Thai_lekkao, 0x0e59 }, /* THAI DIGIT NINE */ +#endif // defined(XK_Thai_kokai) +#if defined(XK_Hangul_Kiyeog) +{ XK_Hangul_Kiyeog, 0x3131 }, /* HANGUL LETTER KIYEOK */ +{ XK_Hangul_SsangKiyeog, 0x3132 }, /* HANGUL LETTER SSANGKIYEOK */ +{ XK_Hangul_KiyeogSios, 0x3133 }, /* HANGUL LETTER KIYEOK-SIOS */ +{ XK_Hangul_Nieun, 0x3134 }, /* HANGUL LETTER NIEUN */ +{ XK_Hangul_NieunJieuj, 0x3135 }, /* HANGUL LETTER NIEUN-CIEUC */ +{ XK_Hangul_NieunHieuh, 0x3136 }, /* HANGUL LETTER NIEUN-HIEUH */ +{ XK_Hangul_Dikeud, 0x3137 }, /* HANGUL LETTER TIKEUT */ +{ XK_Hangul_SsangDikeud, 0x3138 }, /* HANGUL LETTER SSANGTIKEUT */ +{ XK_Hangul_Rieul, 0x3139 }, /* HANGUL LETTER RIEUL */ +{ XK_Hangul_RieulKiyeog, 0x313a }, /* HANGUL LETTER RIEUL-KIYEOK */ +{ XK_Hangul_RieulMieum, 0x313b }, /* HANGUL LETTER RIEUL-MIEUM */ +{ XK_Hangul_RieulPieub, 0x313c }, /* HANGUL LETTER RIEUL-PIEUP */ +{ XK_Hangul_RieulSios, 0x313d }, /* HANGUL LETTER RIEUL-SIOS */ +{ XK_Hangul_RieulTieut, 0x313e }, /* HANGUL LETTER RIEUL-THIEUTH */ +{ XK_Hangul_RieulPhieuf, 0x313f }, /* HANGUL LETTER RIEUL-PHIEUPH */ +{ XK_Hangul_RieulHieuh, 0x3140 }, /* HANGUL LETTER RIEUL-HIEUH */ +{ XK_Hangul_Mieum, 0x3141 }, /* HANGUL LETTER MIEUM */ +{ XK_Hangul_Pieub, 0x3142 }, /* HANGUL LETTER PIEUP */ +{ XK_Hangul_SsangPieub, 0x3143 }, /* HANGUL LETTER SSANGPIEUP */ +{ XK_Hangul_PieubSios, 0x3144 }, /* HANGUL LETTER PIEUP-SIOS */ +{ XK_Hangul_Sios, 0x3145 }, /* HANGUL LETTER SIOS */ +{ XK_Hangul_SsangSios, 0x3146 }, /* HANGUL LETTER SSANGSIOS */ +{ XK_Hangul_Ieung, 0x3147 }, /* HANGUL LETTER IEUNG */ +{ XK_Hangul_Jieuj, 0x3148 }, /* HANGUL LETTER CIEUC */ +{ XK_Hangul_SsangJieuj, 0x3149 }, /* HANGUL LETTER SSANGCIEUC */ +{ XK_Hangul_Cieuc, 0x314a }, /* HANGUL LETTER CHIEUCH */ +{ XK_Hangul_Khieuq, 0x314b }, /* HANGUL LETTER KHIEUKH */ +{ XK_Hangul_Tieut, 0x314c }, /* HANGUL LETTER THIEUTH */ +{ XK_Hangul_Phieuf, 0x314d }, /* HANGUL LETTER PHIEUPH */ +{ XK_Hangul_Hieuh, 0x314e }, /* HANGUL LETTER HIEUH */ +{ XK_Hangul_A, 0x314f }, /* HANGUL LETTER A */ +{ XK_Hangul_AE, 0x3150 }, /* HANGUL LETTER AE */ +{ XK_Hangul_YA, 0x3151 }, /* HANGUL LETTER YA */ +{ XK_Hangul_YAE, 0x3152 }, /* HANGUL LETTER YAE */ +{ XK_Hangul_EO, 0x3153 }, /* HANGUL LETTER EO */ +{ XK_Hangul_E, 0x3154 }, /* HANGUL LETTER E */ +{ XK_Hangul_YEO, 0x3155 }, /* HANGUL LETTER YEO */ +{ XK_Hangul_YE, 0x3156 }, /* HANGUL LETTER YE */ +{ XK_Hangul_O, 0x3157 }, /* HANGUL LETTER O */ +{ XK_Hangul_WA, 0x3158 }, /* HANGUL LETTER WA */ +{ XK_Hangul_WAE, 0x3159 }, /* HANGUL LETTER WAE */ +{ XK_Hangul_OE, 0x315a }, /* HANGUL LETTER OE */ +{ XK_Hangul_YO, 0x315b }, /* HANGUL LETTER YO */ +{ XK_Hangul_U, 0x315c }, /* HANGUL LETTER U */ +{ XK_Hangul_WEO, 0x315d }, /* HANGUL LETTER WEO */ +{ XK_Hangul_WE, 0x315e }, /* HANGUL LETTER WE */ +{ XK_Hangul_WI, 0x315f }, /* HANGUL LETTER WI */ +{ XK_Hangul_YU, 0x3160 }, /* HANGUL LETTER YU */ +{ XK_Hangul_EU, 0x3161 }, /* HANGUL LETTER EU */ +{ XK_Hangul_YI, 0x3162 }, /* HANGUL LETTER YI */ +{ XK_Hangul_I, 0x3163 }, /* HANGUL LETTER I */ +{ XK_Hangul_J_Kiyeog, 0x11a8 }, /* HANGUL JONGSEONG KIYEOK */ +{ XK_Hangul_J_SsangKiyeog, 0x11a9 }, /* HANGUL JONGSEONG SSANGKIYEOK */ +{ XK_Hangul_J_KiyeogSios, 0x11aa }, /* HANGUL JONGSEONG KIYEOK-SIOS */ +{ XK_Hangul_J_Nieun, 0x11ab }, /* HANGUL JONGSEONG NIEUN */ +{ XK_Hangul_J_NieunJieuj, 0x11ac }, /* HANGUL JONGSEONG NIEUN-CIEUC */ +{ XK_Hangul_J_NieunHieuh, 0x11ad }, /* HANGUL JONGSEONG NIEUN-HIEUH */ +{ XK_Hangul_J_Dikeud, 0x11ae }, /* HANGUL JONGSEONG TIKEUT */ +{ XK_Hangul_J_Rieul, 0x11af }, /* HANGUL JONGSEONG RIEUL */ +{ XK_Hangul_J_RieulKiyeog, 0x11b0 }, /* HANGUL JONGSEONG RIEUL-KIYEOK */ +{ XK_Hangul_J_RieulMieum, 0x11b1 }, /* HANGUL JONGSEONG RIEUL-MIEUM */ +{ XK_Hangul_J_RieulPieub, 0x11b2 }, /* HANGUL JONGSEONG RIEUL-PIEUP */ +{ XK_Hangul_J_RieulSios, 0x11b3 }, /* HANGUL JONGSEONG RIEUL-SIOS */ +{ XK_Hangul_J_RieulTieut, 0x11b4 }, /* HANGUL JONGSEONG RIEUL-THIEUTH */ +{ XK_Hangul_J_RieulPhieuf, 0x11b5 }, /* HANGUL JONGSEONG RIEUL-PHIEUPH */ +{ XK_Hangul_J_RieulHieuh, 0x11b6 }, /* HANGUL JONGSEONG RIEUL-HIEUH */ +{ XK_Hangul_J_Mieum, 0x11b7 }, /* HANGUL JONGSEONG MIEUM */ +{ XK_Hangul_J_Pieub, 0x11b8 }, /* HANGUL JONGSEONG PIEUP */ +{ XK_Hangul_J_PieubSios, 0x11b9 }, /* HANGUL JONGSEONG PIEUP-SIOS */ +{ XK_Hangul_J_Sios, 0x11ba }, /* HANGUL JONGSEONG SIOS */ +{ XK_Hangul_J_SsangSios, 0x11bb }, /* HANGUL JONGSEONG SSANGSIOS */ +{ XK_Hangul_J_Ieung, 0x11bc }, /* HANGUL JONGSEONG IEUNG */ +{ XK_Hangul_J_Jieuj, 0x11bd }, /* HANGUL JONGSEONG CIEUC */ +{ XK_Hangul_J_Cieuc, 0x11be }, /* HANGUL JONGSEONG CHIEUCH */ +{ XK_Hangul_J_Khieuq, 0x11bf }, /* HANGUL JONGSEONG KHIEUKH */ +{ XK_Hangul_J_Tieut, 0x11c0 }, /* HANGUL JONGSEONG THIEUTH */ +{ XK_Hangul_J_Phieuf, 0x11c1 }, /* HANGUL JONGSEONG PHIEUPH */ +{ XK_Hangul_J_Hieuh, 0x11c2 }, /* HANGUL JONGSEONG HIEUH */ +{ XK_Hangul_RieulYeorinHieuh, 0x316d }, /* HANGUL LETTER RIEUL-YEORINHIEUH */ +{ XK_Hangul_SunkyeongeumMieum, 0x3171 }, /* HANGUL LETTER KAPYEOUNMIEUM */ +{ XK_Hangul_SunkyeongeumPieub, 0x3178 }, /* HANGUL LETTER KAPYEOUNPIEUP */ +{ XK_Hangul_PanSios, 0x317f }, /* HANGUL LETTER PANSIOS */ +{ XK_Hangul_KkogjiDalrinIeung, 0x3181 }, /* HANGUL LETTER YESIEUNG */ +{ XK_Hangul_SunkyeongeumPhieuf, 0x3184 }, /* HANGUL LETTER KAPYEOUNPHIEUPH */ +{ XK_Hangul_YeorinHieuh, 0x3186 }, /* HANGUL LETTER YEORINHIEUH */ +{ XK_Hangul_AraeA, 0x318d }, /* HANGUL LETTER ARAEA */ +{ XK_Hangul_AraeAE, 0x318e }, /* HANGUL LETTER ARAEAE */ +{ XK_Hangul_J_PanSios, 0x11eb }, /* HANGUL JONGSEONG PANSIOS */ +{ XK_Hangul_J_KkogjiDalrinIeung, 0x11f0 }, /* HANGUL JONGSEONG YESIEUNG */ +{ XK_Hangul_J_YeorinHieuh, 0x11f9 }, /* HANGUL JONGSEONG YEORINHIEUH */ +{ XK_Korean_Won, 0x20a9 }, /* WON SIGN */ +#endif // defined(XK_Hangul_Kiyeog) +{ XK_OE, 0x0152 }, /* LATIN CAPITAL LIGATURE OE */ +{ XK_oe, 0x0153 }, /* LATIN SMALL LIGATURE OE */ +{ XK_Ydiaeresis, 0x0178 }, /* LATIN CAPITAL LETTER Y WITH DIAERESIS */ +{ XK_EuroSign, 0x20ac }, /* EURO SIGN */ + +/* combining dead keys */ +{ XK_dead_abovedot, 0x0307 }, /* COMBINING DOT ABOVE */ +{ XK_dead_abovering, 0x030a }, /* COMBINING RING ABOVE */ +{ XK_dead_acute, 0x0301 }, /* COMBINING ACUTE ACCENT */ +{ XK_dead_breve, 0x0306 }, /* COMBINING BREVE */ +{ XK_dead_caron, 0x030c }, /* COMBINING CARON */ +{ XK_dead_cedilla, 0x0327 }, /* COMBINING CEDILLA */ +{ XK_dead_circumflex, 0x0302 }, /* COMBINING CIRCUMFLEX ACCENT */ +{ XK_dead_diaeresis, 0x0308 }, /* COMBINING DIAERESIS */ +{ XK_dead_doubleacute, 0x030b }, /* COMBINING DOUBLE ACUTE ACCENT */ +{ XK_dead_grave, 0x0300 }, /* COMBINING GRAVE ACCENT */ +{ XK_dead_macron, 0x0304 }, /* COMBINING MACRON */ +{ XK_dead_ogonek, 0x0328 }, /* COMBINING OGONEK */ +{ XK_dead_tilde, 0x0303 } /* COMBINING TILDE */ +}; +/* XXX -- map these too +XK_Cyrillic_GHE_bar +XK_Cyrillic_ZHE_descender +XK_Cyrillic_KA_descender +XK_Cyrillic_KA_vertstroke +XK_Cyrillic_EN_descender +XK_Cyrillic_U_straight +XK_Cyrillic_U_straight_bar +XK_Cyrillic_HA_descender +XK_Cyrillic_CHE_descender +XK_Cyrillic_CHE_vertstroke +XK_Cyrillic_SHHA +XK_Cyrillic_SCHWA +XK_Cyrillic_I_macron +XK_Cyrillic_O_bar +XK_Cyrillic_U_macron +XK_Cyrillic_ghe_bar +XK_Cyrillic_zhe_descender +XK_Cyrillic_ka_descender +XK_Cyrillic_ka_vertstroke +XK_Cyrillic_en_descender +XK_Cyrillic_u_straight +XK_Cyrillic_u_straight_bar +XK_Cyrillic_ha_descender +XK_Cyrillic_che_descender +XK_Cyrillic_che_vertstroke +XK_Cyrillic_shha +XK_Cyrillic_schwa +XK_Cyrillic_i_macron +XK_Cyrillic_o_bar +XK_Cyrillic_u_macron + +XK_Armenian_eternity +XK_Armenian_ligature_ew +XK_Armenian_full_stop +XK_Armenian_verjaket +XK_Armenian_parenright +XK_Armenian_parenleft +XK_Armenian_guillemotright +XK_Armenian_guillemotleft +XK_Armenian_em_dash +XK_Armenian_dot +XK_Armenian_mijaket +XK_Armenian_but +XK_Armenian_separation_mark +XK_Armenian_comma +XK_Armenian_en_dash +XK_Armenian_hyphen +XK_Armenian_yentamna +XK_Armenian_ellipsis +XK_Armenian_amanak +XK_Armenian_exclam +XK_Armenian_accent +XK_Armenian_shesht +XK_Armenian_paruyk +XK_Armenian_question +XK_Armenian_AYB +XK_Armenian_ayb +XK_Armenian_BEN +XK_Armenian_ben +XK_Armenian_GIM +XK_Armenian_gim +XK_Armenian_DA +XK_Armenian_da +XK_Armenian_YECH +XK_Armenian_yech +XK_Armenian_ZA +XK_Armenian_za +XK_Armenian_E +XK_Armenian_e +XK_Armenian_AT +XK_Armenian_at +XK_Armenian_TO +XK_Armenian_to +XK_Armenian_ZHE +XK_Armenian_zhe +XK_Armenian_INI +XK_Armenian_ini +XK_Armenian_LYUN +XK_Armenian_lyun +XK_Armenian_KHE +XK_Armenian_khe +XK_Armenian_TSA +XK_Armenian_tsa +XK_Armenian_KEN +XK_Armenian_ken +XK_Armenian_HO +XK_Armenian_ho +XK_Armenian_DZA +XK_Armenian_dza +XK_Armenian_GHAT +XK_Armenian_ghat +XK_Armenian_TCHE +XK_Armenian_tche +XK_Armenian_MEN +XK_Armenian_men +XK_Armenian_HI +XK_Armenian_hi +XK_Armenian_NU +XK_Armenian_nu +XK_Armenian_SHA +XK_Armenian_sha +XK_Armenian_VO +XK_Armenian_vo +XK_Armenian_CHA +XK_Armenian_cha +XK_Armenian_PE +XK_Armenian_pe +XK_Armenian_JE +XK_Armenian_je +XK_Armenian_RA +XK_Armenian_ra +XK_Armenian_SE +XK_Armenian_se +XK_Armenian_VEV +XK_Armenian_vev +XK_Armenian_TYUN +XK_Armenian_tyun +XK_Armenian_RE +XK_Armenian_re +XK_Armenian_TSO +XK_Armenian_tso +XK_Armenian_VYUN +XK_Armenian_vyun +XK_Armenian_PYUR +XK_Armenian_pyur +XK_Armenian_KE +XK_Armenian_ke +XK_Armenian_O +XK_Armenian_o +XK_Armenian_FE +XK_Armenian_fe +XK_Armenian_apostrophe +XK_Armenian_section_sign + +XK_Georgian_an +XK_Georgian_ban +XK_Georgian_gan +XK_Georgian_don +XK_Georgian_en +XK_Georgian_vin +XK_Georgian_zen +XK_Georgian_tan +XK_Georgian_in +XK_Georgian_kan +XK_Georgian_las +XK_Georgian_man +XK_Georgian_nar +XK_Georgian_on +XK_Georgian_par +XK_Georgian_zhar +XK_Georgian_rae +XK_Georgian_san +XK_Georgian_tar +XK_Georgian_un +XK_Georgian_phar +XK_Georgian_khar +XK_Georgian_ghan +XK_Georgian_qar +XK_Georgian_shin +XK_Georgian_chin +XK_Georgian_can +XK_Georgian_jil +XK_Georgian_cil +XK_Georgian_char +XK_Georgian_xan +XK_Georgian_jhan +XK_Georgian_hae +XK_Georgian_he +XK_Georgian_hie +XK_Georgian_we +XK_Georgian_har +XK_Georgian_hoe +XK_Georgian_fi + +XK_Ccedillaabovedot +XK_Xabovedot +XK_Qabovedot +XK_Ibreve +XK_IE +XK_UO +XK_Zstroke +XK_Gcaron +XK_Obarred +XK_ccedillaabovedot +XK_xabovedot +XK_Ocaron +XK_qabovedot +XK_ibreve +XK_ie +XK_uo +XK_zstroke +XK_gcaron +XK_ocaron +XK_obarred +XK_SCHWA +XK_Lbelowdot +XK_Lstrokebelowdot +XK_Gtilde +XK_lbelowdot +XK_lstrokebelowdot +XK_gtilde +XK_schwa + +XK_Abelowdot +XK_abelowdot +XK_Ahook +XK_ahook +XK_Acircumflexacute +XK_acircumflexacute +XK_Acircumflexgrave +XK_acircumflexgrave +XK_Acircumflexhook +XK_acircumflexhook +XK_Acircumflextilde +XK_acircumflextilde +XK_Acircumflexbelowdot +XK_acircumflexbelowdot +XK_Abreveacute +XK_abreveacute +XK_Abrevegrave +XK_abrevegrave +XK_Abrevehook +XK_abrevehook +XK_Abrevetilde +XK_abrevetilde +XK_Abrevebelowdot +XK_abrevebelowdot +XK_Ebelowdot +XK_ebelowdot +XK_Ehook +XK_ehook +XK_Etilde +XK_etilde +XK_Ecircumflexacute +XK_ecircumflexacute +XK_Ecircumflexgrave +XK_ecircumflexgrave +XK_Ecircumflexhook +XK_ecircumflexhook +XK_Ecircumflextilde +XK_ecircumflextilde +XK_Ecircumflexbelowdot +XK_ecircumflexbelowdot +XK_Ihook +XK_ihook +XK_Ibelowdot +XK_ibelowdot +XK_Obelowdot +XK_obelowdot +XK_Ohook +XK_ohook +XK_Ocircumflexacute +XK_ocircumflexacute +XK_Ocircumflexgrave +XK_ocircumflexgrave +XK_Ocircumflexhook +XK_ocircumflexhook +XK_Ocircumflextilde +XK_ocircumflextilde +XK_Ocircumflexbelowdot +XK_ocircumflexbelowdot +XK_Ohornacute +XK_ohornacute +XK_Ohorngrave +XK_ohorngrave +XK_Ohornhook +XK_ohornhook +XK_Ohorntilde +XK_ohorntilde +XK_Ohornbelowdot +XK_ohornbelowdot +XK_Ubelowdot +XK_ubelowdot +XK_Uhook +XK_uhook +XK_Uhornacute +XK_uhornacute +XK_Uhorngrave +XK_uhorngrave +XK_Uhornhook +XK_uhornhook +XK_Uhorntilde +XK_uhorntilde +XK_Uhornbelowdot +XK_uhornbelowdot +XK_Ybelowdot +XK_ybelowdot +XK_Yhook +XK_yhook +XK_Ytilde +XK_ytilde +XK_Ohorn +XK_ohorn +XK_Uhorn +XK_uhorn +*/ + +// map "Internet" keys to KeyIDs +static const KeySym s_map1008FF[] = +{ + /* 0x00 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x08 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x10 */ 0, kKeyAudioDown, kKeyAudioMute, kKeyAudioUp, + /* 0x14 */ kKeyAudioPlay, kKeyAudioStop, kKeyAudioPrev, kKeyAudioNext, + /* 0x18 */ kKeyWWWHome, kKeyAppMail, 0, kKeyWWWSearch, 0, 0, 0, 0, + /* 0x20 */ 0, 0, 0, 0, 0, 0, kKeyWWWBack, kKeyWWWForward, + /* 0x28 */ kKeyWWWStop, kKeyWWWRefresh, 0, 0, kKeyEject, 0, 0, 0, + /* 0x30 */ kKeyWWWFavorites, 0, kKeyAppMedia, 0, 0, 0, 0, 0, + /* 0x38 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x40 */ kKeyAppUser1, kKeyAppUser2, 0, 0, 0, 0, 0, 0, + /* 0x48 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x50 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x58 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x60 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x68 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x70 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x78 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x80 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x88 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x90 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0x98 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0xa0 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0xa8 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0xb0 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0xb8 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0xc0 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0xc8 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0xd0 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0xd8 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0xe0 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0xe8 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0xf0 */ 0, 0, 0, 0, 0, 0, 0, 0, + /* 0xf8 */ 0, 0, 0, 0, 0, 0, 0, 0 +}; + + +// +// CXWindowsUtil +// + +CXWindowsUtil::CKeySymMap CXWindowsUtil::s_keySymToUCS4; + +bool +CXWindowsUtil::getWindowProperty(Display* display, Window window, + Atom property, CString* data, Atom* type, + SInt32* format, bool deleteProperty) +{ + assert(display != NULL); + + Atom actualType; + int actualDatumSize; + + // ignore errors. XGetWindowProperty() will report failure. + CXWindowsUtil::CErrorLock lock(display); + + // read the property + bool okay = true; + const long length = XMaxRequestSize(display); + long offset = 0; + unsigned long bytesLeft = 1; + while (bytesLeft != 0) { + // get more data + unsigned long numItems; + unsigned char* rawData; + if (XGetWindowProperty(display, window, property, + offset, length, False, AnyPropertyType, + &actualType, &actualDatumSize, + &numItems, &bytesLeft, &rawData) != Success || + actualType == None || actualDatumSize == 0) { + // failed + okay = false; + break; + } + + // compute bytes read and advance offset + unsigned long numBytes; + switch (actualDatumSize) { + case 8: + default: + numBytes = numItems; + offset += numItems / 4; + break; + + case 16: + numBytes = 2 * numItems; + offset += numItems / 2; + break; + + case 32: + numBytes = 4 * numItems; + offset += numItems; + break; + } + + // append data + if (data != NULL) { + data->append((char*)rawData, numBytes); + } + else { + // data is not required so don't try to get any more + bytesLeft = 0; + } + + // done with returned data + XFree(rawData); + } + + // delete the property if requested + if (deleteProperty) { + XDeleteProperty(display, window, property); + } + + // save property info + if (type != NULL) { + *type = actualType; + } + if (format != NULL) { + *format = static_cast(actualDatumSize); + } + + if (okay) { + LOG((CLOG_DEBUG2 "read property %d on window 0x%08x: bytes=%d", property, window, (data == NULL) ? 0 : data->size())); + return true; + } + else { + LOG((CLOG_DEBUG2 "can't read property %d on window 0x%08x", property, window)); + return false; + } +} + +bool +CXWindowsUtil::setWindowProperty(Display* display, Window window, + Atom property, const void* vdata, UInt32 size, + Atom type, SInt32 format) +{ + const UInt32 length = 4 * XMaxRequestSize(display); + const unsigned char* data = reinterpret_cast(vdata); + UInt32 datumSize = static_cast(format / 8); + // format 32 on 64bit systems is 8 bytes not 4. + if (format == 32) { + datumSize = sizeof(Atom); + } + + // save errors + bool error = false; + CXWindowsUtil::CErrorLock lock(display, &error); + + // how much data to send in first chunk? + UInt32 chunkSize = size; + if (chunkSize > length) { + chunkSize = length; + } + + // send first chunk + XChangeProperty(display, window, property, + type, format, PropModeReplace, + data, chunkSize / datumSize); + + // append remaining chunks + data += chunkSize; + size -= chunkSize; + while (!error && size > 0) { + chunkSize = size; + if (chunkSize > length) { + chunkSize = length; + } + XChangeProperty(display, window, property, + type, format, PropModeAppend, + data, chunkSize / datumSize); + data += chunkSize; + size -= chunkSize; + } + + return !error; +} + +Time +CXWindowsUtil::getCurrentTime(Display* display, Window window) +{ + // select property events on window + XWindowAttributes attr; + XGetWindowAttributes(display, window, &attr); + XSelectInput(display, window, attr.your_event_mask | PropertyChangeMask); + + // make a property name to receive dummy change + Atom atom = XInternAtom(display, "TIMESTAMP", False); + + // do a zero-length append to get the current time + unsigned char dummy; + XChangeProperty(display, window, atom, + XA_INTEGER, 8, + PropModeAppend, + &dummy, 0); + + // look for property notify events with the following + CPropertyNotifyPredicateInfo filter; + filter.m_window = window; + filter.m_property = atom; + + // wait for reply + XEvent xevent; + XIfEvent(display, &xevent, &CXWindowsUtil::propertyNotifyPredicate, + (XPointer)&filter); + assert(xevent.type == PropertyNotify); + assert(xevent.xproperty.window == window); + assert(xevent.xproperty.atom == atom); + + // restore event mask + XSelectInput(display, window, attr.your_event_mask); + + return xevent.xproperty.time; +} + +KeyID +CXWindowsUtil::mapKeySymToKeyID(KeySym k) +{ + initKeyMaps(); + + switch (k & 0xffffff00) { + case 0x0000: + // Latin-1 + return static_cast(k); + + case 0xfe00: + // ISO 9995 Function and Modifier Keys + switch (k) { + case XK_ISO_Left_Tab: + return kKeyLeftTab; + + case XK_ISO_Level3_Shift: + return kKeyAltGr; + + case XK_ISO_Level5_Shift: + return XK_ISO_Level5_Shift; //FIXME: there is no "usual" key for this... + + case XK_ISO_Next_Group: + return kKeyNextGroup; + + case XK_ISO_Prev_Group: + return kKeyPrevGroup; + + case XK_dead_grave: + return kKeyDeadGrave; + + case XK_dead_acute: + return kKeyDeadAcute; + + case XK_dead_circumflex: + return kKeyDeadCircumflex; + + case XK_dead_tilde: + return kKeyDeadTilde; + + case XK_dead_macron: + return kKeyDeadMacron; + + case XK_dead_breve: + return kKeyDeadBreve; + + case XK_dead_abovedot: + return kKeyDeadAbovedot; + + case XK_dead_diaeresis: + return kKeyDeadDiaeresis; + + case XK_dead_abovering: + return kKeyDeadAbovering; + + case XK_dead_doubleacute: + return kKeyDeadDoubleacute; + + case XK_dead_caron: + return kKeyDeadCaron; + + case XK_dead_cedilla: + return kKeyDeadCedilla; + + case XK_dead_ogonek: + return kKeyDeadOgonek; + + default: + return kKeyNone; + } + + case 0xff00: + // MISCELLANY + return static_cast(k - 0xff00 + 0xef00); + + case 0x1008ff00: + // "Internet" keys + return s_map1008FF[k & 0xff]; + + default: { + // lookup character in table + CKeySymMap::const_iterator index = s_keySymToUCS4.find(k); + if (index != s_keySymToUCS4.end()) { + return static_cast(index->second); + } + + // unknown character + return kKeyNone; + } + } +} + +UInt32 +CXWindowsUtil::getModifierBitForKeySym(KeySym keysym) +{ + switch (keysym) { + case XK_Shift_L: + case XK_Shift_R: + return kKeyModifierBitShift; + + case XK_Control_L: + case XK_Control_R: + return kKeyModifierBitControl; + + case XK_Alt_L: + case XK_Alt_R: + return kKeyModifierBitAlt; + + case XK_Meta_L: + case XK_Meta_R: + return kKeyModifierBitMeta; + + case XK_Super_L: + case XK_Super_R: + case XK_Hyper_L: + case XK_Hyper_R: + return kKeyModifierBitSuper; + + case XK_Mode_switch: + case XK_ISO_Level3_Shift: + return kKeyModifierBitAltGr; + + case XK_ISO_Level5_Shift: + return kKeyModifierBitLevel5Lock; + + case XK_Caps_Lock: + return kKeyModifierBitCapsLock; + + case XK_Num_Lock: + return kKeyModifierBitNumLock; + + case XK_Scroll_Lock: + return kKeyModifierBitScrollLock; + + default: + return kKeyModifierBitNone; + } +} + +CString +CXWindowsUtil::atomToString(Display* display, Atom atom) +{ + if (atom == 0) { + return "None"; + } + + bool error = false; + CXWindowsUtil::CErrorLock lock(display, &error); + char* name = XGetAtomName(display, atom); + if (error) { + return CStringUtil::print(" (%d)", (int)atom); + } + else { + CString msg = CStringUtil::print("%s (%d)", name, (int)atom); + XFree(name); + return msg; + } +} + +CString +CXWindowsUtil::atomsToString(Display* display, const Atom* atom, UInt32 num) +{ + char** names = new char*[num]; + bool error = false; + CXWindowsUtil::CErrorLock lock(display, &error); + XGetAtomNames(display, const_cast(atom), (int)num, names); + CString msg; + if (error) { + for (UInt32 i = 0; i < num; ++i) { + msg += CStringUtil::print(" (%d), ", (int)atom[i]); + } + } + else { + for (UInt32 i = 0; i < num; ++i) { + msg += CStringUtil::print("%s (%d), ", names[i], (int)atom[i]); + XFree(names[i]); + } + } + delete[] names; + if (msg.size() > 2) { + msg.erase(msg.size() - 2); + } + return msg; +} + +void +CXWindowsUtil::convertAtomProperty(CString& data) +{ + // as best i can tell, 64-bit systems don't pack Atoms into properties + // as 32-bit numbers but rather as the 64-bit numbers they are. that + // seems wrong but we have to cope. sometimes we'll get a list of + // atoms that's 8*n+4 bytes long, missing the trailing 4 bytes which + // should all be 0. since we're going to reference the Atoms as + // 64-bit numbers we have to ensure the last number is a full 64 bits. + if (sizeof(Atom) != 4 && ((data.size() / 4) & 1) != 0) { + UInt32 zero = 0; + data.append(reinterpret_cast(&zero), sizeof(zero)); + } +} + +void +CXWindowsUtil::appendAtomData(CString& data, Atom atom) +{ + data.append(reinterpret_cast(&atom), sizeof(Atom)); +} + +void +CXWindowsUtil::replaceAtomData(CString& data, UInt32 index, Atom atom) +{ + data.replace(index * sizeof(Atom), sizeof(Atom), + reinterpret_cast(&atom), + sizeof(Atom)); +} + +void +CXWindowsUtil::appendTimeData(CString& data, Time time) +{ + data.append(reinterpret_cast(&time), sizeof(Time)); +} + +Bool +CXWindowsUtil::propertyNotifyPredicate(Display*, XEvent* xevent, XPointer arg) +{ + CPropertyNotifyPredicateInfo* filter = + reinterpret_cast(arg); + return (xevent->type == PropertyNotify && + xevent->xproperty.window == filter->m_window && + xevent->xproperty.atom == filter->m_property && + xevent->xproperty.state == PropertyNewValue) ? True : False; +} + +void +CXWindowsUtil::initKeyMaps() +{ + if (s_keySymToUCS4.empty()) { + for (size_t i =0; i < sizeof(s_keymap) / sizeof(s_keymap[0]); ++i) { + s_keySymToUCS4[s_keymap[i].keysym] = s_keymap[i].ucs4; + } + } +} + + +// +// CXWindowsUtil::CErrorLock +// + +CXWindowsUtil::CErrorLock* CXWindowsUtil::CErrorLock::s_top = NULL; + +CXWindowsUtil::CErrorLock::CErrorLock(Display* display) : + m_display(display) +{ + install(&CXWindowsUtil::CErrorLock::ignoreHandler, NULL); +} + +CXWindowsUtil::CErrorLock::CErrorLock(Display* display, bool* flag) : + m_display(display) +{ + install(&CXWindowsUtil::CErrorLock::saveHandler, flag); +} + +CXWindowsUtil::CErrorLock::CErrorLock(Display* display, + ErrorHandler handler, void* data) : + m_display(display) +{ + install(handler, data); +} + +CXWindowsUtil::CErrorLock::~CErrorLock() +{ + // make sure everything finishes before uninstalling handler + if (m_display != NULL) { + XSync(m_display, False); + } + + // restore old handler + XSetErrorHandler(m_oldXHandler); + s_top = m_next; +} + +void +CXWindowsUtil::CErrorLock::install(ErrorHandler handler, void* data) +{ + // make sure everything finishes before installing handler + if (m_display != NULL) { + XSync(m_display, False); + } + + // install handler + m_handler = handler; + m_userData = data; + m_oldXHandler = XSetErrorHandler( + &CXWindowsUtil::CErrorLock::internalHandler); + m_next = s_top; + s_top = this; +} + +int +CXWindowsUtil::CErrorLock::internalHandler(Display* display, XErrorEvent* event) +{ + if (s_top != NULL && s_top->m_handler != NULL) { + s_top->m_handler(display, event, s_top->m_userData); + } + return 0; +} + +void +CXWindowsUtil::CErrorLock::ignoreHandler(Display*, XErrorEvent* e, void*) +{ + LOG((CLOG_DEBUG1 "ignoring X error: %d", e->error_code)); +} + +void +CXWindowsUtil::CErrorLock::saveHandler(Display*, XErrorEvent* e, void* flag) +{ + LOG((CLOG_DEBUG1 "flagging X error: %d", e->error_code)); + *reinterpret_cast(flag) = true; +} diff --git a/src/lib/platform/CXWindowsUtil.h b/src/lib/platform/CXWindowsUtil.h new file mode 100644 index 00000000..150972ad --- /dev/null +++ b/src/lib/platform/CXWindowsUtil.h @@ -0,0 +1,188 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CXWINDOWSUTIL_H +#define CXWINDOWSUTIL_H + +#include "CString.h" +#include "BasicTypes.h" +#include "stdmap.h" +#include "stdvector.h" +#if X_DISPLAY_MISSING +# error X11 is required to build synergy +#else +# include +#endif + +//! X11 utility functions +class CXWindowsUtil { +public: + typedef std::vector KeySyms; + + //! Get property + /*! + Gets property \c property on \c window. \b Appends the data to + \c *data if \c data is not NULL, saves the property type in \c *type + if \c type is not NULL, and saves the property format in \c *format + if \c format is not NULL. If \c deleteProperty is true then the + property is deleted after being read. + */ + static bool getWindowProperty(Display*, + Window window, Atom property, + CString* data, Atom* type, + SInt32* format, bool deleteProperty); + + //! Set property + /*! + Sets property \c property on \c window to \c size bytes of data from + \c data. + */ + static bool setWindowProperty(Display*, + Window window, Atom property, + const void* data, UInt32 size, + Atom type, SInt32 format); + + //! Get X server time + /*! + Returns the current X server time. + */ + static Time getCurrentTime(Display*, Window); + + //! Convert KeySym to KeyID + /*! + Converts a KeySym to the equivalent KeyID. Returns kKeyNone if the + KeySym cannot be mapped. + */ + static UInt32 mapKeySymToKeyID(KeySym); + + //! Convert KeySym to corresponding KeyModifierMask + /*! + Converts a KeySym to the corresponding KeyModifierMask, or 0 if the + KeySym is not a modifier. + */ + static UInt32 getModifierBitForKeySym(KeySym keysym); + + //! Convert Atom to its string + /*! + Converts \p atom to its string representation. + */ + static CString atomToString(Display*, Atom atom); + + //! Convert several Atoms to a string + /*! + Converts each atom in \p atoms to its string representation and + concatenates the results. + */ + static CString atomsToString(Display* display, + const Atom* atom, UInt32 num); + + //! Prepare a property of atoms for use + /*! + 64-bit systems may need to modify a property's data if it's a + list of Atoms before using it. + */ + static void convertAtomProperty(CString& data); + + //! Append an Atom to property data + /*! + Converts \p atom to a 32-bit on-the-wire format and appends it to + \p data. + */ + static void appendAtomData(CString& data, Atom atom); + + //! Replace an Atom in property data + /*! + Converts \p atom to a 32-bit on-the-wire format and replaces the atom + at index \p index in \p data. + */ + static void replaceAtomData(CString& data, + UInt32 index, Atom atom); + + //! Append an Time to property data + /*! + Converts \p time to a 32-bit on-the-wire format and appends it to + \p data. + */ + static void appendTimeData(CString& data, Time time); + + //! X11 error handler + /*! + This class sets an X error handler in the c'tor and restores the + previous error handler in the d'tor. A lock should only be + installed while the display is locked by the thread. + + CErrorLock() ignores errors + CErrorLock(bool* flag) sets *flag to true if any error occurs + */ + class CErrorLock { + public: + //! Error handler type + typedef void (*ErrorHandler)(Display*, XErrorEvent*, void* userData); + + /*! + Ignore X11 errors. + */ + CErrorLock(Display*); + + /*! + Set \c *errorFlag if any error occurs. + */ + CErrorLock(Display*, bool* errorFlag); + + /*! + Call \c handler on each error. + */ + CErrorLock(Display*, ErrorHandler handler, void* userData); + + ~CErrorLock(); + + private: + void install(ErrorHandler, void*); + static int internalHandler(Display*, XErrorEvent*); + static void ignoreHandler(Display*, XErrorEvent*, void*); + static void saveHandler(Display*, XErrorEvent*, void*); + + private: + typedef int (*XErrorHandler)(Display*, XErrorEvent*); + + Display* m_display; + ErrorHandler m_handler; + void* m_userData; + XErrorHandler m_oldXHandler; + CErrorLock* m_next; + static CErrorLock* s_top; + }; + +private: + class CPropertyNotifyPredicateInfo { + public: + Window m_window; + Atom m_property; + }; + + static Bool propertyNotifyPredicate(Display*, + XEvent* xevent, XPointer arg); + + static void initKeyMaps(); + +private: + typedef std::map CKeySymMap; + + static CKeySymMap s_keySymToUCS4; +}; + +#endif diff --git a/src/lib/platform/HookDLL.cpp b/src/lib/platform/HookDLL.cpp new file mode 100644 index 00000000..c266ca95 --- /dev/null +++ b/src/lib/platform/HookDLL.cpp @@ -0,0 +1,341 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +/*-------------------------------------------------------------------------------------------------------- + Original comment: + + APIHIJACK.CPP - Based on DelayLoadProfileDLL.CPP, by Matt Pietrek for MSJ February 2000. + http://msdn.microsoft.com/library/periodic/period00/hood0200.htm + Adapted by Wade Brainerd, wadeb@wadeb.com +--------------------------------------------------------------------------------------------------------*/ + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include "HookDLL.h" + +#include +std::stringstream _hookDllLogStream; +#define LOG(s) \ + _hookDllLogStream.str(""); \ + _hookDllLogStream << "Synergy HookDLL: " << s << std::endl; \ + OutputDebugString( _hookDllLogStream.str().c_str() ); + +//=========================================================================== +// Called from the DLPD_IAT_STUB stubs. Increments "count" field of the stub + +void __cdecl DefaultHook( PVOID dummy ) +{ + // asm only supported on 32-bit +#ifdef _M_IX86 + __asm pushad // Save all general purpose registers + + // Get return address, then subtract 5 (size of a CALL X instruction) + // The result points at a DLPD_IAT_STUB + + // pointer math! &dummy-1 really subtracts sizeof(PVOID) + PDWORD pRetAddr = (PDWORD)(&dummy - 1); + + DLPD_IAT_STUB * pDLPDStub = (DLPD_IAT_STUB *)(*pRetAddr - 5); + + pDLPDStub->count++; + + #if 0 + // Remove the above conditional to get a cheezy API trace from + // the loader process. It's slow! + if ( !IMAGE_SNAP_BY_ORDINAL( pDLPDStub->pszNameOrOrdinal) ) + { + LOG( "Called hooked function: " ); + LOG( (PSTR)pDLPDStub->pszNameOrOrdinal ); + } + #endif + + __asm popad // Restore all general purpose registers +#endif +} + +// This function must be __cdecl!!! +void __cdecl DelayLoadProfileDLL_UpdateCount( PVOID dummy ); + +PIMAGE_IMPORT_DESCRIPTOR g_pFirstImportDesc; + +//=========================================================================== +// Given an HMODULE, returns a pointer to the PE header + +PIMAGE_NT_HEADERS PEHeaderFromHModule(HMODULE hModule) +{ + PIMAGE_NT_HEADERS pNTHeader = 0; + + __try + { + if ( PIMAGE_DOS_HEADER(hModule)->e_magic != IMAGE_DOS_SIGNATURE ) + __leave; + + pNTHeader = PIMAGE_NT_HEADERS(PBYTE(hModule) + + PIMAGE_DOS_HEADER(hModule)->e_lfanew); + + if ( pNTHeader->Signature != IMAGE_NT_SIGNATURE ) + pNTHeader = 0; + } + __except( EXCEPTION_EXECUTE_HANDLER ) + { + } + + return pNTHeader; +} + +//=========================================================================== +// Builds stubs for and redirects the IAT for one DLL (pImportDesc) + +bool RedirectIAT( SDLLHook* DLLHook, PIMAGE_IMPORT_DESCRIPTOR pImportDesc, PVOID pBaseLoadAddr ) +{ + PIMAGE_THUNK_DATA pIAT; // Ptr to import address table + PIMAGE_THUNK_DATA pINT; // Ptr to import names table + PIMAGE_THUNK_DATA pIteratingIAT; + + // Figure out which OS platform we're on + OSVERSIONINFO osvi; + osvi.dwOSVersionInfoSize = sizeof(osvi); + GetVersionEx( &osvi ); + + // If no import names table, we can't redirect this, so bail + if ( pImportDesc->OriginalFirstThunk == 0 ) + { + LOG( "no IAT available." ); + return false; + } + + pIAT = MakePtr( PIMAGE_THUNK_DATA, pBaseLoadAddr, pImportDesc->FirstThunk ); + pINT = MakePtr( PIMAGE_THUNK_DATA, pBaseLoadAddr, pImportDesc->OriginalFirstThunk ); + + // Count how many entries there are in this IAT. Array is 0 terminated + pIteratingIAT = pIAT; + unsigned cFuncs = 0; + while ( pIteratingIAT->u1.Function ) + { + cFuncs++; + pIteratingIAT++; + } + + if ( cFuncs == 0 ) // If no imported functions, we're done! + { + LOG( "no imported functions" ); + return false; + } + + // These next few lines ensure that we'll be able to modify the IAT, + // which is often in a read-only section in the EXE. + DWORD flOldProtect, flNewProtect, flDontCare; + MEMORY_BASIC_INFORMATION mbi; + + // Get the current protection attributes + VirtualQuery( pIAT, &mbi, sizeof(mbi) ); + + // remove ReadOnly and ExecuteRead attributes, add on ReadWrite flag + flNewProtect = mbi.Protect; + flNewProtect &= ~(PAGE_READONLY | PAGE_EXECUTE_READ); + flNewProtect |= (PAGE_READWRITE); + + if ( !VirtualProtect( pIAT, sizeof(PVOID) * cFuncs, + flNewProtect, &flOldProtect) ) + { + LOG( "could not remove ReadOnly and ExecuteRead attributes, or add ReadWrite flag" ); + return false; + } + + // If the Default hook is enabled, build an array of redirection stubs in the processes memory. + DLPD_IAT_STUB * pStubs = 0; + if ( DLLHook->UseDefault ) + { + // Allocate memory for the redirection stubs. Make one extra stub at the + // end to be a sentinel + pStubs = new DLPD_IAT_STUB[ cFuncs + 1]; + if ( !pStubs ) + { + LOG( "could not allocate memory for redirection stubs" ); + return false; + } + } + + // Scan through the IAT, completing the stubs and redirecting the IAT + // entries to point to the stubs + pIteratingIAT = pIAT; + + while ( pIteratingIAT->u1.Function ) + { + void* HookFn = 0; // Set to either the SFunctionHook or pStubs. + + if ( !IMAGE_SNAP_BY_ORDINAL( pINT->u1.Ordinal ) ) // import by name + { + PIMAGE_IMPORT_BY_NAME pImportName = MakePtr( PIMAGE_IMPORT_BY_NAME, pBaseLoadAddr, pINT->u1.AddressOfData ); + + LOG( "checking function with name: " << pImportName->Name ); + + // Iterate through the hook functions, searching for this import. + SFunctionHook* FHook = DLLHook->Functions; + while ( FHook->Name ) + { + if ( lstrcmpi( FHook->Name, (char*)pImportName->Name ) == 0 ) + { + // Save the old function in the SFunctionHook structure and get the new one. + FHook->OrigFn = (void*)pIteratingIAT->u1.Function; + HookFn = FHook->HookFn; + + LOG( "hooked function: " << pImportName->Name ); + + break; + } + + FHook++; + } + + // If the default function is enabled, store the name for the user. + if ( DLLHook->UseDefault ) + pStubs->pszNameOrOrdinal = (DWORD)&pImportName->Name; + } + else // added comparison for ordinal + { + LOG( "checking function at ordinal: " << pINT->u1.Ordinal ); + + SFunctionHook* FHook = DLLHook->Functions; + while ( FHook->Name ) + { + if ( (DWORD)FHook->Name == pINT->u1.Ordinal ) + { + // Save the old function in the SFunctionHook structure and get the new one. + FHook->OrigFn = (void*)pIteratingIAT->u1.Function; + HookFn = FHook->HookFn; + + LOG( "hooked ordinal: " << pINT->u1.Ordinal ); + + break; + } + FHook++; + } + // If the default function is enabled, store the ordinal for the user. + if ( DLLHook->UseDefault ) + pStubs->pszNameOrOrdinal = (DWORD)pINT->u1.Ordinal; + } + + // If the default function is enabled, fill in the fields to the stub code. + if ( DLLHook->UseDefault ) + { + pStubs->data_call = (DWORD)(PDWORD)DLLHook->DefaultFn + - (DWORD)(PDWORD)&pStubs->instr_JMP; + pStubs->data_JMP = *(PDWORD)pIteratingIAT - (DWORD)(PDWORD)&pStubs->count; + + // If it wasn't manually hooked, use the Stub function. + if ( !HookFn ) + HookFn = (void*)pStubs; + } + + // Replace the IAT function pointer if we have a hook. + if ( HookFn ) + { + // Cheez-o hack to see if what we're importing is code or data. + // If it's code, we shouldn't be able to write to it + if ( IsBadWritePtr( (PVOID)pIteratingIAT->u1.Function, 1 ) ) + { + pIteratingIAT->u1.Function = (DWORD)(PDWORD)HookFn; + } + else if ( osvi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS ) + { + // Special hack for Win9X, which builds stubs for imported + // functions in system DLLs (Loaded above 2GB). These stubs are + // writeable, so we have to explicitly check for this case + if ( pIteratingIAT->u1.Function > (DWORD)(PDWORD)0x80000000 ) + pIteratingIAT->u1.Function = (DWORD)(PDWORD)HookFn; + } + } + + if ( DLLHook->UseDefault ) + pStubs++; // Advance to next stub + + pIteratingIAT++; // Advance to next IAT entry + pINT++; // Advance to next INT entry + } + + if ( DLLHook->UseDefault ) + pStubs->pszNameOrOrdinal = 0; // Final stub is a sentinel + + // Put the page attributes back the way they were. + VirtualProtect( pIAT, sizeof(PVOID) * cFuncs, flOldProtect, &flDontCare); + + return true; +} + +//=========================================================================== +// Top level routine to find the EXE's imports, and redirect them +bool HookAPICalls( SDLLHook* hook ) +{ + if ( !hook ) + { + LOG("no hook"); + return false; + } + + HMODULE hModEXE = GetModuleHandle( 0 ); + + PIMAGE_NT_HEADERS pExeNTHdr = PEHeaderFromHModule( hModEXE ); + + if ( !pExeNTHdr ) + { + LOG("no PE header"); + return false; + } + + DWORD importRVA = pExeNTHdr->OptionalHeader.DataDirectory + [IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; + if ( !importRVA ) + { + LOG("no virtual address for image directory entry import"); + return false; + } + + // Convert imports RVA to a usable pointer + PIMAGE_IMPORT_DESCRIPTOR pImportDesc = MakePtr( PIMAGE_IMPORT_DESCRIPTOR, + hModEXE, importRVA ); + + // Save off imports address in a global for later use + g_pFirstImportDesc = pImportDesc; + + // Iterate through each import descriptor, and redirect if appropriate + while ( pImportDesc->FirstThunk ) + { + PSTR pszImportModuleName = MakePtr( PSTR, hModEXE, pImportDesc->Name); + + if ( lstrcmpi( pszImportModuleName, hook->Name ) == 0 ) + { + LOG( "found " << hook->Name << " in process" ); + + if ( RedirectIAT( hook, pImportDesc, (PVOID)hModEXE ) ) + { + LOG( "redirected IAT ok" ); + return true; + } + else + { + LOG( "failed to redirect IAT" ); + } + } + + pImportDesc++; // Advance to next import descriptor + } + + return false; +} + diff --git a/src/lib/platform/HookDLL.h b/src/lib/platform/HookDLL.h new file mode 100644 index 00000000..0f59eac9 --- /dev/null +++ b/src/lib/platform/HookDLL.h @@ -0,0 +1,80 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +/*-------------------------------------------------------------------------------------------------------- + Original comment: + + APIHIJACK.H - Based on DelayLoadProfileDLL.CPP, by Matt Pietrek for MSJ February 2000. + http://msdn.microsoft.com/library/periodic/period00/hood0200.htm + Adapted by Wade Brainerd, wadeb@wadeb.com +--------------------------------------------------------------------------------------------------------*/ + +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +#pragma warning(disable:4200) + +// Macro for convenient pointer addition. +// Essentially treats the last two parameters as DWORDs. The first +// parameter is used to typecast the result to the appropriate pointer type. +#define MakePtr(cast, ptr, addValue ) (cast)( (DWORD)(ptr)+(DWORD)(addValue)) + +// Default Hook Stub Structure: Contains data about the original function, Name/Ordinal, Address +// and a Count field. This is actually a block of assembly code. +#pragma pack( push, 1 ) +struct DLPD_IAT_STUB +{ + BYTE instr_CALL; + DWORD data_call; + BYTE instr_JMP; + DWORD data_JMP; + DWORD count; + DWORD pszNameOrOrdinal; + + DLPD_IAT_STUB() : instr_CALL( 0xE8 ), instr_JMP( 0xE9 ), count( 0 ) {} +}; +#pragma pack( pop ) + +// Example DefaultHook procedure, called from the DLPD_IAT_STUB stubs. +// Increments "count" field of the stub. +// See the implementation for more information. +void __cdecl DefaultHook( PVOID dummy ); + +struct SFunctionHook +{ + char* Name; // Function name, e.g. "DirectDrawCreateEx". + void* HookFn; // Address of your function. + void* OrigFn; // Stored by HookAPICalls, the address of the original function. +}; + +struct SDLLHook +{ + // Name of the DLL, e.g. "DDRAW.DLL" + char* Name; + + // Set true to call the default for all non-hooked functions before they are executed. + bool UseDefault; + void* DefaultFn; + + // Function hook array. Terminated with a NULL Name field. + SFunctionHook Functions[]; +}; + +// Hook functions one or more DLLs. +bool HookAPICalls( SDLLHook* hook ); diff --git a/src/lib/platform/IMSWindowsClipboardFacade.h b/src/lib/platform/IMSWindowsClipboardFacade.h new file mode 100644 index 00000000..23202f10 --- /dev/null +++ b/src/lib/platform/IMSWindowsClipboardFacade.h @@ -0,0 +1,34 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IMWINDOWSCLIPBOARDFACADE +#define IMWINDOWSCLIPBOARDFACADE + +#include "IInterface.h" +#define WIN32_LEAN_AND_MEAN +#include + +class IMSWindowsClipboardConverter; + +class IMSWindowsClipboardFacade : public IInterface +{ +public: + virtual void write(HANDLE win32Data, UINT win32Format) = 0; + virtual ~IMSWindowsClipboardFacade() { } +}; + +#endif \ No newline at end of file diff --git a/src/lib/platform/OSXScreenSaverControl.h b/src/lib/platform/OSXScreenSaverControl.h new file mode 100644 index 00000000..fa2d99e0 --- /dev/null +++ b/src/lib/platform/OSXScreenSaverControl.h @@ -0,0 +1,53 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +// ScreenSaver.framework private API +// Class dumping by Alex Harper http://www.ragingmenace.com/ + +#import + +@protocol ScreenSaverControl +- (double)screenSaverTimeRemaining; +- (void)restartForUser:fp12; +- (void)screenSaverStopNow; +- (void)screenSaverStartNow; +- (void)setScreenSaverCanRun:(char)fp12; +- (BOOL)screenSaverCanRun; +- (BOOL)screenSaverIsRunning; +@end + + +@interface ScreenSaverController:NSObject + ++ controller; ++ monitor; ++ daemonConnectionName; ++ daemonPath; ++ enginePath; +- init; +- (void)dealloc; +- (void)_connectionClosed:fp12; +- (BOOL)screenSaverIsRunning; +- (BOOL)screenSaverCanRun; +- (void)setScreenSaverCanRun:(char)fp12; +- (void)screenSaverStartNow; +- (void)screenSaverStopNow; +- (void)restartForUser:fp12; +- (double)screenSaverTimeRemaining; + +@end + diff --git a/src/lib/platform/XInput13.h b/src/lib/platform/XInput13.h new file mode 100644 index 00000000..dfeffdd2 --- /dev/null +++ b/src/lib/platform/XInput13.h @@ -0,0 +1,228 @@ +/*************************************************************************** +* * +* XInput.h -- This module defines XBOX controller APIs * +* and constansts for the Windows platform. * +* * +* Copyright (c) Microsoft Corp. All rights reserved. * +* * +***************************************************************************/ +#ifndef _XINPUT_H_ +#define _XINPUT_H_ + +#include + +// Current name of the DLL shipped in the same SDK as this header. +// The name reflects the current version +#ifndef XINPUT_USE_9_1_0 +#define XINPUT_DLL_A "xinput1_3.dll" +#define XINPUT_DLL_W L"xinput1_3.dll" +#else +#define XINPUT_DLL_A "xinput9_1_0.dll" +#define XINPUT_DLL_W L"xinput9_1_0.dll" +#endif +#ifdef UNICODE + #define XINPUT_DLL XINPUT_DLL_W +#else + #define XINPUT_DLL XINPUT_DLL_A +#endif + +// +// Device types available in XINPUT_CAPABILITIES +// +#define XINPUT_DEVTYPE_GAMEPAD 0x01 + +// +// Device subtypes available in XINPUT_CAPABILITIES +// +#define XINPUT_DEVSUBTYPE_GAMEPAD 0x01 + +#ifndef XINPUT_USE_9_1_0 + +#define XINPUT_DEVSUBTYPE_WHEEL 0x02 +#define XINPUT_DEVSUBTYPE_ARCADE_STICK 0x03 +#define XINPUT_DEVSUBTYPE_FLIGHT_SICK 0x04 +#define XINPUT_DEVSUBTYPE_DANCE_PAD 0x05 +#define XINPUT_DEVSUBTYPE_GUITAR 0x06 +#define XINPUT_DEVSUBTYPE_DRUM_KIT 0x08 + +#endif // !XINPUT_USE_9_1_0 + + + +// +// Flags for XINPUT_CAPABILITIES +// +#define XINPUT_CAPS_VOICE_SUPPORTED 0x0004 + +// +// Constants for gamepad buttons +// +#define XINPUT_GAMEPAD_DPAD_UP 0x0001 +#define XINPUT_GAMEPAD_DPAD_DOWN 0x0002 +#define XINPUT_GAMEPAD_DPAD_LEFT 0x0004 +#define XINPUT_GAMEPAD_DPAD_RIGHT 0x0008 +#define XINPUT_GAMEPAD_START 0x0010 +#define XINPUT_GAMEPAD_BACK 0x0020 +#define XINPUT_GAMEPAD_LEFT_THUMB 0x0040 +#define XINPUT_GAMEPAD_RIGHT_THUMB 0x0080 +#define XINPUT_GAMEPAD_LEFT_SHOULDER 0x0100 +#define XINPUT_GAMEPAD_RIGHT_SHOULDER 0x0200 +#define XINPUT_GAMEPAD_A 0x1000 +#define XINPUT_GAMEPAD_B 0x2000 +#define XINPUT_GAMEPAD_X 0x4000 +#define XINPUT_GAMEPAD_Y 0x8000 + + +// +// Gamepad thresholds +// +#define XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE 7849 +#define XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE 8689 +#define XINPUT_GAMEPAD_TRIGGER_THRESHOLD 30 + +// +// Flags to pass to XInputGetCapabilities +// +#define XINPUT_FLAG_GAMEPAD 0x00000001 + + +#ifndef XINPUT_USE_9_1_0 + +// +// Devices that support batteries +// +#define BATTERY_DEVTYPE_GAMEPAD 0x00 +#define BATTERY_DEVTYPE_HEADSET 0x01 + +// +// Flags for battery status level +// +#define BATTERY_TYPE_DISCONNECTED 0x00 // This device is not connected +#define BATTERY_TYPE_WIRED 0x01 // Wired device, no battery +#define BATTERY_TYPE_ALKALINE 0x02 // Alkaline battery source +#define BATTERY_TYPE_NIMH 0x03 // Nickel Metal Hydride battery source +#define BATTERY_TYPE_UNKNOWN 0xFF // Cannot determine the battery type + +// These are only valid for wireless, connected devices, with known battery types +// The amount of use time remaining depends on the type of device. +#define BATTERY_LEVEL_EMPTY 0x00 +#define BATTERY_LEVEL_LOW 0x01 +#define BATTERY_LEVEL_MEDIUM 0x02 +#define BATTERY_LEVEL_FULL 0x03 + +// User index definitions +#define XUSER_MAX_COUNT 4 + +#define XUSER_INDEX_ANY 0x000000FF + + +// +// Codes returned for the gamepad keystroke +// + +#define VK_PAD_A 0x5800 +#define VK_PAD_B 0x5801 +#define VK_PAD_X 0x5802 +#define VK_PAD_Y 0x5803 +#define VK_PAD_RSHOULDER 0x5804 +#define VK_PAD_LSHOULDER 0x5805 +#define VK_PAD_LTRIGGER 0x5806 +#define VK_PAD_RTRIGGER 0x5807 + +#define VK_PAD_DPAD_UP 0x5810 +#define VK_PAD_DPAD_DOWN 0x5811 +#define VK_PAD_DPAD_LEFT 0x5812 +#define VK_PAD_DPAD_RIGHT 0x5813 +#define VK_PAD_START 0x5814 +#define VK_PAD_BACK 0x5815 +#define VK_PAD_LTHUMB_PRESS 0x5816 +#define VK_PAD_RTHUMB_PRESS 0x5817 + +#define VK_PAD_LTHUMB_UP 0x5820 +#define VK_PAD_LTHUMB_DOWN 0x5821 +#define VK_PAD_LTHUMB_RIGHT 0x5822 +#define VK_PAD_LTHUMB_LEFT 0x5823 +#define VK_PAD_LTHUMB_UPLEFT 0x5824 +#define VK_PAD_LTHUMB_UPRIGHT 0x5825 +#define VK_PAD_LTHUMB_DOWNRIGHT 0x5826 +#define VK_PAD_LTHUMB_DOWNLEFT 0x5827 + +#define VK_PAD_RTHUMB_UP 0x5830 +#define VK_PAD_RTHUMB_DOWN 0x5831 +#define VK_PAD_RTHUMB_RIGHT 0x5832 +#define VK_PAD_RTHUMB_LEFT 0x5833 +#define VK_PAD_RTHUMB_UPLEFT 0x5834 +#define VK_PAD_RTHUMB_UPRIGHT 0x5835 +#define VK_PAD_RTHUMB_DOWNRIGHT 0x5836 +#define VK_PAD_RTHUMB_DOWNLEFT 0x5837 + +// +// Flags used in XINPUT_KEYSTROKE +// +#define XINPUT_KEYSTROKE_KEYDOWN 0x0001 +#define XINPUT_KEYSTROKE_KEYUP 0x0002 +#define XINPUT_KEYSTROKE_REPEAT 0x0004 + +#endif //!XINPUT_USE_9_1_0 + +// +// Structures used by XInput APIs +// +typedef struct _XINPUT_GAMEPAD +{ + WORD wButtons; + BYTE bLeftTrigger; + BYTE bRightTrigger; + SHORT sThumbLX; + SHORT sThumbLY; + SHORT sThumbRX; + SHORT sThumbRY; +} XINPUT_GAMEPAD, *PXINPUT_GAMEPAD; + +typedef struct _XINPUT_STATE +{ + DWORD dwPacketNumber; + XINPUT_GAMEPAD Gamepad; +} XINPUT_STATE, *PXINPUT_STATE; + +typedef struct _XINPUT_VIBRATION +{ + WORD wLeftMotorSpeed; + WORD wRightMotorSpeed; +} XINPUT_VIBRATION, *PXINPUT_VIBRATION; + +typedef struct _XINPUT_CAPABILITIES +{ + BYTE Type; + BYTE SubType; + WORD Flags; + XINPUT_GAMEPAD Gamepad; + XINPUT_VIBRATION Vibration; +} XINPUT_CAPABILITIES, *PXINPUT_CAPABILITIES; + +#ifndef XINPUT_USE_9_1_0 + +typedef struct _XINPUT_BATTERY_INFORMATION +{ + BYTE BatteryType; + BYTE BatteryLevel; +} XINPUT_BATTERY_INFORMATION, *PXINPUT_BATTERY_INFORMATION; + +typedef struct _XINPUT_KEYSTROKE +{ + WORD VirtualKey; + WCHAR Unicode; + WORD Flags; + BYTE UserIndex; + BYTE HidCode; +} XINPUT_KEYSTROKE, *PXINPUT_KEYSTROKE; + +#endif // !XINPUT_USE_9_1_0 + +// +// XInput APIs +// + +// now defined in proxy class. + +#endif //_XINPUT_H_ diff --git a/src/lib/platform/XInputHook.cpp b/src/lib/platform/XInputHook.cpp new file mode 100644 index 00000000..9a0ca1ba --- /dev/null +++ b/src/lib/platform/XInputHook.cpp @@ -0,0 +1,295 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#define WIN32_LEAN_AND_MEAN +#define SYNERGY_EXPORT_XINPUT_HOOKS +#define REQUIRED_XINPUT_DLL "xinput1_3.dll" +#define HOOK_TIMEOUT 10000 // 10 sec + +#include +#include +#include "XInputHook.h" +#include "HookDLL.h" +#include + +HINSTANCE dll; +char name[256]; + +#pragma data_seg(".SHARED") + +HHOOK s_hook = NULL; + +// @todo use a struct for multiple gamepad support +WORD s_buttons = 0; +SHORT s_leftStickX = 0; +SHORT s_leftStickY = 0; +SHORT s_rightStickX = 0; +SHORT s_rightStickY = 0; +BYTE s_leftTrigger = 0; +BYTE s_rightTrigger = 0; +BOOL s_timingReqQueued = FALSE; +BOOL s_timingRespQueued = FALSE; +DWORD s_lastFakeMillis = 0; +WORD s_fakeFreqMillis = 0; +DWORD s_packetNumber = 0; +WORD s_leftMotor = 0; +WORD s_rightMotor = 0; +BOOL s_feedbackQueued = FALSE; + +#pragma data_seg() + +#pragma comment(linker, "/SECTION:.SHARED,RWS") + +#include +std::stringstream _xInputHookLogStream; +#define LOG(s) \ + _xInputHookLogStream.str(""); \ + _xInputHookLogStream << "Synergy XInputHook: " << s << endl; \ + OutputDebugString( _xInputHookLogStream.str().c_str()) + +using namespace std; + +SDLLHook s_xInputHook = +{ + XINPUT_DLL, + false, NULL, + { + { (char*)(0x80000002), HookXInputGetState }, + { (char*)(0x80000003), HookXInputSetState }, + { (char*)(0x80000004), HookXInputGetCapabilities }, + } +}; + +BOOL APIENTRY +DllMain(HINSTANCE module, DWORD reason, LPVOID reserved) +{ + if (reason == DLL_PROCESS_ATTACH) + { + dll = module; + + // disable unwanted thread notifications to reduce overhead + DisableThreadLibraryCalls(dll); + + GetModuleFileName(GetModuleHandle(NULL), name, sizeof(name)); + + // don't hook synergys (this needs to detect real input) + if (string(name).find("synergy") == string::npos) + { + LOG("checking '" << name << "' for " << XINPUT_DLL); + HookAPICalls(&s_xInputHook); + } + } + + return TRUE; +} + +void +SetXInputButtons(DWORD userIndex, WORD buttons) +{ + s_buttons = buttons; + + s_packetNumber++; + + LOG("SetXInputButtons: idx=" << userIndex << ", btns=" << buttons); +} + +void +SetXInputSticks(DWORD userIndex, SHORT lx, SHORT ly, SHORT rx, SHORT ry) +{ + s_leftStickX = lx; + s_leftStickY = ly; + s_rightStickX = rx; + s_rightStickY = ry; + + s_packetNumber++; + + LOG("SetXInputSticks:" << + " l=" << s_leftStickX << "," << s_leftStickY << + " r=" << s_rightStickX << "," << s_rightStickY); +} + +void +SetXInputTriggers(DWORD userIndex, BYTE left, BYTE right) +{ + s_leftTrigger = left; + s_rightTrigger = right; + + s_packetNumber++; + + LOG("SetXInputTriggers: " << + "l=" << (int)left << " r=" << (int)right); +} + +void +QueueXInputTimingReq() +{ + s_timingReqQueued = TRUE; +} + +BOOL +DequeueXInputTimingResp() +{ + BOOL result = s_timingRespQueued; + s_timingRespQueued = FALSE; + return result; +} + +BOOL +DequeueXInputFeedback(WORD* leftMotor, WORD* rightMotor) +{ + if (s_feedbackQueued) + { + *leftMotor = s_leftMotor; + *rightMotor = s_rightMotor; + s_feedbackQueued = FALSE; + return TRUE; + } + return FALSE; +} + +WORD +GetXInputFakeFreqMillis() +{ + return s_fakeFreqMillis; +} + +DWORD WINAPI +HookXInputGetState(DWORD userIndex, XINPUT_STATE* state) +{ + // @todo multiple device support + if (userIndex != 0) + { + return ERROR_DEVICE_NOT_CONNECTED; + } + + DWORD now = GetTickCount(); + s_fakeFreqMillis = (WORD)(now - s_lastFakeMillis); + s_lastFakeMillis = now; + + state->dwPacketNumber = s_packetNumber; + state->Gamepad.wButtons = s_buttons; + state->Gamepad.bLeftTrigger = s_leftTrigger; + state->Gamepad.bRightTrigger = s_rightTrigger; + state->Gamepad.sThumbLX = s_leftStickX; + state->Gamepad.sThumbLY = s_leftStickY; + state->Gamepad.sThumbRX = s_rightStickX; + state->Gamepad.sThumbRY = s_rightStickY; + + LOG("HookXInputGetState" + << ", idx=" << userIndex + << ", pkt=" << state->dwPacketNumber + << ", btn=" << state->Gamepad.wButtons + << ", t1=" << (int)state->Gamepad.bLeftTrigger + << ", t2=" << (int)state->Gamepad.bRightTrigger + << ", s1=" << state->Gamepad.sThumbLX << "," << state->Gamepad.sThumbLY + << ", s2=" << state->Gamepad.sThumbRX << "," << state->Gamepad.sThumbRY); + + if (s_timingReqQueued) + { + s_timingRespQueued = TRUE; + s_timingReqQueued = FALSE; + LOG("timing response queued"); + } + + return ERROR_SUCCESS; +} + +DWORD WINAPI +HookXInputSetState(DWORD userIndex, XINPUT_VIBRATION* vibration) +{ + // @todo multiple device support + if (userIndex != 0) + { + return ERROR_DEVICE_NOT_CONNECTED; + } + + // only change values and queue feedback change if + // feedback has actually changed. + if ((s_leftMotor != vibration->wLeftMotorSpeed) || + (s_rightMotor != vibration->wRightMotorSpeed)) + { + s_leftMotor = vibration->wLeftMotorSpeed; + s_rightMotor = vibration->wRightMotorSpeed; + s_feedbackQueued = TRUE; + + LOG("HookXInputSetState" + ", idx=" << userIndex << + ", lm=" << s_leftMotor << + ", rm=" << s_rightMotor); + } + + return ERROR_SUCCESS; +} + +DWORD WINAPI +HookXInputGetCapabilities(DWORD userIndex, DWORD flags, XINPUT_CAPABILITIES* capabilities) +{ + // @todo multiple device support + if (userIndex != 0) + { + return ERROR_DEVICE_NOT_CONNECTED; + } + + LOG("HookXInputGetCapabilities" + ", idx=" << userIndex << + ", flags=" << flags); + + capabilities->Type = 1; + capabilities->SubType = 1; + capabilities->Flags = 4; + capabilities->Gamepad.bLeftTrigger = 1; + capabilities->Gamepad.bRightTrigger = 1; + capabilities->Gamepad.sThumbLX = 1; + capabilities->Gamepad.sThumbLY = 1; + capabilities->Gamepad.sThumbRX = 1; + capabilities->Gamepad.sThumbRY = 1; + capabilities->Gamepad.wButtons = 62463; + capabilities->Vibration.wLeftMotorSpeed = 1; + capabilities->Vibration.wRightMotorSpeed = 1; + + return ERROR_SUCCESS; +} + +synxinhk_API LRESULT CALLBACK +HookProc(int code, WPARAM wParam, LPARAM lParam) +{ + return CallNextHookEx(s_hook, code, wParam, lParam); +} + +synxinhk_API BOOL +InstallXInputHook() +{ + if (_stricmp(XINPUT_DLL, REQUIRED_XINPUT_DLL) != 0) + { + LOG("DLL not supported: " << XINPUT_DLL); + return FALSE; + } + + LOG("installing hook"); + s_hook = SetWindowsHookEx(WH_CBT, HookProc, dll, 0); + LOG("hook installed"); + + return TRUE; +} + +synxinhk_API void +RemoveXInputHook() +{ + LOG("removing hook"); + UnhookWindowsHookEx(s_hook); + LOG("hook removed"); +} diff --git a/src/lib/platform/XInputHook.h b/src/lib/platform/XInputHook.h new file mode 100644 index 00000000..1a10cc5e --- /dev/null +++ b/src/lib/platform/XInputHook.h @@ -0,0 +1,43 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 + +#ifdef synxinhk_EXPORTS +#define synxinhk_API __declspec(dllexport) +#else +#define synxinhk_API __declspec(dllimport) +#endif + +synxinhk_API LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam); +synxinhk_API BOOL InstallXInputHook(); +synxinhk_API void RemoveXInputHook(); +synxinhk_API void SetXInputButtons(DWORD userIndex, WORD buttons); +synxinhk_API void SetXInputSticks(DWORD userIndex, SHORT lx, SHORT ly, SHORT rx, SHORT ry); +synxinhk_API void SetXInputTriggers(DWORD userIndex, BYTE left, BYTE right); +synxinhk_API void QueueXInputTimingReq(); +synxinhk_API BOOL DequeueXInputTimingResp(); +synxinhk_API WORD GetXInputFakeFreqMillis(); +synxinhk_API BOOL DequeueXInputFeedback(WORD* leftMotor, WORD* rightMotor); + +#ifdef SYNERGY_EXPORT_XINPUT_HOOKS + +synxinhk_API DWORD WINAPI HookXInputGetState(DWORD dwUserIndex, XINPUT_STATE* pState); +synxinhk_API DWORD WINAPI HookXInputSetState(DWORD dwUserIndex, XINPUT_VIBRATION* pVibration); +synxinhk_API DWORD WINAPI HookXInputGetCapabilities(DWORD userIndex, DWORD flags, XINPUT_CAPABILITIES* capabilities); + +#endif diff --git a/src/lib/platform/XInputProxy13.cpp b/src/lib/platform/XInputProxy13.cpp new file mode 100644 index 00000000..1a864cee --- /dev/null +++ b/src/lib/platform/XInputProxy13.cpp @@ -0,0 +1,72 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#define SYNERGY_EXPORT_XINPUT_HOOKS +#define WIN32_LEAN_AND_MEAN + +#include +#include "XInputProxy13.h" +#include "XInputHook.h" + +#pragma comment(linker, "/EXPORT:XInputGetState=_XInputGetState@8,@2") +#pragma comment(linker, "/EXPORT:XInputSetState=_XInputSetState@8,@3") +#pragma comment(linker, "/EXPORT:XInputGetCapabilities=_XInputGetCapabilities@12,@4") +#pragma comment(linker, "/EXPORT:XInputEnable=_XInputEnable@4,@5") +#pragma comment(linker, "/EXPORT:XInputGetDSoundAudioDeviceGuids=_XInputGetDSoundAudioDeviceGuids@12,@6") +#pragma comment(linker, "/EXPORT:XInputGetBatteryInformation=_XInputGetBatteryInformation@12,@7") +#pragma comment(linker, "/EXPORT:XInputGetKeystroke=_XInputGetKeystroke@12,@8") + +sxinpx13_API DWORD WINAPI +XInputGetState(DWORD dwUserIndex, XINPUT_STATE* pState) +{ + return HookXInputGetState(dwUserIndex, pState); +} + +sxinpx13_API DWORD WINAPI +XInputSetState(DWORD dwUserIndex, XINPUT_VIBRATION* pVibration) +{ + return HookXInputSetState(dwUserIndex, pVibration); +} + +sxinpx13_API DWORD WINAPI +XInputGetCapabilities(DWORD dwUserIndex, DWORD dwFlags, XINPUT_CAPABILITIES* pCapabilities) +{ + return HookXInputGetCapabilities(dwUserIndex, dwFlags, pCapabilities); +} + +sxinpx13_API void WINAPI +XInputEnable(BOOL enable) +{ +} + +sxinpx13_API DWORD WINAPI +XInputGetDSoundAudioDeviceGuids(DWORD dwUserIndex, GUID* pDSoundRenderGuid, GUID* pDSoundCaptureGuid) +{ + return ERROR_DEVICE_NOT_CONNECTED; +} + +sxinpx13_API DWORD WINAPI +XInputGetBatteryInformation(DWORD dwUserIndex, BYTE devType, XINPUT_BATTERY_INFORMATION* pBatteryInformation) +{ + return ERROR_DEVICE_NOT_CONNECTED; +} + +sxinpx13_API DWORD WINAPI +XInputGetKeystroke(DWORD dwUserIndex, DWORD dwReserved, PXINPUT_KEYSTROKE pKeystroke) +{ + return ERROR_DEVICE_NOT_CONNECTED; +} diff --git a/src/lib/platform/XInputProxy13.h b/src/lib/platform/XInputProxy13.h new file mode 100644 index 00000000..1b96cb73 --- /dev/null +++ b/src/lib/platform/XInputProxy13.h @@ -0,0 +1,79 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 + +#ifdef sxinpx13_EXPORTS +#define sxinpx13_API __declspec(dllexport) +#else +#define sxinpx13_API __declspec(dllimport) +#endif + +#include "XInput13.h" + +#ifdef __cplusplus +extern "C" { +#endif + +sxinpx13_API DWORD WINAPI XInputGetState +( + __in DWORD dwUserIndex, // Index of the gamer associated with the device + __out XINPUT_STATE* pState // Receives the current state +); + +sxinpx13_API DWORD WINAPI XInputSetState +( + __in DWORD dwUserIndex, // Index of the gamer associated with the device + __in XINPUT_VIBRATION* pVibration // The vibration information to send to the controller +); + +sxinpx13_API DWORD WINAPI XInputGetCapabilities +( + __in DWORD dwUserIndex, // Index of the gamer associated with the device + __in DWORD dwFlags, // Input flags that identify the device type + __out XINPUT_CAPABILITIES* pCapabilities // Receives the capabilities +); + +sxinpx13_API void WINAPI XInputEnable +( + __in BOOL enable // [in] Indicates whether xinput is enabled or disabled. +); + +sxinpx13_API DWORD WINAPI XInputGetDSoundAudioDeviceGuids +( + __in DWORD dwUserIndex, // Index of the gamer associated with the device + __out GUID* pDSoundRenderGuid, // DSound device ID for render + __out GUID* pDSoundCaptureGuid // DSound device ID for capture +); + +sxinpx13_API DWORD WINAPI XInputGetBatteryInformation +( + __in DWORD dwUserIndex, // Index of the gamer associated with the device + __in BYTE devType, // Which device on this user index + __out XINPUT_BATTERY_INFORMATION* pBatteryInformation // Contains the level and types of batteries +); + +sxinpx13_API DWORD WINAPI XInputGetKeystroke +( + __in DWORD dwUserIndex, // Index of the gamer associated with the device + __reserved DWORD dwReserved, // Reserved for future use + __out PXINPUT_KEYSTROKE pKeystroke // Pointer to an XINPUT_KEYSTROKE structure that receives an input event. +); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/src/lib/server/CBaseClientProxy.cpp b/src/lib/server/CBaseClientProxy.cpp new file mode 100644 index 00000000..f23cc6be --- /dev/null +++ b/src/lib/server/CBaseClientProxy.cpp @@ -0,0 +1,55 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2006 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CBaseClientProxy.h" + +// +// CBaseClientProxy +// + +CBaseClientProxy::CBaseClientProxy(const CString& name) : + m_name(name), + m_x(0), + m_y(0) +{ + // do nothing +} + +CBaseClientProxy::~CBaseClientProxy() +{ + // do nothing +} + +void +CBaseClientProxy::setJumpCursorPos(SInt32 x, SInt32 y) +{ + m_x = x; + m_y = y; +} + +void +CBaseClientProxy::getJumpCursorPos(SInt32& x, SInt32& y) const +{ + x = m_x; + y = m_y; +} + +CString +CBaseClientProxy::getName() const +{ + return m_name; +} diff --git a/src/lib/server/CBaseClientProxy.h b/src/lib/server/CBaseClientProxy.h new file mode 100644 index 00000000..4ad2e6a2 --- /dev/null +++ b/src/lib/server/CBaseClientProxy.h @@ -0,0 +1,92 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CBASECLIENTPROXY_H +#define CBASECLIENTPROXY_H + +#include "IClient.h" +#include "CString.h" + +//! Generic proxy for client or primary +class CBaseClientProxy : public IClient { +public: + /*! + \c name is the name of the client. + */ + CBaseClientProxy(const CString& name); + ~CBaseClientProxy(); + + //! @name manipulators + //@{ + + //! Save cursor position + /*! + Save the position of the cursor when jumping from client. + */ + void setJumpCursorPos(SInt32 x, SInt32 y); + + //@} + //! @name accessors + //@{ + + //! Get cursor position + /*! + Get the position of the cursor when last jumping from client. + */ + void getJumpCursorPos(SInt32& x, SInt32& y) const; + + //@} + + // IScreen + virtual void* getEventTarget() const = 0; + virtual bool getClipboard(ClipboardID id, IClipboard*) const = 0; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const = 0; + virtual void getCursorPos(SInt32& x, SInt32& y) const = 0; + + // IClient overrides + virtual void enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, + bool forScreensaver) = 0; + virtual bool leave() = 0; + virtual void setClipboard(ClipboardID, const IClipboard*) = 0; + virtual void grabClipboard(ClipboardID) = 0; + virtual void setClipboardDirty(ClipboardID, bool) = 0; + virtual void keyDown(KeyID, KeyModifierMask, KeyButton) = 0; + virtual void keyRepeat(KeyID, KeyModifierMask, + SInt32 count, KeyButton) = 0; + virtual void keyUp(KeyID, KeyModifierMask, KeyButton) = 0; + virtual void mouseDown(ButtonID) = 0; + virtual void mouseUp(ButtonID) = 0; + virtual void mouseMove(SInt32 xAbs, SInt32 yAbs) = 0; + virtual void mouseRelativeMove(SInt32 xRel, SInt32 yRel) = 0; + virtual void mouseWheel(SInt32 xDelta, SInt32 yDelta) = 0; + virtual void gameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) = 0; + virtual void gameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) = 0; + virtual void gameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) = 0; + virtual void gameDeviceTimingReq() = 0; + virtual void screensaver(bool activate) = 0; + virtual void resetOptions() = 0; + virtual void setOptions(const COptionsList& options) = 0; + virtual CString getName() const; + +private: + CString m_name; + SInt32 m_x, m_y; +}; + +#endif diff --git a/src/lib/server/CClientListener.cpp b/src/lib/server/CClientListener.cpp new file mode 100644 index 00000000..5defd907 --- /dev/null +++ b/src/lib/server/CClientListener.cpp @@ -0,0 +1,207 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClientListener.h" +#include "CClientProxy.h" +#include "CClientProxyUnknown.h" +#include "CPacketStreamFilter.h" +#include "IStreamFilterFactory.h" +#include "IDataSocket.h" +#include "IListenSocket.h" +#include "ISocketFactory.h" +#include "XSocket.h" +#include "CLog.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" + +// +// CClientListener +// + +CEvent::Type CClientListener::s_connectedEvent = CEvent::kUnknown; + +CClientListener::CClientListener(const CNetworkAddress& address, + ISocketFactory* socketFactory, + IStreamFilterFactory* streamFilterFactory) : + m_socketFactory(socketFactory), + m_streamFilterFactory(streamFilterFactory), + m_server(NULL) +{ + assert(m_socketFactory != NULL); + + try { + // create listen socket + m_listen = m_socketFactory->createListen(); + + // bind listen address + LOG((CLOG_DEBUG1 "binding listen socket")); + m_listen->bind(address); + } + catch (XSocketAddressInUse&) { + delete m_listen; + delete m_socketFactory; + delete m_streamFilterFactory; + throw; + } + catch (XBase&) { + delete m_listen; + delete m_socketFactory; + delete m_streamFilterFactory; + throw; + } + LOG((CLOG_DEBUG1 "listening for clients")); + + // setup event handler + EVENTQUEUE->adoptHandler(IListenSocket::getConnectingEvent(), m_listen, + new TMethodEventJob(this, + &CClientListener::handleClientConnecting)); +} + +CClientListener::~CClientListener() +{ + LOG((CLOG_DEBUG1 "stop listening for clients")); + + // discard already connected clients + for (CNewClients::iterator index = m_newClients.begin(); + index != m_newClients.end(); ++index) { + CClientProxyUnknown* client = *index; + EVENTQUEUE->removeHandler( + CClientProxyUnknown::getSuccessEvent(), client); + EVENTQUEUE->removeHandler( + CClientProxyUnknown::getFailureEvent(), client); + EVENTQUEUE->removeHandler( + CClientProxy::getDisconnectedEvent(), client); + delete client; + } + + // discard waiting clients + CClientProxy* client = getNextClient(); + while (client != NULL) { + delete client; + client = getNextClient(); + } + + EVENTQUEUE->removeHandler(IListenSocket::getConnectingEvent(), m_listen); + delete m_listen; + delete m_socketFactory; + delete m_streamFilterFactory; +} + +void +CClientListener::setServer(CServer* server) +{ + assert(server != NULL); + m_server = server; +} + +CClientProxy* +CClientListener::getNextClient() +{ + CClientProxy* client = NULL; + if (!m_waitingClients.empty()) { + client = m_waitingClients.front(); + m_waitingClients.pop_front(); + EVENTQUEUE->removeHandler(CClientProxy::getDisconnectedEvent(), client); + } + return client; +} + +CEvent::Type +CClientListener::getConnectedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_connectedEvent, + "CClientListener::connected"); +} + +void +CClientListener::handleClientConnecting(const CEvent&, void*) +{ + // accept client connection + IStream* stream = m_listen->accept(); + if (stream == NULL) { + return; + } + LOG((CLOG_NOTE "accepted client connection")); + + // filter socket messages, including a packetizing filter + if (m_streamFilterFactory != NULL) { + stream = m_streamFilterFactory->create(stream, true); + } + stream = new CPacketStreamFilter(stream, true); + + assert(m_server != NULL); + + // create proxy for unknown client + CClientProxyUnknown* client = new CClientProxyUnknown(stream, 30.0, m_server); + m_newClients.insert(client); + + // watch for events from unknown client + EVENTQUEUE->adoptHandler(CClientProxyUnknown::getSuccessEvent(), client, + new TMethodEventJob(this, + &CClientListener::handleUnknownClient, client)); + EVENTQUEUE->adoptHandler(CClientProxyUnknown::getFailureEvent(), client, + new TMethodEventJob(this, + &CClientListener::handleUnknownClient, client)); +} + +void +CClientListener::handleUnknownClient(const CEvent&, void* vclient) +{ + CClientProxyUnknown* unknownClient = + reinterpret_cast(vclient); + + // we should have the client in our new client list + assert(m_newClients.count(unknownClient) == 1); + + // get the real client proxy and install it + CClientProxy* client = unknownClient->orphanClientProxy(); + if (client != NULL) { + // handshake was successful + m_waitingClients.push_back(client); + EVENTQUEUE->addEvent(CEvent(getConnectedEvent(), this)); + + // watch for client to disconnect while it's in our queue + EVENTQUEUE->adoptHandler(CClientProxy::getDisconnectedEvent(), client, + new TMethodEventJob(this, + &CClientListener::handleClientDisconnected, + client)); + } + + // now finished with unknown client + EVENTQUEUE->removeHandler(CClientProxyUnknown::getSuccessEvent(), client); + EVENTQUEUE->removeHandler(CClientProxyUnknown::getFailureEvent(), client); + m_newClients.erase(unknownClient); + delete unknownClient; +} + +void +CClientListener::handleClientDisconnected(const CEvent&, void* vclient) +{ + CClientProxy* client = reinterpret_cast(vclient); + + // find client in waiting clients queue + for (CWaitingClients::iterator i = m_waitingClients.begin(), + n = m_waitingClients.end(); i != n; ++i) { + if (*i == client) { + m_waitingClients.erase(i); + EVENTQUEUE->removeHandler(CClientProxy::getDisconnectedEvent(), + client); + delete client; + break; + } + } +} diff --git a/src/lib/server/CClientListener.h b/src/lib/server/CClientListener.h new file mode 100644 index 00000000..4a0bf676 --- /dev/null +++ b/src/lib/server/CClientListener.h @@ -0,0 +1,88 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CCLIENTLISTENER_H +#define CCLIENTLISTENER_H + +#include "CConfig.h" +#include "CEvent.h" +#include "stddeque.h" +#include "stdset.h" + +class CClientProxy; +class CClientProxyUnknown; +class CNetworkAddress; +class IListenSocket; +class ISocketFactory; +class IStreamFilterFactory; +class CServer; + +class CClientListener { +public: + // The factories are adopted. + CClientListener(const CNetworkAddress&, + ISocketFactory*, IStreamFilterFactory*); + ~CClientListener(); + + //! @name manipulators + //@{ + + void setServer(CServer* server); + + //@} + + //! @name accessors + //@{ + + //! Get next connected client + /*! + Returns the next connected client and removes it from the internal + list. The client is responsible for deleting the returned client. + Returns NULL if no clients are available. + */ + CClientProxy* getNextClient(); + + //! Get connected event type + /*! + Returns the connected event type. This is sent whenever a + a client connects. + */ + static CEvent::Type getConnectedEvent(); + + //@} + +private: + // client connection event handlers + void handleClientConnecting(const CEvent&, void*); + void handleUnknownClient(const CEvent&, void*); + void handleClientDisconnected(const CEvent&, void*); + +private: + typedef std::set CNewClients; + typedef std::deque CWaitingClients; + + IListenSocket* m_listen; + ISocketFactory* m_socketFactory; + IStreamFilterFactory* m_streamFilterFactory; + CNewClients m_newClients; + CWaitingClients m_waitingClients; + + static CEvent::Type s_connectedEvent; + CServer* m_server; +}; + +#endif diff --git a/src/lib/server/CClientProxy.cpp b/src/lib/server/CClientProxy.cpp new file mode 100644 index 00000000..713c8757 --- /dev/null +++ b/src/lib/server/CClientProxy.cpp @@ -0,0 +1,93 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClientProxy.h" +#include "CProtocolUtil.h" +#include "IStream.h" +#include "CLog.h" +#include "CEventQueue.h" + +// +// CClientProxy +// + +CEvent::Type CClientProxy::s_readyEvent = CEvent::kUnknown; +CEvent::Type CClientProxy::s_disconnectedEvent = CEvent::kUnknown; +CEvent::Type CClientProxy::s_clipboardChangedEvent= CEvent::kUnknown; +CEvent::Type CClientProxy::s_gameDeviceTimingRecvEvent= CEvent::kUnknown; + +CClientProxy::CClientProxy(const CString& name, IStream* stream) : + CBaseClientProxy(name), + m_stream(stream) +{ + // do nothing +} + +CClientProxy::~CClientProxy() +{ + delete m_stream; +} + +void +CClientProxy::close(const char* msg) +{ + LOG((CLOG_DEBUG1 "send close \"%s\" to \"%s\"", msg, getName().c_str())); + CProtocolUtil::writef(getStream(), msg); + + // force the close to be sent before we return + getStream()->flush(); +} + +IStream* +CClientProxy::getStream() const +{ + return m_stream; +} + +CEvent::Type +CClientProxy::getReadyEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_readyEvent, + "CClientProxy::ready"); +} + +CEvent::Type +CClientProxy::getDisconnectedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_disconnectedEvent, + "CClientProxy::disconnected"); +} + +CEvent::Type +CClientProxy::getClipboardChangedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_clipboardChangedEvent, + "CClientProxy::clipboardChanged"); +} + +CEvent::Type +CClientProxy::getGameDeviceTimingRespEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_gameDeviceTimingRecvEvent, + "CClientProxy::gameDeviceTimingRecv"); +} + +void* +CClientProxy::getEventTarget() const +{ + return static_cast(const_cast(this)); +} diff --git a/src/lib/server/CClientProxy.h b/src/lib/server/CClientProxy.h new file mode 100644 index 00000000..95a0dad4 --- /dev/null +++ b/src/lib/server/CClientProxy.h @@ -0,0 +1,128 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CCLIENTPROXY_H +#define CCLIENTPROXY_H + +#include "CBaseClientProxy.h" +#include "CEvent.h" +#include "CString.h" + +class IStream; + +//! Generic proxy for client +class CClientProxy : public CBaseClientProxy { +public: + /*! + \c name is the name of the client. + */ + CClientProxy(const CString& name, IStream* adoptedStream); + ~CClientProxy(); + + //! @name manipulators + //@{ + + //! Disconnect + /*! + Ask the client to disconnect, using \p msg as the reason. + */ + void close(const char* msg); + + //@} + //! @name accessors + //@{ + + //! Get stream + /*! + Returns the stream passed to the c'tor. + */ + IStream* getStream() const; + + //! Get ready event type + /*! + Returns the ready event type. This is sent when the client has + completed the initial handshake. Until it is sent, the client is + not fully connected. + */ + static CEvent::Type getReadyEvent(); + + //! Get disconnect event type + /*! + Returns the disconnect event type. This is sent when the client + disconnects or is disconnected. The target is getEventTarget(). + */ + static CEvent::Type getDisconnectedEvent(); + + //! Get clipboard changed event type + /*! + Returns the clipboard changed event type. This is sent whenever the + contents of the clipboard has changed. The data is a pointer to a + IScreen::CClipboardInfo. + */ + static CEvent::Type getClipboardChangedEvent(); + + //! Get game device timing receive event type + /*! + Returns the game device timing receive event type. This is set + whenever the server receives to a timing event response from a client. + */ + static CEvent::Type getGameDeviceTimingRespEvent(); + + //@} + + // IScreen + virtual void* getEventTarget() const; + virtual bool getClipboard(ClipboardID id, IClipboard*) const = 0; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const = 0; + virtual void getCursorPos(SInt32& x, SInt32& y) const = 0; + + // IClient overrides + virtual void enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, + bool forScreensaver) = 0; + virtual bool leave() = 0; + virtual void setClipboard(ClipboardID, const IClipboard*) = 0; + virtual void grabClipboard(ClipboardID) = 0; + virtual void setClipboardDirty(ClipboardID, bool) = 0; + virtual void keyDown(KeyID, KeyModifierMask, KeyButton) = 0; + virtual void keyRepeat(KeyID, KeyModifierMask, + SInt32 count, KeyButton) = 0; + virtual void keyUp(KeyID, KeyModifierMask, KeyButton) = 0; + virtual void mouseDown(ButtonID) = 0; + virtual void mouseUp(ButtonID) = 0; + virtual void mouseMove(SInt32 xAbs, SInt32 yAbs) = 0; + virtual void mouseRelativeMove(SInt32 xRel, SInt32 yRel) = 0; + virtual void mouseWheel(SInt32 xDelta, SInt32 yDelta) = 0; + virtual void screensaver(bool activate) = 0; + virtual void resetOptions() = 0; + virtual void setOptions(const COptionsList& options) = 0; + virtual void gameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) = 0; + virtual void gameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) = 0; + virtual void gameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) = 0; + virtual void gameDeviceTimingReq() = 0; + +private: + IStream* m_stream; + + static CEvent::Type s_readyEvent; + static CEvent::Type s_disconnectedEvent; + static CEvent::Type s_clipboardChangedEvent; + static CEvent::Type s_gameDeviceTimingRecvEvent; +}; + +#endif diff --git a/src/lib/server/CClientProxy1_0.cpp b/src/lib/server/CClientProxy1_0.cpp new file mode 100644 index 00000000..d7d3cf83 --- /dev/null +++ b/src/lib/server/CClientProxy1_0.cpp @@ -0,0 +1,529 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClientProxy1_0.h" +#include "CProtocolUtil.h" +#include "XSynergy.h" +#include "IStream.h" +#include "CLog.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" +#include + +// +// CClientProxy1_0 +// + +CClientProxy1_0::CClientProxy1_0(const CString& name, IStream* stream) : + CClientProxy(name, stream), + m_heartbeatTimer(NULL), + m_parser(&CClientProxy1_0::parseHandshakeMessage) +{ + // install event handlers + EVENTQUEUE->adoptHandler(stream->getInputReadyEvent(), + stream->getEventTarget(), + new TMethodEventJob(this, + &CClientProxy1_0::handleData, NULL)); + EVENTQUEUE->adoptHandler(stream->getOutputErrorEvent(), + stream->getEventTarget(), + new TMethodEventJob(this, + &CClientProxy1_0::handleWriteError, NULL)); + EVENTQUEUE->adoptHandler(stream->getInputShutdownEvent(), + stream->getEventTarget(), + new TMethodEventJob(this, + &CClientProxy1_0::handleDisconnect, NULL)); + EVENTQUEUE->adoptHandler(stream->getOutputShutdownEvent(), + stream->getEventTarget(), + new TMethodEventJob(this, + &CClientProxy1_0::handleWriteError, NULL)); + EVENTQUEUE->adoptHandler(CEvent::kTimer, this, + new TMethodEventJob(this, + &CClientProxy1_0::handleFlatline, NULL)); + + setHeartbeatRate(kHeartRate, kHeartRate * kHeartBeatsUntilDeath); + + LOG((CLOG_DEBUG1 "querying client \"%s\" info", getName().c_str())); + CProtocolUtil::writef(getStream(), kMsgQInfo); +} + +CClientProxy1_0::~CClientProxy1_0() +{ + removeHandlers(); +} + +void +CClientProxy1_0::disconnect() +{ + removeHandlers(); + getStream()->close(); + EVENTQUEUE->addEvent(CEvent(getDisconnectedEvent(), getEventTarget())); +} + +void +CClientProxy1_0::removeHandlers() +{ + // uninstall event handlers + EVENTQUEUE->removeHandler(getStream()->getInputReadyEvent(), + getStream()->getEventTarget()); + EVENTQUEUE->removeHandler(getStream()->getOutputErrorEvent(), + getStream()->getEventTarget()); + EVENTQUEUE->removeHandler(getStream()->getInputShutdownEvent(), + getStream()->getEventTarget()); + EVENTQUEUE->removeHandler(getStream()->getOutputShutdownEvent(), + getStream()->getEventTarget()); + EVENTQUEUE->removeHandler(CEvent::kTimer, this); + + // remove timer + removeHeartbeatTimer(); +} + +void +CClientProxy1_0::addHeartbeatTimer() +{ + if (m_heartbeatAlarm > 0.0) { + m_heartbeatTimer = EVENTQUEUE->newOneShotTimer(m_heartbeatAlarm, this); + } +} + +void +CClientProxy1_0::removeHeartbeatTimer() +{ + if (m_heartbeatTimer != NULL) { + EVENTQUEUE->deleteTimer(m_heartbeatTimer); + m_heartbeatTimer = NULL; + } +} + +void +CClientProxy1_0::resetHeartbeatTimer() +{ + // reset the alarm + removeHeartbeatTimer(); + addHeartbeatTimer(); +} + +void +CClientProxy1_0::resetHeartbeatRate() +{ + setHeartbeatRate(kHeartRate, kHeartRate * kHeartBeatsUntilDeath); +} + +void +CClientProxy1_0::setHeartbeatRate(double, double alarm) +{ + m_heartbeatAlarm = alarm; +} + +void +CClientProxy1_0::handleData(const CEvent&, void*) +{ + // handle messages until there are no more. first read message code. + UInt8 code[4]; + UInt32 n = getStream()->read(code, 4); + while (n != 0) { + // verify we got an entire code + if (n != 4) { + LOG((CLOG_ERR "incomplete message from \"%s\": %d bytes", getName().c_str(), n)); + disconnect(); + return; + } + + // parse message + LOG((CLOG_DEBUG2 "msg from \"%s\": %c%c%c%c", getName().c_str(), code[0], code[1], code[2], code[3])); + if (!(this->*m_parser)(code)) { + LOG((CLOG_ERR "invalid message from client \"%s\": %c%c%c%c", getName().c_str(), code[0], code[1], code[2], code[3])); + disconnect(); + return; + } + + // next message + n = getStream()->read(code, 4); + } + + // restart heartbeat timer + resetHeartbeatTimer(); +} + +bool +CClientProxy1_0::parseHandshakeMessage(const UInt8* code) +{ + if (memcmp(code, kMsgCNoop, 4) == 0) { + // discard no-ops + LOG((CLOG_DEBUG2 "no-op from", getName().c_str())); + return true; + } + else if (memcmp(code, kMsgDInfo, 4) == 0) { + // future messages get parsed by parseMessage + m_parser = &CClientProxy1_0::parseMessage; + if (recvInfo()) { + EVENTQUEUE->addEvent(CEvent(getReadyEvent(), getEventTarget())); + addHeartbeatTimer(); + return true; + } + } + return false; +} + +bool +CClientProxy1_0::parseMessage(const UInt8* code) +{ + if (memcmp(code, kMsgDInfo, 4) == 0) { + if (recvInfo()) { + EVENTQUEUE->addEvent( + CEvent(getShapeChangedEvent(), getEventTarget())); + return true; + } + return false; + } + else if (memcmp(code, kMsgCNoop, 4) == 0) { + // discard no-ops + LOG((CLOG_DEBUG2 "no-op from", getName().c_str())); + return true; + } + else if (memcmp(code, kMsgCClipboard, 4) == 0) { + return recvGrabClipboard(); + } + else if (memcmp(code, kMsgDClipboard, 4) == 0) { + return recvClipboard(); + } + return false; +} + +void +CClientProxy1_0::handleDisconnect(const CEvent&, void*) +{ + LOG((CLOG_NOTE "client \"%s\" has disconnected", getName().c_str())); + disconnect(); +} + +void +CClientProxy1_0::handleWriteError(const CEvent&, void*) +{ + LOG((CLOG_WARN "error writing to client \"%s\"", getName().c_str())); + disconnect(); +} + +void +CClientProxy1_0::handleFlatline(const CEvent&, void*) +{ + // didn't get a heartbeat fast enough. assume client is dead. + LOG((CLOG_NOTE "client \"%s\" is dead", getName().c_str())); + disconnect(); +} + +bool +CClientProxy1_0::getClipboard(ClipboardID id, IClipboard* clipboard) const +{ + CClipboard::copy(clipboard, &m_clipboard[id].m_clipboard); + return true; +} + +void +CClientProxy1_0::getShape(SInt32& x, SInt32& y, SInt32& w, SInt32& h) const +{ + x = m_info.m_x; + y = m_info.m_y; + w = m_info.m_w; + h = m_info.m_h; +} + +void +CClientProxy1_0::getCursorPos(SInt32& x, SInt32& y) const +{ + // note -- this returns the cursor pos from when we last got client info + x = m_info.m_mx; + y = m_info.m_my; +} + +void +CClientProxy1_0::enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, bool) +{ + LOG((CLOG_DEBUG1 "send enter to \"%s\", %d,%d %d %04x", getName().c_str(), xAbs, yAbs, seqNum, mask)); + CProtocolUtil::writef(getStream(), kMsgCEnter, + xAbs, yAbs, seqNum, mask); +} + +bool +CClientProxy1_0::leave() +{ + LOG((CLOG_DEBUG1 "send leave to \"%s\"", getName().c_str())); + CProtocolUtil::writef(getStream(), kMsgCLeave); + + // we can never prevent the user from leaving + return true; +} + +void +CClientProxy1_0::setClipboard(ClipboardID id, const IClipboard* clipboard) +{ + // ignore if this clipboard is already clean + if (m_clipboard[id].m_dirty) { + // this clipboard is now clean + m_clipboard[id].m_dirty = false; + CClipboard::copy(&m_clipboard[id].m_clipboard, clipboard); + + CString data = m_clipboard[id].m_clipboard.marshall(); + LOG((CLOG_DEBUG "send clipboard %d to \"%s\" size=%d", id, getName().c_str(), data.size())); + CProtocolUtil::writef(getStream(), kMsgDClipboard, id, 0, &data); + } +} + +void +CClientProxy1_0::grabClipboard(ClipboardID id) +{ + LOG((CLOG_DEBUG "send grab clipboard %d to \"%s\"", id, getName().c_str())); + CProtocolUtil::writef(getStream(), kMsgCClipboard, id, 0); + + // this clipboard is now dirty + m_clipboard[id].m_dirty = true; +} + +void +CClientProxy1_0::setClipboardDirty(ClipboardID id, bool dirty) +{ + m_clipboard[id].m_dirty = dirty; +} + +void +CClientProxy1_0::keyDown(KeyID key, KeyModifierMask mask, KeyButton) +{ + LOG((CLOG_DEBUG1 "send key down to \"%s\" id=%d, mask=0x%04x", getName().c_str(), key, mask)); + CProtocolUtil::writef(getStream(), kMsgDKeyDown1_0, key, mask); +} + +void +CClientProxy1_0::keyRepeat(KeyID key, KeyModifierMask mask, + SInt32 count, KeyButton) +{ + LOG((CLOG_DEBUG1 "send key repeat to \"%s\" id=%d, mask=0x%04x, count=%d", getName().c_str(), key, mask, count)); + CProtocolUtil::writef(getStream(), kMsgDKeyRepeat1_0, key, mask, count); +} + +void +CClientProxy1_0::keyUp(KeyID key, KeyModifierMask mask, KeyButton) +{ + LOG((CLOG_DEBUG1 "send key up to \"%s\" id=%d, mask=0x%04x", getName().c_str(), key, mask)); + CProtocolUtil::writef(getStream(), kMsgDKeyUp1_0, key, mask); +} + +void +CClientProxy1_0::mouseDown(ButtonID button) +{ + LOG((CLOG_DEBUG1 "send mouse down to \"%s\" id=%d", getName().c_str(), button)); + CProtocolUtil::writef(getStream(), kMsgDMouseDown, button); +} + +void +CClientProxy1_0::mouseUp(ButtonID button) +{ + LOG((CLOG_DEBUG1 "send mouse up to \"%s\" id=%d", getName().c_str(), button)); + CProtocolUtil::writef(getStream(), kMsgDMouseUp, button); +} + +void +CClientProxy1_0::mouseMove(SInt32 xAbs, SInt32 yAbs) +{ + LOG((CLOG_DEBUG2 "send mouse move to \"%s\" %d,%d", getName().c_str(), xAbs, yAbs)); + CProtocolUtil::writef(getStream(), kMsgDMouseMove, xAbs, yAbs); +} + +void +CClientProxy1_0::mouseRelativeMove(SInt32, SInt32) +{ + // ignore -- not supported in protocol 1.0 +} + +void +CClientProxy1_0::mouseWheel(SInt32, SInt32 yDelta) +{ + // clients prior to 1.3 only support the y axis + LOG((CLOG_DEBUG2 "send mouse wheel to \"%s\" %+d", getName().c_str(), yDelta)); + CProtocolUtil::writef(getStream(), kMsgDMouseWheel1_0, yDelta); +} + +void +CClientProxy1_0::gameDeviceButtons(GameDeviceID, GameDeviceButton) +{ + // ignore -- not supported in protocol 1.0 + LOG((CLOG_DEBUG "gameDeviceButtons not supported")); +} + +void +CClientProxy1_0::gameDeviceSticks(GameDeviceID, SInt16, SInt16, SInt16, SInt16) +{ + // ignore -- not supported in protocol 1.0 + LOG((CLOG_DEBUG "gameDeviceSticks not supported")); +} + +void +CClientProxy1_0::gameDeviceTriggers(GameDeviceID, UInt8, UInt8) +{ + // ignore -- not supported in protocol 1.0 + LOG((CLOG_DEBUG "gameDeviceTriggers not supported")); +} + +void +CClientProxy1_0::gameDeviceTimingReq() +{ + // ignore -- not supported in protocol 1.0 + LOG((CLOG_DEBUG "gameDeviceTimingReq not supported")); +} + +void +CClientProxy1_0::screensaver(bool on) +{ + LOG((CLOG_DEBUG1 "send screen saver to \"%s\" on=%d", getName().c_str(), on ? 1 : 0)); + CProtocolUtil::writef(getStream(), kMsgCScreenSaver, on ? 1 : 0); +} + +void +CClientProxy1_0::resetOptions() +{ + LOG((CLOG_DEBUG1 "send reset options to \"%s\"", getName().c_str())); + CProtocolUtil::writef(getStream(), kMsgCResetOptions); + + // reset heart rate and death + resetHeartbeatRate(); + removeHeartbeatTimer(); + addHeartbeatTimer(); +} + +void +CClientProxy1_0::setOptions(const COptionsList& options) +{ + LOG((CLOG_DEBUG1 "send set options to \"%s\" size=%d", getName().c_str(), options.size())); + CProtocolUtil::writef(getStream(), kMsgDSetOptions, &options); + + // check options + for (UInt32 i = 0, n = (UInt32)options.size(); i < n; i += 2) { + if (options[i] == kOptionHeartbeat) { + double rate = 1.0e-3 * static_cast(options[i + 1]); + if (rate <= 0.0) { + rate = -1.0; + } + setHeartbeatRate(rate, rate * kHeartBeatsUntilDeath); + removeHeartbeatTimer(); + addHeartbeatTimer(); + } + } +} + +bool +CClientProxy1_0::recvInfo() +{ + // parse the message + SInt16 x, y, w, h, dummy1, mx, my; + if (!CProtocolUtil::readf(getStream(), kMsgDInfo + 4, + &x, &y, &w, &h, &dummy1, &mx, &my)) { + return false; + } + LOG((CLOG_DEBUG "received client \"%s\" info shape=%d,%d %dx%d at %d,%d", getName().c_str(), x, y, w, h, mx, my)); + + // validate + if (w <= 0 || h <= 0) { + return false; + } + if (mx < x || mx >= x + w || my < y || my >= y + h) { + mx = x + w / 2; + my = y + h / 2; + } + + // save + m_info.m_x = x; + m_info.m_y = y; + m_info.m_w = w; + m_info.m_h = h; + m_info.m_mx = mx; + m_info.m_my = my; + + // acknowledge receipt + LOG((CLOG_DEBUG1 "send info ack to \"%s\"", getName().c_str())); + CProtocolUtil::writef(getStream(), kMsgCInfoAck); + return true; +} + +bool +CClientProxy1_0::recvClipboard() +{ + // parse message + ClipboardID id; + UInt32 seqNum; + CString data; + if (!CProtocolUtil::readf(getStream(), + kMsgDClipboard + 4, &id, &seqNum, &data)) { + return false; + } + LOG((CLOG_DEBUG "received client \"%s\" clipboard %d seqnum=%d, size=%d", getName().c_str(), id, seqNum, data.size())); + + // validate + if (id >= kClipboardEnd) { + return false; + } + + // save clipboard + m_clipboard[id].m_clipboard.unmarshall(data, 0); + m_clipboard[id].m_sequenceNumber = seqNum; + + // notify + CClipboardInfo* info = new CClipboardInfo; + info->m_id = id; + info->m_sequenceNumber = seqNum; + EVENTQUEUE->addEvent(CEvent(getClipboardChangedEvent(), + getEventTarget(), info)); + + return true; +} + +bool +CClientProxy1_0::recvGrabClipboard() +{ + // parse message + ClipboardID id; + UInt32 seqNum; + if (!CProtocolUtil::readf(getStream(), kMsgCClipboard + 4, &id, &seqNum)) { + return false; + } + LOG((CLOG_DEBUG "received client \"%s\" grabbed clipboard %d seqnum=%d", getName().c_str(), id, seqNum)); + + // validate + if (id >= kClipboardEnd) { + return false; + } + + // notify + CClipboardInfo* info = new CClipboardInfo; + info->m_id = id; + info->m_sequenceNumber = seqNum; + EVENTQUEUE->addEvent(CEvent(getClipboardGrabbedEvent(), + getEventTarget(), info)); + + return true; +} + + +// +// CClientProxy1_0::CClientClipboard +// + +CClientProxy1_0::CClientClipboard::CClientClipboard() : + m_clipboard(), + m_sequenceNumber(0), + m_dirty(true) +{ + // do nothing +} diff --git a/src/lib/server/CClientProxy1_0.h b/src/lib/server/CClientProxy1_0.h new file mode 100644 index 00000000..b60e7113 --- /dev/null +++ b/src/lib/server/CClientProxy1_0.h @@ -0,0 +1,107 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CCLIENTPROXY1_0_H +#define CCLIENTPROXY1_0_H + +#include "CClientProxy.h" +#include "CClipboard.h" +#include "ProtocolTypes.h" + +class CEvent; +class CEventQueueTimer; + +//! Proxy for client implementing protocol version 1.0 +class CClientProxy1_0 : public CClientProxy { +public: + CClientProxy1_0(const CString& name, IStream* adoptedStream); + ~CClientProxy1_0(); + + // IScreen + virtual bool getClipboard(ClipboardID id, IClipboard*) const; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const; + virtual void getCursorPos(SInt32& x, SInt32& y) const; + + // IClient overrides + virtual void enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, + bool forScreensaver); + virtual bool leave(); + virtual void setClipboard(ClipboardID, const IClipboard*); + virtual void grabClipboard(ClipboardID); + virtual void setClipboardDirty(ClipboardID, bool); + virtual void keyDown(KeyID, KeyModifierMask, KeyButton); + virtual void keyRepeat(KeyID, KeyModifierMask, + SInt32 count, KeyButton); + virtual void keyUp(KeyID, KeyModifierMask, KeyButton); + virtual void mouseDown(ButtonID); + virtual void mouseUp(ButtonID); + virtual void mouseMove(SInt32 xAbs, SInt32 yAbs); + virtual void mouseRelativeMove(SInt32 xRel, SInt32 yRel); + virtual void mouseWheel(SInt32 xDelta, SInt32 yDelta); + virtual void screensaver(bool activate); + virtual void resetOptions(); + virtual void setOptions(const COptionsList& options); + virtual void gameDeviceButtons(GameDeviceID id, GameDeviceButton buttons); + virtual void gameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2); + virtual void gameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2); + virtual void gameDeviceTimingReq(); + +protected: + virtual bool parseHandshakeMessage(const UInt8* code); + virtual bool parseMessage(const UInt8* code); + + virtual void resetHeartbeatRate(); + virtual void setHeartbeatRate(double rate, double alarm); + virtual void resetHeartbeatTimer(); + virtual void addHeartbeatTimer(); + virtual void removeHeartbeatTimer(); + +private: + void disconnect(); + void removeHandlers(); + + void handleData(const CEvent&, void*); + void handleDisconnect(const CEvent&, void*); + void handleWriteError(const CEvent&, void*); + void handleFlatline(const CEvent&, void*); + + bool recvInfo(); + bool recvClipboard(); + bool recvGrabClipboard(); + +private: + typedef bool (CClientProxy1_0::*MessageParser)(const UInt8*); + struct CClientClipboard { + public: + CClientClipboard(); + + public: + CClipboard m_clipboard; + UInt32 m_sequenceNumber; + bool m_dirty; + }; + + CClientInfo m_info; + CClientClipboard m_clipboard[kClipboardEnd]; + double m_heartbeatAlarm; + CEventQueueTimer* m_heartbeatTimer; + MessageParser m_parser; +}; + +#endif diff --git a/src/lib/server/CClientProxy1_1.cpp b/src/lib/server/CClientProxy1_1.cpp new file mode 100644 index 00000000..e0785032 --- /dev/null +++ b/src/lib/server/CClientProxy1_1.cpp @@ -0,0 +1,58 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClientProxy1_1.h" +#include "CProtocolUtil.h" +#include "CLog.h" +#include + +// +// CClientProxy1_1 +// + +CClientProxy1_1::CClientProxy1_1(const CString& name, IStream* stream) : + CClientProxy1_0(name, stream) +{ + // do nothing +} + +CClientProxy1_1::~CClientProxy1_1() +{ + // do nothing +} + +void +CClientProxy1_1::keyDown(KeyID key, KeyModifierMask mask, KeyButton button) +{ + LOG((CLOG_DEBUG1 "send key down to \"%s\" id=%d, mask=0x%04x, button=0x%04x", getName().c_str(), key, mask, button)); + CProtocolUtil::writef(getStream(), kMsgDKeyDown, key, mask, button); +} + +void +CClientProxy1_1::keyRepeat(KeyID key, KeyModifierMask mask, + SInt32 count, KeyButton button) +{ + LOG((CLOG_DEBUG1 "send key repeat to \"%s\" id=%d, mask=0x%04x, count=%d, button=0x%04x", getName().c_str(), key, mask, count, button)); + CProtocolUtil::writef(getStream(), kMsgDKeyRepeat, key, mask, count, button); +} + +void +CClientProxy1_1::keyUp(KeyID key, KeyModifierMask mask, KeyButton button) +{ + LOG((CLOG_DEBUG1 "send key up to \"%s\" id=%d, mask=0x%04x, button=0x%04x", getName().c_str(), key, mask, button)); + CProtocolUtil::writef(getStream(), kMsgDKeyUp, key, mask, button); +} diff --git a/src/lib/server/CClientProxy1_1.h b/src/lib/server/CClientProxy1_1.h new file mode 100644 index 00000000..aa1b2a7e --- /dev/null +++ b/src/lib/server/CClientProxy1_1.h @@ -0,0 +1,36 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CCLIENTPROXY1_1_H +#define CCLIENTPROXY1_1_H + +#include "CClientProxy1_0.h" + +//! Proxy for client implementing protocol version 1.1 +class CClientProxy1_1 : public CClientProxy1_0 { +public: + CClientProxy1_1(const CString& name, IStream* adoptedStream); + ~CClientProxy1_1(); + + // IClient overrides + virtual void keyDown(KeyID, KeyModifierMask, KeyButton); + virtual void keyRepeat(KeyID, KeyModifierMask, + SInt32 count, KeyButton); + virtual void keyUp(KeyID, KeyModifierMask, KeyButton); +}; + +#endif diff --git a/src/lib/server/CClientProxy1_2.cpp b/src/lib/server/CClientProxy1_2.cpp new file mode 100644 index 00000000..80e102eb --- /dev/null +++ b/src/lib/server/CClientProxy1_2.cpp @@ -0,0 +1,42 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClientProxy1_2.h" +#include "CProtocolUtil.h" +#include "CLog.h" + +// +// CClientProxy1_1 +// + +CClientProxy1_2::CClientProxy1_2(const CString& name, IStream* stream) : + CClientProxy1_1(name, stream) +{ + // do nothing +} + +CClientProxy1_2::~CClientProxy1_2() +{ + // do nothing +} + +void +CClientProxy1_2::mouseRelativeMove(SInt32 xRel, SInt32 yRel) +{ + LOG((CLOG_DEBUG2 "send mouse relative move to \"%s\" %d,%d", getName().c_str(), xRel, yRel)); + CProtocolUtil::writef(getStream(), kMsgDMouseRelMove, xRel, yRel); +} diff --git a/src/lib/server/CClientProxy1_2.h b/src/lib/server/CClientProxy1_2.h new file mode 100644 index 00000000..1ac9a25b --- /dev/null +++ b/src/lib/server/CClientProxy1_2.h @@ -0,0 +1,33 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CCLIENTPROXY1_2_H +#define CCLIENTPROXY1_2_H + +#include "CClientProxy1_1.h" + +//! Proxy for client implementing protocol version 1.2 +class CClientProxy1_2 : public CClientProxy1_1 { +public: + CClientProxy1_2(const CString& name, IStream* adoptedStream); + ~CClientProxy1_2(); + + // IClient overrides + virtual void mouseRelativeMove(SInt32 xRel, SInt32 yRel); +}; + +#endif diff --git a/src/lib/server/CClientProxy1_3.cpp b/src/lib/server/CClientProxy1_3.cpp new file mode 100644 index 00000000..97c56e77 --- /dev/null +++ b/src/lib/server/CClientProxy1_3.cpp @@ -0,0 +1,119 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2006 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClientProxy1_3.h" +#include "CProtocolUtil.h" +#include "CLog.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" +#include +#include + +// +// CClientProxy1_3 +// + +CClientProxy1_3::CClientProxy1_3(const CString& name, IStream* stream) : + CClientProxy1_2(name, stream), + m_keepAliveRate(kKeepAliveRate), + m_keepAliveTimer(NULL) +{ + setHeartbeatRate(kKeepAliveRate, kKeepAliveRate * kKeepAlivesUntilDeath); +} + +CClientProxy1_3::~CClientProxy1_3() +{ + // cannot do this in superclass or our override wouldn't get called + removeHeartbeatTimer(); +} + +void +CClientProxy1_3::mouseWheel(SInt32 xDelta, SInt32 yDelta) +{ + LOG((CLOG_DEBUG2 "send mouse wheel to \"%s\" %+d,%+d", getName().c_str(), xDelta, yDelta)); + CProtocolUtil::writef(getStream(), kMsgDMouseWheel, xDelta, yDelta); +} + +bool +CClientProxy1_3::parseMessage(const UInt8* code) +{ + // process message + if (memcmp(code, kMsgCKeepAlive, 4) == 0) { + // reset alarm + resetHeartbeatTimer(); + return true; + } + else { + return CClientProxy1_2::parseMessage(code); + } +} + +void +CClientProxy1_3::resetHeartbeatRate() +{ + setHeartbeatRate(kKeepAliveRate, kKeepAliveRate * kKeepAlivesUntilDeath); +} + +void +CClientProxy1_3::setHeartbeatRate(double rate, double) +{ + m_keepAliveRate = rate; + CClientProxy1_2::setHeartbeatRate(rate, rate * kKeepAlivesUntilDeath); +} + +void +CClientProxy1_3::resetHeartbeatTimer() +{ + // reset the alarm but not the keep alive timer + CClientProxy1_2::removeHeartbeatTimer(); + CClientProxy1_2::addHeartbeatTimer(); +} + +void +CClientProxy1_3::addHeartbeatTimer() +{ + // create and install a timer to periodically send keep alives + if (m_keepAliveRate > 0.0) { + m_keepAliveTimer = EVENTQUEUE->newTimer(m_keepAliveRate, NULL); + EVENTQUEUE->adoptHandler(CEvent::kTimer, m_keepAliveTimer, + new TMethodEventJob(this, + &CClientProxy1_3::handleKeepAlive, NULL)); + } + + // superclass does the alarm + CClientProxy1_2::addHeartbeatTimer(); +} + +void +CClientProxy1_3::removeHeartbeatTimer() +{ + // remove the timer that sends keep alives periodically + if (m_keepAliveTimer != NULL) { + EVENTQUEUE->removeHandler(CEvent::kTimer, m_keepAliveTimer); + EVENTQUEUE->deleteTimer(m_keepAliveTimer); + m_keepAliveTimer = NULL; + } + + // superclass does the alarm + CClientProxy1_2::removeHeartbeatTimer(); +} + +void +CClientProxy1_3::handleKeepAlive(const CEvent&, void*) +{ + CProtocolUtil::writef(getStream(), kMsgCKeepAlive); +} diff --git a/src/lib/server/CClientProxy1_3.h b/src/lib/server/CClientProxy1_3.h new file mode 100644 index 00000000..22f5ae3b --- /dev/null +++ b/src/lib/server/CClientProxy1_3.h @@ -0,0 +1,50 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2006 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CCLIENTPROXY1_3_H +#define CCLIENTPROXY1_3_H + +#include "CClientProxy1_2.h" + +//! Proxy for client implementing protocol version 1.3 +class CClientProxy1_3 : public CClientProxy1_2 { +public: + CClientProxy1_3(const CString& name, IStream* adoptedStream); + ~CClientProxy1_3(); + + // IClient overrides + virtual void mouseWheel(SInt32 xDelta, SInt32 yDelta); + +protected: + // CClientProxy overrides + virtual bool parseMessage(const UInt8* code); + virtual void resetHeartbeatRate(); + virtual void setHeartbeatRate(double rate, double alarm); + virtual void resetHeartbeatTimer(); + virtual void addHeartbeatTimer(); + virtual void removeHeartbeatTimer(); + +private: + void handleKeepAlive(const CEvent&, void*); + + +private: + double m_keepAliveRate; + CEventQueueTimer* m_keepAliveTimer; +}; + +#endif diff --git a/src/lib/server/CClientProxy1_4.cpp b/src/lib/server/CClientProxy1_4.cpp new file mode 100644 index 00000000..6f0b2d8a --- /dev/null +++ b/src/lib/server/CClientProxy1_4.cpp @@ -0,0 +1,111 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClientProxy1_4.h" +#include "CProtocolUtil.h" +#include "CLog.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" +#include +#include +#include "CServer.h" + +// +// CClientProxy1_4 +// + +CClientProxy1_4::CClientProxy1_4(const CString& name, IStream* stream, CServer* server) : + CClientProxy1_3(name, stream), m_server(server) +{ + assert(m_server != NULL); +} + +CClientProxy1_4::~CClientProxy1_4() +{ +} + +void +CClientProxy1_4::gameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) +{ + LOG((CLOG_DEBUG2 "send game device buttons to \"%s\" id=%d buttons=%d", getName().c_str(), id, buttons)); + CProtocolUtil::writef(getStream(), kMsgDGameButtons, id, buttons); +} + +void +CClientProxy1_4::gameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) +{ + LOG((CLOG_DEBUG2 "send game device sticks to \"%s\" id=%d s1=%+d,%+d s2=%+d,%+d", getName().c_str(), id, x1, y1, x2, y2)); + CProtocolUtil::writef(getStream(), kMsgDGameSticks, id, x1, y1, x2, y2); +} + +void +CClientProxy1_4::gameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) +{ + LOG((CLOG_DEBUG2 "send game device triggers to \"%s\" id=%d t1=%d t2=%d", getName().c_str(), id, t1, t2)); + CProtocolUtil::writef(getStream(), kMsgDGameTriggers, id, t1, t2); +} + +void +CClientProxy1_4::gameDeviceTimingReq() +{ + LOG((CLOG_DEBUG2 "send game device timing request to \"%s\"", getName().c_str())); + CProtocolUtil::writef(getStream(), kMsgCGameTimingReq); +} + +bool +CClientProxy1_4::parseMessage(const UInt8* code) +{ + // process message + if (memcmp(code, kMsgCGameTimingResp, 4) == 0) { + gameDeviceTimingResp(); + } + + else if (memcmp(code, kMsgDGameFeedback, 4) == 0) { + gameDeviceFeedback(); + } + + else { + return CClientProxy1_3::parseMessage(code); + } + + return true; +} + +void +CClientProxy1_4::gameDeviceFeedback() +{ + // parse + GameDeviceID id; + UInt16 m1, m2; + CProtocolUtil::readf(getStream(), kMsgDGameFeedback + 4, &id, &m1, &m2); + LOG((CLOG_DEBUG2 "recv game device feedback id=%d m1=%d m2=%d", id, m1, m2)); + + // forward + m_server->gameDeviceFeedback(id, m1, m2); +} + +void +CClientProxy1_4::gameDeviceTimingResp() +{ + // parse + UInt16 freq; + CProtocolUtil::readf(getStream(), kMsgCGameTimingResp + 4, &freq); + LOG((CLOG_DEBUG2 "recv game device timing response freq=%dms", freq)); + + // forward + m_server->gameDeviceTimingResp(freq); +} diff --git a/src/lib/server/CClientProxy1_4.h b/src/lib/server/CClientProxy1_4.h new file mode 100644 index 00000000..0da559ca --- /dev/null +++ b/src/lib/server/CClientProxy1_4.h @@ -0,0 +1,47 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClientProxy1_3.h" +#include "GameDeviceTypes.h" + +class CServer; + +//! Proxy for client implementing protocol version 1.4 +class CClientProxy1_4 : public CClientProxy1_3 { +public: + CClientProxy1_4(const CString& name, IStream* adoptedStream, CServer* server); + ~CClientProxy1_4(); + + // IClient overrides + virtual void gameDeviceButtons(GameDeviceID id, GameDeviceButton buttons); + virtual void gameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2); + virtual void gameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2); + virtual void gameDeviceTimingReq(); + +protected: + // CClientProxy overrides + virtual bool parseMessage(const UInt8* code); + +private: + // message handlers + void gameDeviceTimingResp(); + void gameDeviceFeedback(); + + CServer* m_server; +}; diff --git a/src/lib/server/CClientProxyUnknown.cpp b/src/lib/server/CClientProxyUnknown.cpp new file mode 100644 index 00000000..2965d93c --- /dev/null +++ b/src/lib/server/CClientProxyUnknown.cpp @@ -0,0 +1,299 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClientProxyUnknown.h" +#include "CClientProxy1_0.h" +#include "CClientProxy1_1.h" +#include "CClientProxy1_2.h" +#include "CClientProxy1_3.h" +#include "CClientProxy1_4.h" +#include "ProtocolTypes.h" +#include "CProtocolUtil.h" +#include "XSynergy.h" +#include "IStream.h" +#include "XIO.h" +#include "CLog.h" +#include "CString.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" +#include "CServer.h" + +// +// CClientProxyUnknown +// + +CEvent::Type CClientProxyUnknown::s_successEvent = CEvent::kUnknown; +CEvent::Type CClientProxyUnknown::s_failureEvent = CEvent::kUnknown; + +CClientProxyUnknown::CClientProxyUnknown(IStream* stream, double timeout, CServer* server) : + m_stream(stream), + m_proxy(NULL), + m_ready(false), + m_server(server) +{ + assert(m_server != NULL); + + EVENTQUEUE->adoptHandler(CEvent::kTimer, this, + new TMethodEventJob(this, + &CClientProxyUnknown::handleTimeout, NULL)); + m_timer = EVENTQUEUE->newOneShotTimer(timeout, this); + addStreamHandlers(); + + LOG((CLOG_DEBUG1 "saying hello")); + CProtocolUtil::writef(m_stream, kMsgHello, + kProtocolMajorVersion, + kProtocolMinorVersion); +} + +CClientProxyUnknown::~CClientProxyUnknown() +{ + removeHandlers(); + removeTimer(); + delete m_stream; + delete m_proxy; +} + +CClientProxy* +CClientProxyUnknown::orphanClientProxy() +{ + if (m_ready) { + removeHandlers(); + CClientProxy* proxy = m_proxy; + m_proxy = NULL; + return proxy; + } + else { + return NULL; + } +} + +CEvent::Type +CClientProxyUnknown::getSuccessEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_successEvent, + "CClientProxy::success"); +} + +CEvent::Type +CClientProxyUnknown::getFailureEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_failureEvent, + "CClientProxy::failure"); +} + +void +CClientProxyUnknown::sendSuccess() +{ + m_ready = true; + removeTimer(); + EVENTQUEUE->addEvent(CEvent(getSuccessEvent(), this)); +} + +void +CClientProxyUnknown::sendFailure() +{ + delete m_proxy; + m_proxy = NULL; + m_ready = false; + removeHandlers(); + removeTimer(); + EVENTQUEUE->addEvent(CEvent(getFailureEvent(), this)); +} + +void +CClientProxyUnknown::addStreamHandlers() +{ + assert(m_stream != NULL); + + EVENTQUEUE->adoptHandler(m_stream->getInputReadyEvent(), + m_stream->getEventTarget(), + new TMethodEventJob(this, + &CClientProxyUnknown::handleData)); + EVENTQUEUE->adoptHandler(m_stream->getOutputErrorEvent(), + m_stream->getEventTarget(), + new TMethodEventJob(this, + &CClientProxyUnknown::handleWriteError)); + EVENTQUEUE->adoptHandler(m_stream->getInputShutdownEvent(), + m_stream->getEventTarget(), + new TMethodEventJob(this, + &CClientProxyUnknown::handleDisconnect)); + EVENTQUEUE->adoptHandler(m_stream->getOutputShutdownEvent(), + m_stream->getEventTarget(), + new TMethodEventJob(this, + &CClientProxyUnknown::handleWriteError)); +} + +void +CClientProxyUnknown::addProxyHandlers() +{ + assert(m_proxy != NULL); + + EVENTQUEUE->adoptHandler(CClientProxy::getReadyEvent(), + m_proxy, + new TMethodEventJob(this, + &CClientProxyUnknown::handleReady)); + EVENTQUEUE->adoptHandler(CClientProxy::getDisconnectedEvent(), + m_proxy, + new TMethodEventJob(this, + &CClientProxyUnknown::handleDisconnect)); +} + +void +CClientProxyUnknown::removeHandlers() +{ + if (m_stream != NULL) { + EVENTQUEUE->removeHandler(m_stream->getInputReadyEvent(), + m_stream->getEventTarget()); + EVENTQUEUE->removeHandler(m_stream->getOutputErrorEvent(), + m_stream->getEventTarget()); + EVENTQUEUE->removeHandler(m_stream->getInputShutdownEvent(), + m_stream->getEventTarget()); + EVENTQUEUE->removeHandler(m_stream->getOutputShutdownEvent(), + m_stream->getEventTarget()); + } + if (m_proxy != NULL) { + EVENTQUEUE->removeHandler(CClientProxy::getReadyEvent(), + m_proxy); + EVENTQUEUE->removeHandler(CClientProxy::getDisconnectedEvent(), + m_proxy); + } +} + +void +CClientProxyUnknown::removeTimer() +{ + if (m_timer != NULL) { + EVENTQUEUE->deleteTimer(m_timer); + EVENTQUEUE->removeHandler(CEvent::kTimer, this); + m_timer = NULL; + } +} + +void +CClientProxyUnknown::handleData(const CEvent&, void*) +{ + LOG((CLOG_DEBUG1 "parsing hello reply")); + + CString name(""); + try { + // limit the maximum length of the hello + UInt32 n = m_stream->getSize(); + if (n > kMaxHelloLength) { + LOG((CLOG_DEBUG1 "hello reply too long")); + throw XBadClient(); + } + + // parse the reply to hello + SInt16 major, minor; + if (!CProtocolUtil::readf(m_stream, kMsgHelloBack, + &major, &minor, &name)) { + throw XBadClient(); + } + + // disallow invalid version numbers + if (major <= 0 || minor < 0) { + throw XIncompatibleClient(major, minor); + } + + // remove stream event handlers. the proxy we're about to create + // may install its own handlers and we don't want to accidentally + // remove those later. + removeHandlers(); + + // create client proxy for highest version supported by the client + if (major == 1) { + switch (minor) { + case 0: + m_proxy = new CClientProxy1_0(name, m_stream); + break; + + case 1: + m_proxy = new CClientProxy1_1(name, m_stream); + break; + + case 2: + m_proxy = new CClientProxy1_2(name, m_stream); + break; + + case 3: + m_proxy = new CClientProxy1_3(name, m_stream); + break; + + case 4: + m_proxy = new CClientProxy1_4(name, m_stream, m_server); + break; + } + } + + // hangup (with error) if version isn't supported + if (m_proxy == NULL) { + throw XIncompatibleClient(major, minor); + } + + // the proxy is created and now proxy now owns the stream + LOG((CLOG_DEBUG1 "created proxy for client \"%s\" version %d.%d", name.c_str(), major, minor)); + m_stream = NULL; + + // wait until the proxy signals that it's ready or has disconnected + addProxyHandlers(); + return; + } + catch (XIncompatibleClient& e) { + // client is incompatible + LOG((CLOG_WARN "client \"%s\" has incompatible version %d.%d)", name.c_str(), e.getMajor(), e.getMinor())); + CProtocolUtil::writef(m_stream, + kMsgEIncompatible, + kProtocolMajorVersion, kProtocolMinorVersion); + } + catch (XBadClient&) { + // client not behaving + LOG((CLOG_WARN "protocol error from client \"%s\"", name.c_str())); + CProtocolUtil::writef(m_stream, kMsgEBad); + } + catch (XBase& e) { + // misc error + LOG((CLOG_WARN "error communicating with client \"%s\": %s", name.c_str(), e.what())); + } + sendFailure(); +} + +void +CClientProxyUnknown::handleWriteError(const CEvent&, void*) +{ + LOG((CLOG_NOTE "error communicating with new client")); + sendFailure(); +} + +void +CClientProxyUnknown::handleTimeout(const CEvent&, void*) +{ + LOG((CLOG_NOTE "new client is unresponsive")); + sendFailure(); +} + +void +CClientProxyUnknown::handleDisconnect(const CEvent&, void*) +{ + LOG((CLOG_NOTE "new client disconnected")); + sendFailure(); +} + +void +CClientProxyUnknown::handleReady(const CEvent&, void*) +{ + sendSuccess(); +} diff --git a/src/lib/server/CClientProxyUnknown.h b/src/lib/server/CClientProxyUnknown.h new file mode 100644 index 00000000..7c8dad72 --- /dev/null +++ b/src/lib/server/CClientProxyUnknown.h @@ -0,0 +1,88 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CCLIENTPROXYUNKNOWN_H +#define CCLIENTPROXYUNKNOWN_H + +#include "CEvent.h" + +class CClientProxy; +class CEventQueueTimer; +class IStream; +class CServer; + +class CClientProxyUnknown { +public: + CClientProxyUnknown(IStream* stream, double timeout, CServer* server); + ~CClientProxyUnknown(); + + //! @name manipulators + //@{ + + //! Get the client proxy + /*! + Returns the client proxy created after a successful handshake + (i.e. when this object sends a success event). Returns NULL + if the handshake is unsuccessful or incomplete. + */ + CClientProxy* orphanClientProxy(); + + //@} + //! @name accessors + //@{ + + //! Get success event type + /*! + Returns the success event type. This is sent when the client has + correctly responded to the hello message. The target is this. + */ + static CEvent::Type getSuccessEvent(); + + //! Get failure event type + /*! + Returns the failure event type. This is sent when a client fails + to correctly respond to the hello message. The target is this. + */ + static CEvent::Type getFailureEvent(); + + //@} + +private: + void sendSuccess(); + void sendFailure(); + void addStreamHandlers(); + void addProxyHandlers(); + void removeHandlers(); + void removeTimer(); + void handleData(const CEvent&, void*); + void handleWriteError(const CEvent&, void*); + void handleTimeout(const CEvent&, void*); + void handleDisconnect(const CEvent&, void*); + void handleReady(const CEvent&, void*); + +private: + IStream* m_stream; + CEventQueueTimer* m_timer; + CClientProxy* m_proxy; + bool m_ready; + + static CEvent::Type s_successEvent; + static CEvent::Type s_failureEvent; + CServer* m_server; +}; + +#endif diff --git a/src/lib/server/CConfig.cpp b/src/lib/server/CConfig.cpp new file mode 100644 index 00000000..deb2cf49 --- /dev/null +++ b/src/lib/server/CConfig.cpp @@ -0,0 +1,2323 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CConfig.h" +#include "CServer.h" +#include "CKeyMap.h" +#include "KeyTypes.h" +#include "XSocket.h" +#include "stdistream.h" +#include "stdostream.h" +#include + +// +// CConfig +// + +CConfig::CConfig() : m_hasLockToScreenAction(false) +{ + // do nothing +} + +CConfig::~CConfig() +{ + // do nothing +} + +bool +CConfig::addScreen(const CString& name) +{ + // alias name must not exist + if (m_nameToCanonicalName.find(name) != m_nameToCanonicalName.end()) { + return false; + } + + // add cell + m_map.insert(std::make_pair(name, CCell())); + + // add name + m_nameToCanonicalName.insert(std::make_pair(name, name)); + + return true; +} + +bool +CConfig::renameScreen(const CString& oldName, + const CString& newName) +{ + // get canonical name and find cell + CString oldCanonical = getCanonicalName(oldName); + CCellMap::iterator index = m_map.find(oldCanonical); + if (index == m_map.end()) { + return false; + } + + // accept if names are equal but replace with new name to maintain + // case. otherwise, the new name must not exist. + if (!CStringUtil::CaselessCmp::equal(oldName, newName) && + m_nameToCanonicalName.find(newName) != m_nameToCanonicalName.end()) { + return false; + } + + // update cell + CCell tmpCell = index->second; + m_map.erase(index); + m_map.insert(std::make_pair(newName, tmpCell)); + + // update name + m_nameToCanonicalName.erase(oldCanonical); + m_nameToCanonicalName.insert(std::make_pair(newName, newName)); + + // update connections + CName oldNameObj(this, oldName); + for (index = m_map.begin(); index != m_map.end(); ++index) { + index->second.rename(oldNameObj, newName); + } + + // update alias targets + if (CStringUtil::CaselessCmp::equal(oldName, oldCanonical)) { + for (CNameMap::iterator iter = m_nameToCanonicalName.begin(); + iter != m_nameToCanonicalName.end(); ++iter) { + if (CStringUtil::CaselessCmp::equal( + iter->second, oldCanonical)) { + iter->second = newName; + } + } + } + + return true; +} + +void +CConfig::removeScreen(const CString& name) +{ + // get canonical name and find cell + CString canonical = getCanonicalName(name); + CCellMap::iterator index = m_map.find(canonical); + if (index == m_map.end()) { + return; + } + + // remove from map + m_map.erase(index); + + // disconnect + CName nameObj(this, name); + for (index = m_map.begin(); index != m_map.end(); ++index) { + index->second.remove(nameObj); + } + + // remove aliases (and canonical name) + for (CNameMap::iterator iter = m_nameToCanonicalName.begin(); + iter != m_nameToCanonicalName.end(); ) { + if (iter->second == canonical) { + m_nameToCanonicalName.erase(iter++); + } + else { + ++index; + } + } +} + +void +CConfig::removeAllScreens() +{ + m_map.clear(); + m_nameToCanonicalName.clear(); +} + +bool +CConfig::addAlias(const CString& canonical, const CString& alias) +{ + // alias name must not exist + if (m_nameToCanonicalName.find(alias) != m_nameToCanonicalName.end()) { + return false; + } + + // canonical name must be known + if (m_map.find(canonical) == m_map.end()) { + return false; + } + + // insert alias + m_nameToCanonicalName.insert(std::make_pair(alias, canonical)); + + return true; +} + +bool +CConfig::removeAlias(const CString& alias) +{ + // must not be a canonical name + if (m_map.find(alias) != m_map.end()) { + return false; + } + + // find alias + CNameMap::iterator index = m_nameToCanonicalName.find(alias); + if (index == m_nameToCanonicalName.end()) { + return false; + } + + // remove alias + m_nameToCanonicalName.erase(index); + + return true; +} + +bool +CConfig::removeAliases(const CString& canonical) +{ + // must be a canonical name + if (m_map.find(canonical) == m_map.end()) { + return false; + } + + // find and removing matching aliases + for (CNameMap::iterator index = m_nameToCanonicalName.begin(); + index != m_nameToCanonicalName.end(); ) { + if (index->second == canonical && index->first != canonical) { + m_nameToCanonicalName.erase(index++); + } + else { + ++index; + } + } + + return true; +} + +void +CConfig::removeAllAliases() +{ + // remove all names + m_nameToCanonicalName.clear(); + + // put the canonical names back in + for (CCellMap::iterator index = m_map.begin(); + index != m_map.end(); ++index) { + m_nameToCanonicalName.insert( + std::make_pair(index->first, index->first)); + } +} + +bool +CConfig::connect(const CString& srcName, + EDirection srcSide, + float srcStart, float srcEnd, + const CString& dstName, + float dstStart, float dstEnd) +{ + assert(srcSide >= kFirstDirection && srcSide <= kLastDirection); + + // find source cell + CCellMap::iterator index = m_map.find(getCanonicalName(srcName)); + if (index == m_map.end()) { + return false; + } + + // add link + CCellEdge srcEdge(srcSide, CInterval(srcStart, srcEnd)); + CCellEdge dstEdge(dstName, srcSide, CInterval(dstStart, dstEnd)); + return index->second.add(srcEdge, dstEdge); +} + +bool +CConfig::disconnect(const CString& srcName, EDirection srcSide) +{ + assert(srcSide >= kFirstDirection && srcSide <= kLastDirection); + + // find source cell + CCellMap::iterator index = m_map.find(srcName); + if (index == m_map.end()) { + return false; + } + + // disconnect side + index->second.remove(srcSide); + + return true; +} + +bool +CConfig::disconnect(const CString& srcName, EDirection srcSide, float position) +{ + assert(srcSide >= kFirstDirection && srcSide <= kLastDirection); + + // find source cell + CCellMap::iterator index = m_map.find(srcName); + if (index == m_map.end()) { + return false; + } + + // disconnect side + index->second.remove(srcSide, position); + + return true; +} + +void +CConfig::setSynergyAddress(const CNetworkAddress& addr) +{ + m_synergyAddress = addr; +} + +bool +CConfig::addOption(const CString& name, OptionID option, OptionValue value) +{ + // find options + CScreenOptions* options = NULL; + if (name.empty()) { + options = &m_globalOptions; + } + else { + CCellMap::iterator index = m_map.find(name); + if (index != m_map.end()) { + options = &index->second.m_options; + } + } + if (options == NULL) { + return false; + } + + // add option + options->insert(std::make_pair(option, value)); + return true; +} + +bool +CConfig::removeOption(const CString& name, OptionID option) +{ + // find options + CScreenOptions* options = NULL; + if (name.empty()) { + options = &m_globalOptions; + } + else { + CCellMap::iterator index = m_map.find(name); + if (index != m_map.end()) { + options = &index->second.m_options; + } + } + if (options == NULL) { + return false; + } + + // remove option + options->erase(option); + return true; +} + +bool +CConfig::removeOptions(const CString& name) +{ + // find options + CScreenOptions* options = NULL; + if (name.empty()) { + options = &m_globalOptions; + } + else { + CCellMap::iterator index = m_map.find(name); + if (index != m_map.end()) { + options = &index->second.m_options; + } + } + if (options == NULL) { + return false; + } + + // remove options + options->clear(); + return true; +} + +bool +CConfig::isValidScreenName(const CString& name) const +{ + // name is valid if matches validname + // name ::= [_A-Za-z0-9] | [_A-Za-z0-9][-_A-Za-z0-9]*[_A-Za-z0-9] + // domain ::= . name + // validname ::= name domain* + // we also accept names ending in . because many OS X users have + // so misconfigured their systems. + + // empty name is invalid + if (name.empty()) { + return false; + } + + // check each dot separated part + CString::size_type b = 0; + for (;;) { + // accept trailing . + if (b == name.size()) { + break; + } + + // find end of part + CString::size_type e = name.find('.', b); + if (e == CString::npos) { + e = name.size(); + } + + // part may not be empty + if (e - b < 1) { + return false; + } + + // check first and last characters + if (!(isalnum(name[b]) || name[b] == '_') || + !(isalnum(name[e - 1]) || name[e - 1] == '_')) { + return false; + } + + // check interior characters + for (CString::size_type i = b; i < e; ++i) { + if (!isalnum(name[i]) && name[i] != '_' && name[i] != '-') { + return false; + } + } + + // next part + if (e == name.size()) { + // no more parts + break; + } + b = e + 1; + } + + return true; +} + +CConfig::const_iterator +CConfig::begin() const +{ + return const_iterator(m_map.begin()); +} + +CConfig::const_iterator +CConfig::end() const +{ + return const_iterator(m_map.end()); +} + +CConfig::all_const_iterator +CConfig::beginAll() const +{ + return m_nameToCanonicalName.begin(); +} + +CConfig::all_const_iterator +CConfig::endAll() const +{ + return m_nameToCanonicalName.end(); +} + +bool +CConfig::isScreen(const CString& name) const +{ + return (m_nameToCanonicalName.count(name) > 0); +} + +bool +CConfig::isCanonicalName(const CString& name) const +{ + return (!name.empty() && + CStringUtil::CaselessCmp::equal(getCanonicalName(name), name)); +} + +CString +CConfig::getCanonicalName(const CString& name) const +{ + CNameMap::const_iterator index = m_nameToCanonicalName.find(name); + if (index == m_nameToCanonicalName.end()) { + return CString(); + } + else { + return index->second; + } +} + +CString +CConfig::getNeighbor(const CString& srcName, EDirection srcSide, + float position, float* positionOut) const +{ + assert(srcSide >= kFirstDirection && srcSide <= kLastDirection); + + // find source cell + CCellMap::const_iterator index = m_map.find(getCanonicalName(srcName)); + if (index == m_map.end()) { + return CString(); + } + + // find edge + const CCellEdge* srcEdge, *dstEdge; + if (!index->second.getLink(srcSide, position, srcEdge, dstEdge)) { + // no neighbor + return ""; + } + else { + // compute position on neighbor + if (positionOut != NULL) { + *positionOut = + dstEdge->inverseTransform(srcEdge->transform(position)); + } + + // return neighbor's name + return getCanonicalName(dstEdge->getName()); + } +} + +bool +CConfig::hasNeighbor(const CString& srcName, EDirection srcSide) const +{ + return hasNeighbor(srcName, srcSide, 0.0f, 1.0f); +} + +bool +CConfig::hasNeighbor(const CString& srcName, EDirection srcSide, + float start, float end) const +{ + assert(srcSide >= kFirstDirection && srcSide <= kLastDirection); + + // find source cell + CCellMap::const_iterator index = m_map.find(getCanonicalName(srcName)); + if (index == m_map.end()) { + return false; + } + + return index->second.overlaps(CCellEdge(srcSide, CInterval(start, end))); +} + +CConfig::link_const_iterator +CConfig::beginNeighbor(const CString& srcName) const +{ + CCellMap::const_iterator index = m_map.find(getCanonicalName(srcName)); + assert(index != m_map.end()); + return index->second.begin(); +} + +CConfig::link_const_iterator +CConfig::endNeighbor(const CString& srcName) const +{ + CCellMap::const_iterator index = m_map.find(getCanonicalName(srcName)); + assert(index != m_map.end()); + return index->second.end(); +} + +const CNetworkAddress& +CConfig::getSynergyAddress() const +{ + return m_synergyAddress; +} + +const CConfig::CScreenOptions* +CConfig::getOptions(const CString& name) const +{ + // find options + const CScreenOptions* options = NULL; + if (name.empty()) { + options = &m_globalOptions; + } + else { + CCellMap::const_iterator index = m_map.find(name); + if (index != m_map.end()) { + options = &index->second.m_options; + } + } + + // return options + return options; +} + +bool +CConfig::hasLockToScreenAction() const +{ + return m_hasLockToScreenAction; +} + +bool +CConfig::operator==(const CConfig& x) const +{ + if (m_synergyAddress != x.m_synergyAddress) { + return false; + } + if (m_map.size() != x.m_map.size()) { + return false; + } + if (m_nameToCanonicalName.size() != x.m_nameToCanonicalName.size()) { + return false; + } + + // compare global options + if (m_globalOptions != x.m_globalOptions) { + return false; + } + + for (CCellMap::const_iterator index1 = m_map.begin(), + index2 = x.m_map.begin(); + index1 != m_map.end(); ++index1, ++index2) { + // compare names + if (!CStringUtil::CaselessCmp::equal(index1->first, index2->first)) { + return false; + } + + // compare cells + if (index1->second != index2->second) { + return false; + } + } + + for (CNameMap::const_iterator index1 = m_nameToCanonicalName.begin(), + index2 = x.m_nameToCanonicalName.begin(); + index1 != m_nameToCanonicalName.end(); + ++index1, ++index2) { + if (!CStringUtil::CaselessCmp::equal(index1->first, index2->first) || + !CStringUtil::CaselessCmp::equal(index1->second, index2->second)) { + return false; + } + } + + // compare input filters + if (m_inputFilter != x.m_inputFilter) { + return false; + } + + return true; +} + +bool +CConfig::operator!=(const CConfig& x) const +{ + return !operator==(x); +} + +void +CConfig::read(CConfigReadContext& context) +{ + CConfig tmp; + while (context) { + tmp.readSection(context); + } + *this = tmp; +} + +const char* +CConfig::dirName(EDirection dir) +{ + static const char* s_name[] = { "left", "right", "up", "down" }; + + assert(dir >= kFirstDirection && dir <= kLastDirection); + + return s_name[dir - kFirstDirection]; +} + +CInputFilter* +CConfig::getInputFilter() +{ + return &m_inputFilter; +} + +CString +CConfig::formatInterval(const CInterval& x) +{ + if (x.first == 0.0f && x.second == 1.0f) { + return ""; + } + return CStringUtil::print("(%d,%d)", (int)(x.first * 100.0f + 0.5f), + (int)(x.second * 100.0f + 0.5f)); +} + +void +CConfig::readSection(CConfigReadContext& s) +{ + static const char s_section[] = "section:"; + static const char s_options[] = "options"; + static const char s_screens[] = "screens"; + static const char s_links[] = "links"; + static const char s_aliases[] = "aliases"; + + CString line; + if (!s.readLine(line)) { + // no more sections + return; + } + + // should be a section header + if (line.find(s_section) != 0) { + throw XConfigRead(s, "found data outside section"); + } + + // get section name + CString::size_type i = line.find_first_not_of(" \t", sizeof(s_section) - 1); + if (i == CString::npos) { + throw XConfigRead(s, "section name is missing"); + } + CString name = line.substr(i); + i = name.find_first_of(" \t"); + if (i != CString::npos) { + throw XConfigRead(s, "unexpected data after section name"); + } + + // read section + if (name == s_options) { + readSectionOptions(s); + } + else if (name == s_screens) { + readSectionScreens(s); + } + else if (name == s_links) { + readSectionLinks(s); + } + else if (name == s_aliases) { + readSectionAliases(s); + } + else { + throw XConfigRead(s, "unknown section name \"%{1}\"", name); + } +} + +void +CConfig::readSectionOptions(CConfigReadContext& s) +{ + CString line; + while (s.readLine(line)) { + // check for end of section + if (line == "end") { + return; + } + + // parse argument: `nameAndArgs = [values][;[values]]' + // nameAndArgs := [(arg[,...])] + // values := valueAndArgs[,valueAndArgs]... + // valueAndArgs := [(arg[,...])] + CString::size_type i = 0; + CString name, value; + CConfigReadContext::ArgList nameArgs, valueArgs; + s.parseNameWithArgs("name", line, "=", i, name, nameArgs); + ++i; + s.parseNameWithArgs("value", line, ",;\n", i, value, valueArgs); + + bool handled = true; + if (name == "address") { + try { + m_synergyAddress = CNetworkAddress(value, kDefaultPort); + m_synergyAddress.resolve(); + } + catch (XSocketAddress& e) { + throw XConfigRead(s, + CString("invalid address argument ") + e.what()); + } + } + else if (name == "heartbeat") { + addOption("", kOptionHeartbeat, s.parseInt(value)); + } + else if (name == "switchCorners") { + addOption("", kOptionScreenSwitchCorners, s.parseCorners(value)); + } + else if (name == "switchCornerSize") { + addOption("", kOptionScreenSwitchCornerSize, s.parseInt(value)); + } + else if (name == "switchDelay") { + addOption("", kOptionScreenSwitchDelay, s.parseInt(value)); + } + else if (name == "switchDoubleTap") { + addOption("", kOptionScreenSwitchTwoTap, s.parseInt(value)); + } + else if (name == "switchNeedsShift") { + addOption("", kOptionScreenSwitchNeedsShift, s.parseBoolean(value)); + } + else if (name == "switchNeedsControl") { + addOption("", kOptionScreenSwitchNeedsControl, s.parseBoolean(value)); + } + else if (name == "switchNeedsAlt") { + addOption("", kOptionScreenSwitchNeedsAlt, s.parseBoolean(value)); + } + else if (name == "screenSaverSync") { + addOption("", kOptionScreenSaverSync, s.parseBoolean(value)); + } + else if (name == "relativeMouseMoves") { + addOption("", kOptionRelativeMouseMoves, s.parseBoolean(value)); + } + else if (name == "win32KeepForeground") { + addOption("", kOptionWin32KeepForeground, s.parseBoolean(value)); + } + else { + handled = false; + } + + if (handled) { + // make sure handled options aren't followed by more values + if (i < line.size() && (line[i] == ',' || line[i] == ';')) { + throw XConfigRead(s, "to many arguments to %s", name.c_str()); + } + } + else { + // make filter rule + CInputFilter::CRule rule(parseCondition(s, name, nameArgs)); + + // save first action (if any) + if (!value.empty() || line[i] != ';') { + parseAction(s, value, valueArgs, rule, true); + } + + // get remaining activate actions + while (i < line.length() && line[i] != ';') { + ++i; + s.parseNameWithArgs("value", line, ",;\n", i, value, valueArgs); + parseAction(s, value, valueArgs, rule, true); + } + + // get deactivate actions + if (i < line.length() && line[i] == ';') { + // allow trailing ';' + i = line.find_first_not_of(" \t", i + 1); + if (i == CString::npos) { + i = line.length(); + } + else { + --i; + } + + // get actions + while (i < line.length()) { + ++i; + s.parseNameWithArgs("value", line, ",\n", + i, value, valueArgs); + parseAction(s, value, valueArgs, rule, false); + } + } + + // add rule + m_inputFilter.addFilterRule(rule); + } + } + throw XConfigRead(s, "unexpected end of options section"); +} + +void +CConfig::readSectionScreens(CConfigReadContext& s) +{ + CString line; + CString screen; + while (s.readLine(line)) { + // check for end of section + if (line == "end") { + return; + } + + // see if it's the next screen + if (line[line.size() - 1] == ':') { + // strip : + screen = line.substr(0, line.size() - 1); + + // verify validity of screen name + if (!isValidScreenName(screen)) { + throw XConfigRead(s, "invalid screen name \"%{1}\"", screen); + } + + // add the screen to the configuration + if (!addScreen(screen)) { + throw XConfigRead(s, "duplicate screen name \"%{1}\"", screen); + } + } + else if (screen.empty()) { + throw XConfigRead(s, "argument before first screen"); + } + else { + // parse argument: `=' + CString::size_type i = line.find_first_of(" \t="); + if (i == 0) { + throw XConfigRead(s, "missing argument name"); + } + if (i == CString::npos) { + throw XConfigRead(s, "missing ="); + } + CString name = line.substr(0, i); + i = line.find_first_not_of(" \t", i); + if (i == CString::npos || line[i] != '=') { + throw XConfigRead(s, "missing ="); + } + i = line.find_first_not_of(" \t", i + 1); + CString value; + if (i != CString::npos) { + value = line.substr(i); + } + + // handle argument + if (name == "halfDuplexCapsLock") { + addOption(screen, kOptionHalfDuplexCapsLock, + s.parseBoolean(value)); + } + else if (name == "halfDuplexNumLock") { + addOption(screen, kOptionHalfDuplexNumLock, + s.parseBoolean(value)); + } + else if (name == "halfDuplexScrollLock") { + addOption(screen, kOptionHalfDuplexScrollLock, + s.parseBoolean(value)); + } + else if (name == "shift") { + addOption(screen, kOptionModifierMapForShift, + s.parseModifierKey(value)); + } + else if (name == "ctrl") { + addOption(screen, kOptionModifierMapForControl, + s.parseModifierKey(value)); + } + else if (name == "alt") { + addOption(screen, kOptionModifierMapForAlt, + s.parseModifierKey(value)); + } + else if (name == "altgr") { + addOption(screen, kOptionModifierMapForAltGr, + s.parseModifierKey(value)); + } + else if (name == "meta") { + addOption(screen, kOptionModifierMapForMeta, + s.parseModifierKey(value)); + } + else if (name == "super") { + addOption(screen, kOptionModifierMapForSuper, + s.parseModifierKey(value)); + } + else if (name == "xtestIsXineramaUnaware") { + addOption(screen, kOptionXTestXineramaUnaware, + s.parseBoolean(value)); + } + else if (name == "switchCorners") { + addOption(screen, kOptionScreenSwitchCorners, + s.parseCorners(value)); + } + else if (name == "switchCornerSize") { + addOption(screen, kOptionScreenSwitchCornerSize, + s.parseInt(value)); + } + else if (name == "preserveFocus") { + addOption(screen, kOptionScreenPreserveFocus, + s.parseBoolean(value)); + } + else { + // unknown argument + throw XConfigRead(s, "unknown argument \"%{1}\"", name); + } + } + } + throw XConfigRead(s, "unexpected end of screens section"); +} + +void +CConfig::readSectionLinks(CConfigReadContext& s) +{ + CString line; + CString screen; + while (s.readLine(line)) { + // check for end of section + if (line == "end") { + return; + } + + // see if it's the next screen + if (line[line.size() - 1] == ':') { + // strip : + screen = line.substr(0, line.size() - 1); + + // verify we know about the screen + if (!isScreen(screen)) { + throw XConfigRead(s, "unknown screen name \"%{1}\"", screen); + } + if (!isCanonicalName(screen)) { + throw XConfigRead(s, "cannot use screen name alias here"); + } + } + else if (screen.empty()) { + throw XConfigRead(s, "argument before first screen"); + } + else { + // parse argument: `[(,)]=[(,)]' + // the stuff in brackets is optional. interval values must be + // in the range [0,100] and start < end. if not given the + // interval is taken to be (0,100). + CString::size_type i = 0; + CString side, dstScreen, srcArgString, dstArgString; + CConfigReadContext::ArgList srcArgs, dstArgs; + s.parseNameWithArgs("link", line, "=", i, side, srcArgs); + ++i; + s.parseNameWithArgs("screen", line, "", i, dstScreen, dstArgs); + CInterval srcInterval(s.parseInterval(srcArgs)); + CInterval dstInterval(s.parseInterval(dstArgs)); + + // handle argument + EDirection dir; + if (side == "left") { + dir = kLeft; + } + else if (side == "right") { + dir = kRight; + } + else if (side == "up") { + dir = kTop; + } + else if (side == "down") { + dir = kBottom; + } + else { + // unknown argument + throw XConfigRead(s, "unknown side \"%{1}\" in link", side); + } + if (!isScreen(dstScreen)) { + throw XConfigRead(s, "unknown screen name \"%{1}\"", dstScreen); + } + if (!connect(screen, dir, + srcInterval.first, srcInterval.second, + dstScreen, + dstInterval.first, dstInterval.second)) { + throw XConfigRead(s, "overlapping range"); + } + } + } + throw XConfigRead(s, "unexpected end of links section"); +} + +void +CConfig::readSectionAliases(CConfigReadContext& s) +{ + CString line; + CString screen; + while (s.readLine(line)) { + // check for end of section + if (line == "end") { + return; + } + + // see if it's the next screen + if (line[line.size() - 1] == ':') { + // strip : + screen = line.substr(0, line.size() - 1); + + // verify we know about the screen + if (!isScreen(screen)) { + throw XConfigRead(s, "unknown screen name \"%{1}\"", screen); + } + if (!isCanonicalName(screen)) { + throw XConfigRead(s, "cannot use screen name alias here"); + } + } + else if (screen.empty()) { + throw XConfigRead(s, "argument before first screen"); + } + else { + // verify validity of screen name + if (!isValidScreenName(line)) { + throw XConfigRead(s, "invalid screen alias \"%{1}\"", line); + } + + // add alias + if (!addAlias(screen, line)) { + throw XConfigRead(s, "alias \"%{1}\" is already used", line); + } + } + } + throw XConfigRead(s, "unexpected end of aliases section"); +} + + +CInputFilter::CCondition* +CConfig::parseCondition(CConfigReadContext& s, + const CString& name, const std::vector& args) +{ + if (name == "keystroke") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for condition: keystroke(modifiers+key)"); + } + + IPlatformScreen::CKeyInfo* keyInfo = s.parseKeystroke(args[0]); + + return new CInputFilter::CKeystrokeCondition(keyInfo); + } + + if (name == "mousebutton") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for condition: mousebutton(modifiers+button)"); + } + + IPlatformScreen::CButtonInfo* mouseInfo = s.parseMouse(args[0]); + + return new CInputFilter::CMouseButtonCondition(mouseInfo); + } + + if (name == "connect") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for condition: connect([screen])"); + } + + CString screen = args[0]; + if (isScreen(screen)) { + screen = getCanonicalName(screen); + } + else if (!screen.empty()) { + throw XConfigRead(s, "unknown screen name \"%{1}\" in connect", screen); + } + + return new CInputFilter::CScreenConnectedCondition(screen); + } + + throw XConfigRead(s, "unknown argument \"%{1}\"", name); +} + +void +CConfig::parseAction(CConfigReadContext& s, + const CString& name, const std::vector& args, + CInputFilter::CRule& rule, bool activate) +{ + CInputFilter::CAction* action; + + if (name == "keystroke" || name == "keyDown" || name == "keyUp") { + if (args.size() < 1 || args.size() > 2) { + throw XConfigRead(s, "syntax for action: keystroke(modifiers+key[,screens])"); + } + + IPlatformScreen::CKeyInfo* keyInfo; + if (args.size() == 1) { + keyInfo = s.parseKeystroke(args[0]); + } + else { + std::set screens; + parseScreens(s, args[1], screens); + keyInfo = s.parseKeystroke(args[0], screens); + } + + if (name == "keystroke") { + IPlatformScreen::CKeyInfo* keyInfo2 = + IKeyState::CKeyInfo::alloc(*keyInfo); + action = new CInputFilter::CKeystrokeAction(keyInfo2, true); + rule.adoptAction(action, true); + action = new CInputFilter::CKeystrokeAction(keyInfo, false); + activate = false; + } + else if (name == "keyDown") { + action = new CInputFilter::CKeystrokeAction(keyInfo, true); + } + else { + action = new CInputFilter::CKeystrokeAction(keyInfo, false); + } + } + + else if (name == "mousebutton" || + name == "mouseDown" || name == "mouseUp") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for action: mousebutton(modifiers+button)"); + } + + IPlatformScreen::CButtonInfo* mouseInfo = s.parseMouse(args[0]); + + if (name == "mousebutton") { + IPlatformScreen::CButtonInfo* mouseInfo2 = + IPlatformScreen::CButtonInfo::alloc(*mouseInfo); + action = new CInputFilter::CMouseButtonAction(mouseInfo2, true); + rule.adoptAction(action, true); + action = new CInputFilter::CMouseButtonAction(mouseInfo, false); + activate = false; + } + else if (name == "mouseDown") { + action = new CInputFilter::CMouseButtonAction(mouseInfo, true); + } + else { + action = new CInputFilter::CMouseButtonAction(mouseInfo, false); + } + } + +/* XXX -- not supported + else if (name == "modifier") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for action: modifier(modifiers)"); + } + + KeyModifierMask mask = s.parseModifier(args[0]); + + action = new CInputFilter::CModifierAction(mask, ~mask); + } +*/ + + else if (name == "switchToScreen") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for action: switchToScreen(name)"); + } + + CString screen = args[0]; + if (isScreen(screen)) { + screen = getCanonicalName(screen); + } + else if (!screen.empty()) { + throw XConfigRead(s, "unknown screen name in switchToScreen"); + } + + action = new CInputFilter::CSwitchToScreenAction(screen); + } + + else if (name == "switchInDirection") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for action: switchInDirection()"); + } + + EDirection direction; + if (args[0] == "left") { + direction = kLeft; + } + else if (args[0] == "right") { + direction = kRight; + } + else if (args[0] == "up") { + direction = kTop; + } + else if (args[0] == "down") { + direction = kBottom; + } + else { + throw XConfigRead(s, "unknown direction \"%{1}\" in switchToScreen", args[0]); + } + + action = new CInputFilter::CSwitchInDirectionAction(direction); + } + + else if (name == "lockCursorToScreen") { + if (args.size() > 1) { + throw XConfigRead(s, "syntax for action: lockCursorToScreen([{off|on|toggle}])"); + } + + CInputFilter::CLockCursorToScreenAction::Mode mode = + CInputFilter::CLockCursorToScreenAction::kToggle; + if (args.size() == 1) { + if (args[0] == "off") { + mode = CInputFilter::CLockCursorToScreenAction::kOff; + } + else if (args[0] == "on") { + mode = CInputFilter::CLockCursorToScreenAction::kOn; + } + else if (args[0] == "toggle") { + mode = CInputFilter::CLockCursorToScreenAction::kToggle; + } + else { + throw XConfigRead(s, "syntax for action: lockCursorToScreen([{off|on|toggle}])"); + } + } + + if (mode != CInputFilter::CLockCursorToScreenAction::kOff) { + m_hasLockToScreenAction = true; + } + + action = new CInputFilter::CLockCursorToScreenAction(mode); + } + + else if (name == "keyboardBroadcast") { + if (args.size() > 2) { + throw XConfigRead(s, "syntax for action: keyboardBroadcast([{off|on|toggle}[,screens]])"); + } + + CInputFilter::CKeyboardBroadcastAction::Mode mode = + CInputFilter::CKeyboardBroadcastAction::kToggle; + if (args.size() >= 1) { + if (args[0] == "off") { + mode = CInputFilter::CKeyboardBroadcastAction::kOff; + } + else if (args[0] == "on") { + mode = CInputFilter::CKeyboardBroadcastAction::kOn; + } + else if (args[0] == "toggle") { + mode = CInputFilter::CKeyboardBroadcastAction::kToggle; + } + else { + throw XConfigRead(s, "syntax for action: keyboardBroadcast([{off|on|toggle}[,screens]])"); + } + } + + std::set screens; + if (args.size() >= 2) { + parseScreens(s, args[1], screens); + } + + action = new CInputFilter::CKeyboardBroadcastAction(mode, screens); + } + + else { + throw XConfigRead(s, "unknown action argument \"%{1}\"", name); + } + + rule.adoptAction(action, activate); +} + +void +CConfig::parseScreens(CConfigReadContext& c, + const CString& s, std::set& screens) const +{ + screens.clear(); + + CString::size_type i = 0; + while (i < s.size()) { + // find end of next screen name + CString::size_type j = s.find(':', i); + if (j == CString::npos) { + j = s.size(); + } + + // extract name + CString rawName; + i = s.find_first_not_of(" \t", i); + if (i < j) { + rawName = s.substr(i, s.find_last_not_of(" \t", j - 1) - i + 1); + } + + // add name + if (rawName == "*") { + screens.insert("*"); + } + else if (!rawName.empty()) { + CString name = getCanonicalName(rawName); + if (name.empty()) { + throw XConfigRead(c, "unknown screen name \"%{1}\"", rawName); + } + screens.insert(name); + } + + // next + i = j + 1; + } +} + +const char* +CConfig::getOptionName(OptionID id) +{ + if (id == kOptionHalfDuplexCapsLock) { + return "halfDuplexCapsLock"; + } + if (id == kOptionHalfDuplexNumLock) { + return "halfDuplexNumLock"; + } + if (id == kOptionHalfDuplexScrollLock) { + return "halfDuplexScrollLock"; + } + if (id == kOptionModifierMapForShift) { + return "shift"; + } + if (id == kOptionModifierMapForControl) { + return "ctrl"; + } + if (id == kOptionModifierMapForAlt) { + return "alt"; + } + if (id == kOptionModifierMapForAltGr) { + return "altgr"; + } + if (id == kOptionModifierMapForMeta) { + return "meta"; + } + if (id == kOptionModifierMapForSuper) { + return "super"; + } + if (id == kOptionHeartbeat) { + return "heartbeat"; + } + if (id == kOptionScreenSwitchCorners) { + return "switchCorners"; + } + if (id == kOptionScreenSwitchCornerSize) { + return "switchCornerSize"; + } + if (id == kOptionScreenSwitchDelay) { + return "switchDelay"; + } + if (id == kOptionScreenSwitchTwoTap) { + return "switchDoubleTap"; + } + if (id == kOptionScreenSwitchNeedsShift) { + return "switchNeedsShift"; + } + if (id == kOptionScreenSwitchNeedsControl) { + return "switchNeedsControl"; + } + if (id == kOptionScreenSwitchNeedsAlt) { + return "switchNeedsAlt"; + } + if (id == kOptionScreenSaverSync) { + return "screenSaverSync"; + } + if (id == kOptionXTestXineramaUnaware) { + return "xtestIsXineramaUnaware"; + } + if (id == kOptionRelativeMouseMoves) { + return "relativeMouseMoves"; + } + if (id == kOptionWin32KeepForeground) { + return "win32KeepForeground"; + } + if (id == kOptionScreenPreserveFocus) { + return "preserveFocus"; + } + return NULL; +} + +CString +CConfig::getOptionValue(OptionID id, OptionValue value) +{ + if (id == kOptionHalfDuplexCapsLock || + id == kOptionHalfDuplexNumLock || + id == kOptionHalfDuplexScrollLock || + id == kOptionScreenSwitchNeedsShift || + id == kOptionScreenSwitchNeedsControl || + id == kOptionScreenSwitchNeedsAlt || + id == kOptionScreenSaverSync || + id == kOptionXTestXineramaUnaware || + id == kOptionRelativeMouseMoves || + id == kOptionWin32KeepForeground || + id == kOptionScreenPreserveFocus) { + return (value != 0) ? "true" : "false"; + } + if (id == kOptionModifierMapForShift || + id == kOptionModifierMapForControl || + id == kOptionModifierMapForAlt || + id == kOptionModifierMapForAltGr || + id == kOptionModifierMapForMeta || + id == kOptionModifierMapForSuper) { + switch (value) { + case kKeyModifierIDShift: + return "shift"; + + case kKeyModifierIDControl: + return "ctrl"; + + case kKeyModifierIDAlt: + return "alt"; + + case kKeyModifierIDAltGr: + return "altgr"; + + case kKeyModifierIDMeta: + return "meta"; + + case kKeyModifierIDSuper: + return "super"; + + default: + return "none"; + } + } + if (id == kOptionHeartbeat || + id == kOptionScreenSwitchCornerSize || + id == kOptionScreenSwitchDelay || + id == kOptionScreenSwitchTwoTap) { + return CStringUtil::print("%d", value); + } + if (id == kOptionScreenSwitchCorners) { + std::string result("none"); + if ((value & kTopLeftMask) != 0) { + result += " +top-left"; + } + if ((value & kTopRightMask) != 0) { + result += " +top-right"; + } + if ((value & kBottomLeftMask) != 0) { + result += " +bottom-left"; + } + if ((value & kBottomRightMask) != 0) { + result += " +bottom-right"; + } + return result; + } + + return ""; +} + + +// +// CConfig::CName +// + +CConfig::CName::CName(CConfig* config, const CString& name) : + m_config(config), + m_name(config->getCanonicalName(name)) +{ + // do nothing +} + +bool +CConfig::CName::operator==(const CString& name) const +{ + CString canonical = m_config->getCanonicalName(name); + return CStringUtil::CaselessCmp::equal(canonical, m_name); +} + + +// +// CConfig::CCellEdge +// + +CConfig::CCellEdge::CCellEdge(EDirection side, float position) +{ + init("", side, CInterval(position, position)); +} + +CConfig::CCellEdge::CCellEdge(EDirection side, const CInterval& interval) +{ + assert(interval.first >= 0.0f); + assert(interval.second <= 1.0f); + assert(interval.first < interval.second); + + init("", side, interval); +} + +CConfig::CCellEdge::CCellEdge(const CString& name, + EDirection side, const CInterval& interval) +{ + assert(interval.first >= 0.0f); + assert(interval.second <= 1.0f); + assert(interval.first < interval.second); + + init(name, side, interval); +} + +CConfig::CCellEdge::~CCellEdge() +{ + // do nothing +} + +void +CConfig::CCellEdge::init(const CString& name, EDirection side, + const CInterval& interval) +{ + assert(side != kNoDirection); + + m_name = name; + m_side = side; + m_interval = interval; +} + +CConfig::CInterval +CConfig::CCellEdge::getInterval() const +{ + return m_interval; +} + +void +CConfig::CCellEdge::setName(const CString& newName) +{ + m_name = newName; +} + +CString +CConfig::CCellEdge::getName() const +{ + return m_name; +} + +EDirection +CConfig::CCellEdge::getSide() const +{ + return m_side; +} + +bool +CConfig::CCellEdge::overlaps(const CCellEdge& edge) const +{ + const CInterval& x = m_interval; + const CInterval& y = edge.m_interval; + if (m_side != edge.m_side) { + return false; + } + return (x.first >= y.first && x.first < y.second) || + (x.second > y.first && x.second <= y.second) || + (y.first >= x.first && y.first < x.second) || + (y.second > x.first && y.second <= x.second); +} + +bool +CConfig::CCellEdge::isInside(float x) const +{ + return (x >= m_interval.first && x < m_interval.second); +} + +float +CConfig::CCellEdge::transform(float x) const +{ + return (x - m_interval.first) / (m_interval.second - m_interval.first); +} + + +float +CConfig::CCellEdge::inverseTransform(float x) const +{ + return x * (m_interval.second - m_interval.first) + m_interval.first; +} + +bool +CConfig::CCellEdge::operator<(const CCellEdge& o) const +{ + if (static_cast(m_side) < static_cast(o.m_side)) { + return true; + } + else if (static_cast(m_side) > static_cast(o.m_side)) { + return false; + } + + return (m_interval.first < o.m_interval.first); +} + +bool +CConfig::CCellEdge::operator==(const CCellEdge& x) const +{ + return (m_side == x.m_side && m_interval == x.m_interval); +} + +bool +CConfig::CCellEdge::operator!=(const CCellEdge& x) const +{ + return !operator==(x); +} + + +// +// CConfig::CCell +// + +bool +CConfig::CCell::add(const CCellEdge& src, const CCellEdge& dst) +{ + // cannot add an edge that overlaps other existing edges but we + // can exactly replace an edge. + if (!hasEdge(src) && overlaps(src)) { + return false; + } + + m_neighbors.erase(src); + m_neighbors.insert(std::make_pair(src, dst)); + return true; +} + +void +CConfig::CCell::remove(EDirection side) +{ + for (CEdgeLinks::iterator j = m_neighbors.begin(); + j != m_neighbors.end(); ) { + if (j->first.getSide() == side) { + m_neighbors.erase(j++); + } + else { + ++j; + } + } +} + +void +CConfig::CCell::remove(EDirection side, float position) +{ + for (CEdgeLinks::iterator j = m_neighbors.begin(); + j != m_neighbors.end(); ++j) { + if (j->first.getSide() == side && j->first.isInside(position)) { + m_neighbors.erase(j); + break; + } + } +} +void +CConfig::CCell::remove(const CName& name) +{ + for (CEdgeLinks::iterator j = m_neighbors.begin(); + j != m_neighbors.end(); ) { + if (name == j->second.getName()) { + m_neighbors.erase(j++); + } + else { + ++j; + } + } +} + +void +CConfig::CCell::rename(const CName& oldName, const CString& newName) +{ + for (CEdgeLinks::iterator j = m_neighbors.begin(); + j != m_neighbors.end(); ++j) { + if (oldName == j->second.getName()) { + j->second.setName(newName); + } + } +} + +bool +CConfig::CCell::hasEdge(const CCellEdge& edge) const +{ + CEdgeLinks::const_iterator i = m_neighbors.find(edge); + return (i != m_neighbors.end() && i->first == edge); +} + +bool +CConfig::CCell::overlaps(const CCellEdge& edge) const +{ + CEdgeLinks::const_iterator i = m_neighbors.upper_bound(edge); + if (i != m_neighbors.end() && i->first.overlaps(edge)) { + return true; + } + if (i != m_neighbors.begin() && (--i)->first.overlaps(edge)) { + return true; + } + return false; +} + +bool +CConfig::CCell::getLink(EDirection side, float position, + const CCellEdge*& src, const CCellEdge*& dst) const +{ + CCellEdge edge(side, position); + CEdgeLinks::const_iterator i = m_neighbors.upper_bound(edge); + if (i == m_neighbors.begin()) { + return false; + } + --i; + if (i->first.getSide() == side && i->first.isInside(position)) { + src = &i->first; + dst = &i->second; + return true; + } + return false; +} + +bool +CConfig::CCell::operator==(const CCell& x) const +{ + // compare options + if (m_options != x.m_options) { + return false; + } + + // compare links + if (m_neighbors.size() != x.m_neighbors.size()) { + return false; + } + for (CEdgeLinks::const_iterator index1 = m_neighbors.begin(), + index2 = x.m_neighbors.begin(); + index1 != m_neighbors.end(); + ++index1, ++index2) { + if (index1->first != index2->first) { + return false; + } + if (index1->second != index2->second) { + return false; + } + + // operator== doesn't compare names. only compare destination + // names. + if (!CStringUtil::CaselessCmp::equal(index1->second.getName(), + index2->second.getName())) { + return false; + } + } + return true; +} + +bool +CConfig::CCell::operator!=(const CCell& x) const +{ + return !operator==(x); +} + +CConfig::CCell::const_iterator +CConfig::CCell::begin() const +{ + return m_neighbors.begin(); +} + +CConfig::CCell::const_iterator +CConfig::CCell::end() const +{ + return m_neighbors.end(); +} + + +// +// CConfig I/O +// + +std::istream& +operator>>(std::istream& s, CConfig& config) +{ + CConfigReadContext context(s); + config.read(context); + return s; +} + +std::ostream& +operator<<(std::ostream& s, const CConfig& config) +{ + // screens section + s << "section: screens" << std::endl; + for (CConfig::const_iterator screen = config.begin(); + screen != config.end(); ++screen) { + s << "\t" << screen->c_str() << ":" << std::endl; + const CConfig::CScreenOptions* options = config.getOptions(*screen); + if (options != NULL && options->size() > 0) { + for (CConfig::CScreenOptions::const_iterator + option = options->begin(); + option != options->end(); ++option) { + const char* name = CConfig::getOptionName(option->first); + CString value = CConfig::getOptionValue(option->first, + option->second); + if (name != NULL && !value.empty()) { + s << "\t\t" << name << " = " << value << std::endl; + } + } + } + } + s << "end" << std::endl; + + // links section + CString neighbor; + s << "section: links" << std::endl; + for (CConfig::const_iterator screen = config.begin(); + screen != config.end(); ++screen) { + s << "\t" << screen->c_str() << ":" << std::endl; + + for (CConfig::link_const_iterator + link = config.beginNeighbor(*screen), + nend = config.endNeighbor(*screen); link != nend; ++link) { + s << "\t\t" << CConfig::dirName(link->first.getSide()) << + CConfig::formatInterval(link->first.getInterval()) << + " = " << link->second.getName().c_str() << + CConfig::formatInterval(link->second.getInterval()) << + std::endl; + } + } + s << "end" << std::endl; + + // aliases section (if there are any) + if (config.m_map.size() != config.m_nameToCanonicalName.size()) { + // map canonical to alias + typedef std::multimap CMNameMap; + CMNameMap aliases; + for (CConfig::CNameMap::const_iterator + index = config.m_nameToCanonicalName.begin(); + index != config.m_nameToCanonicalName.end(); + ++index) { + if (index->first != index->second) { + aliases.insert(std::make_pair(index->second, index->first)); + } + } + + // dump it + CString screen; + s << "section: aliases" << std::endl; + for (CMNameMap::const_iterator index = aliases.begin(); + index != aliases.end(); ++index) { + if (index->first != screen) { + screen = index->first; + s << "\t" << screen.c_str() << ":" << std::endl; + } + s << "\t\t" << index->second.c_str() << std::endl; + } + s << "end" << std::endl; + } + + // options section + s << "section: options" << std::endl; + const CConfig::CScreenOptions* options = config.getOptions(""); + if (options != NULL && options->size() > 0) { + for (CConfig::CScreenOptions::const_iterator + option = options->begin(); + option != options->end(); ++option) { + const char* name = CConfig::getOptionName(option->first); + CString value = CConfig::getOptionValue(option->first, + option->second); + if (name != NULL && !value.empty()) { + s << "\t" << name << " = " << value << std::endl; + } + } + } + if (config.m_synergyAddress.isValid()) { + s << "\taddress = " << + config.m_synergyAddress.getHostname().c_str() << std::endl; + } + s << config.m_inputFilter.format("\t"); + s << "end" << std::endl; + + return s; +} + + +// +// CConfigReadContext +// + +CConfigReadContext::CConfigReadContext(std::istream& s, SInt32 firstLine) : + m_stream(s), + m_line(firstLine - 1) +{ + // do nothing +} + +CConfigReadContext::~CConfigReadContext() +{ + // do nothing +} + +bool +CConfigReadContext::readLine(CString& line) +{ + ++m_line; + while (std::getline(m_stream, line)) { + // strip leading whitespace + CString::size_type i = line.find_first_not_of(" \t"); + if (i != CString::npos) { + line.erase(0, i); + } + + // strip comments and then trailing whitespace + i = line.find('#'); + if (i != CString::npos) { + line.erase(i); + } + i = line.find_last_not_of(" \r\t"); + if (i != CString::npos) { + line.erase(i + 1); + } + + // return non empty line + if (!line.empty()) { + // make sure there are no invalid characters + for (i = 0; i < line.length(); ++i) { + if (!isgraph(line[i]) && line[i] != ' ' && line[i] != '\t') { + throw XConfigRead(*this, + "invalid character %{1}", + CStringUtil::print("%#2x", line[i])); + } + } + + return true; + } + + // next line + ++m_line; + } + return false; +} + +UInt32 +CConfigReadContext::getLineNumber() const +{ + return m_line; +} + +CConfigReadContext::operator void*() const +{ + return m_stream; +} + +bool +CConfigReadContext::operator!() const +{ + return !m_stream; +} + +OptionValue +CConfigReadContext::parseBoolean(const CString& arg) const +{ + if (CStringUtil::CaselessCmp::equal(arg, "true")) { + return static_cast(true); + } + if (CStringUtil::CaselessCmp::equal(arg, "false")) { + return static_cast(false); + } + throw XConfigRead(*this, "invalid boolean argument \"%{1}\"", arg); +} + +OptionValue +CConfigReadContext::parseInt(const CString& arg) const +{ + const char* s = arg.c_str(); + char* end; + long tmp = strtol(s, &end, 10); + if (*end != '\0') { + // invalid characters + throw XConfigRead(*this, "invalid integer argument \"%{1}\"", arg); + } + OptionValue value = static_cast(tmp); + if (value != tmp) { + // out of range + throw XConfigRead(*this, "integer argument \"%{1}\" out of range", arg); + } + return value; +} + +OptionValue +CConfigReadContext::parseModifierKey(const CString& arg) const +{ + if (CStringUtil::CaselessCmp::equal(arg, "shift")) { + return static_cast(kKeyModifierIDShift); + } + if (CStringUtil::CaselessCmp::equal(arg, "ctrl")) { + return static_cast(kKeyModifierIDControl); + } + if (CStringUtil::CaselessCmp::equal(arg, "alt")) { + return static_cast(kKeyModifierIDAlt); + } + if (CStringUtil::CaselessCmp::equal(arg, "altgr")) { + return static_cast(kKeyModifierIDAltGr); + } + if (CStringUtil::CaselessCmp::equal(arg, "meta")) { + return static_cast(kKeyModifierIDMeta); + } + if (CStringUtil::CaselessCmp::equal(arg, "super")) { + return static_cast(kKeyModifierIDSuper); + } + if (CStringUtil::CaselessCmp::equal(arg, "none")) { + return static_cast(kKeyModifierIDNull); + } + throw XConfigRead(*this, "invalid argument \"%{1}\"", arg); +} + +OptionValue +CConfigReadContext::parseCorner(const CString& arg) const +{ + if (CStringUtil::CaselessCmp::equal(arg, "left")) { + return kTopLeftMask | kBottomLeftMask; + } + else if (CStringUtil::CaselessCmp::equal(arg, "right")) { + return kTopRightMask | kBottomRightMask; + } + else if (CStringUtil::CaselessCmp::equal(arg, "top")) { + return kTopLeftMask | kTopRightMask; + } + else if (CStringUtil::CaselessCmp::equal(arg, "bottom")) { + return kBottomLeftMask | kBottomRightMask; + } + else if (CStringUtil::CaselessCmp::equal(arg, "top-left")) { + return kTopLeftMask; + } + else if (CStringUtil::CaselessCmp::equal(arg, "top-right")) { + return kTopRightMask; + } + else if (CStringUtil::CaselessCmp::equal(arg, "bottom-left")) { + return kBottomLeftMask; + } + else if (CStringUtil::CaselessCmp::equal(arg, "bottom-right")) { + return kBottomRightMask; + } + else if (CStringUtil::CaselessCmp::equal(arg, "none")) { + return kNoCornerMask; + } + else if (CStringUtil::CaselessCmp::equal(arg, "all")) { + return kAllCornersMask; + } + throw XConfigRead(*this, "invalid argument \"%{1}\"", arg); +} + +OptionValue +CConfigReadContext::parseCorners(const CString& args) const +{ + // find first token + CString::size_type i = args.find_first_not_of(" \t", 0); + if (i == CString::npos) { + throw XConfigRead(*this, "missing corner argument"); + } + CString::size_type j = args.find_first_of(" \t", i); + + // parse first corner token + OptionValue corners = parseCorner(args.substr(i, j - i)); + + // get +/- + i = args.find_first_not_of(" \t", j); + while (i != CString::npos) { + // parse +/- + bool add; + if (args[i] == '-') { + add = false; + } + else if (args[i] == '+') { + add = true; + } + else { + throw XConfigRead(*this, + "invalid corner operator \"%{1}\"", + CString(args.c_str() + i, 1)); + } + + // get next corner token + i = args.find_first_not_of(" \t", i + 1); + j = args.find_first_of(" \t", i); + if (i == CString::npos) { + throw XConfigRead(*this, "missing corner argument"); + } + + // parse next corner token + if (add) { + corners |= parseCorner(args.substr(i, j - i)); + } + else { + corners &= ~parseCorner(args.substr(i, j - i)); + } + i = args.find_first_not_of(" \t", j); + } + + return corners; +} + +CConfig::CInterval +CConfigReadContext::parseInterval(const ArgList& args) const +{ + if (args.size() == 0) { + return CConfig::CInterval(0.0f, 1.0f); + } + if (args.size() != 2 || args[0].empty() || args[1].empty()) { + throw XConfigRead(*this, "invalid interval \"%{1}\"", concatArgs(args)); + } + + char* end; + long startValue = strtol(args[0].c_str(), &end, 10); + if (end[0] != '\0') { + throw XConfigRead(*this, "invalid interval \"%{1}\"", concatArgs(args)); + } + long endValue = strtol(args[1].c_str(), &end, 10); + if (end[0] != '\0') { + throw XConfigRead(*this, "invalid interval \"%{1}\"", concatArgs(args)); + } + + if (startValue < 0 || startValue > 100 || + endValue < 0 || endValue > 100 || + startValue >= endValue) { + throw XConfigRead(*this, "invalid interval \"%{1}\"", concatArgs(args)); + } + + return CConfig::CInterval(startValue / 100.0f, endValue / 100.0f); +} + +void +CConfigReadContext::parseNameWithArgs( + const CString& type, const CString& line, + const CString& delim, CString::size_type& index, + CString& name, ArgList& args) const +{ + // skip leading whitespace + CString::size_type i = line.find_first_not_of(" \t", index); + if (i == CString::npos) { + throw XConfigRead(*this, CString("missing ") + type); + } + + // find end of name + CString::size_type j = line.find_first_of(" \t(" + delim, i); + if (j == CString::npos) { + j = line.length(); + } + + // save name + name = line.substr(i, j - i); + args.clear(); + + // is it okay to not find a delimiter? + bool needDelim = (!delim.empty() && delim.find('\n') == CString::npos); + + // skip whitespace + i = line.find_first_not_of(" \t", j); + if (i == CString::npos && needDelim) { + // expected delimiter but didn't find it + throw XConfigRead(*this, CString("missing ") + delim[0]); + } + if (i == CString::npos) { + // no arguments + index = line.length(); + return; + } + if (line[i] != '(') { + // no arguments + index = i; + return; + } + + // eat '(' + ++i; + + // parse arguments + j = line.find_first_of(",)", i); + while (j != CString::npos) { + // extract arg + CString arg(line.substr(i, j - i)); + i = j; + + // trim whitespace + j = arg.find_first_not_of(" \t"); + if (j != CString::npos) { + arg.erase(0, j); + } + j = arg.find_last_not_of(" \t"); + if (j != CString::npos) { + arg.erase(j + 1); + } + + // save arg + args.push_back(arg); + + // exit loop at end of arguments + if (line[i] == ')') { + break; + } + + // eat ',' + ++i; + + // next + j = line.find_first_of(",)", i); + } + + // verify ')' + if (j == CString::npos) { + // expected ) + throw XConfigRead(*this, "missing )"); + } + + // eat ')' + ++i; + + // skip whitespace + j = line.find_first_not_of(" \t", i); + if (j == CString::npos && needDelim) { + // expected delimiter but didn't find it + throw XConfigRead(*this, CString("missing ") + delim[0]); + } + + // verify delimiter + if (needDelim && delim.find(line[j]) == CString::npos) { + throw XConfigRead(*this, CString("expected ") + delim[0]); + } + + if (j == CString::npos) { + j = line.length(); + } + + index = j; + return; +} + +IPlatformScreen::CKeyInfo* +CConfigReadContext::parseKeystroke(const CString& keystroke) const +{ + return parseKeystroke(keystroke, std::set()); +} + +IPlatformScreen::CKeyInfo* +CConfigReadContext::parseKeystroke(const CString& keystroke, + const std::set& screens) const +{ + CString s = keystroke; + + KeyModifierMask mask; + if (!CKeyMap::parseModifiers(s, mask)) { + throw XConfigRead(*this, "unable to parse key modifiers"); + } + + KeyID key; + if (!CKeyMap::parseKey(s, key)) { + throw XConfigRead(*this, "unable to parse key"); + } + + if (key == kKeyNone && mask == 0) { + throw XConfigRead(*this, "missing key and/or modifiers in keystroke"); + } + + return IPlatformScreen::CKeyInfo::alloc(key, mask, 0, 0, screens); +} + +IPlatformScreen::CButtonInfo* +CConfigReadContext::parseMouse(const CString& mouse) const +{ + CString s = mouse; + + KeyModifierMask mask; + if (!CKeyMap::parseModifiers(s, mask)) { + throw XConfigRead(*this, "unable to parse button modifiers"); + } + + char* end; + ButtonID button = (ButtonID)strtol(s.c_str(), &end, 10); + if (*end != '\0') { + throw XConfigRead(*this, "unable to parse button"); + } + if (s.empty() || button <= 0) { + throw XConfigRead(*this, "invalid button"); + } + + return IPlatformScreen::CButtonInfo::alloc(button, mask); +} + +KeyModifierMask +CConfigReadContext::parseModifier(const CString& modifiers) const +{ + CString s = modifiers; + + KeyModifierMask mask; + if (!CKeyMap::parseModifiers(s, mask)) { + throw XConfigRead(*this, "unable to parse modifiers"); + } + + if (mask == 0) { + throw XConfigRead(*this, "no modifiers specified"); + } + + return mask; +} + +CString +CConfigReadContext::concatArgs(const ArgList& args) +{ + CString s("("); + for (size_t i = 0; i < args.size(); ++i) { + if (i != 0) { + s += ","; + } + s += args[i]; + } + s += ")"; + return s; +} + + +// +// CConfig I/O exceptions +// + +XConfigRead::XConfigRead(const CConfigReadContext& context, + const CString& error) : + m_error(CStringUtil::print("line %d: %s", + context.getLineNumber(), error.c_str())) +{ + // do nothing +} + +XConfigRead::XConfigRead(const CConfigReadContext& context, + const char* errorFmt, const CString& arg) : + m_error(CStringUtil::print("line %d: ", context.getLineNumber()) + + CStringUtil::format(errorFmt, arg.c_str())) +{ + // do nothing +} + +XConfigRead::~XConfigRead() +{ + // do nothing +} + +CString +XConfigRead::getWhat() const throw() +{ + return format("XConfigRead", "read error: %{1}", m_error.c_str()); +} diff --git a/src/lib/server/CConfig.h b/src/lib/server/CConfig.h new file mode 100644 index 00000000..32add041 --- /dev/null +++ b/src/lib/server/CConfig.h @@ -0,0 +1,539 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CCONFIG_H +#define CCONFIG_H + +#include "OptionTypes.h" +#include "ProtocolTypes.h" +#include "CNetworkAddress.h" +#include "CStringUtil.h" +#include "CInputFilter.h" +#include "XBase.h" +#include "stdmap.h" +#include "stdset.h" +#include "IPlatformScreen.h" +#include + +class CConfig; +class CConfigReadContext; + +namespace std { +template <> +struct iterator_traits { + typedef CString value_type; + typedef ptrdiff_t difference_type; + typedef bidirectional_iterator_tag iterator_category; + typedef CString* pointer; + typedef CString& reference; +}; +}; + +//! Server configuration +/*! +This class holds server configuration information. That includes +the names of screens and their aliases, the links between them, +and network addresses. + +Note that case is preserved in screen names but is ignored when +comparing names. Screen names and their aliases share a +namespace and must be unique. +*/ +class CConfig { +public: + typedef std::map CScreenOptions; + typedef std::pair CInterval; + + class CCellEdge { + public: + CCellEdge(EDirection side, float position); + CCellEdge(EDirection side, const CInterval&); + CCellEdge(const CString& name, EDirection side, const CInterval&); + ~CCellEdge(); + + CInterval getInterval() const; + void setName(const CString& newName); + CString getName() const; + EDirection getSide() const; + bool overlaps(const CCellEdge&) const; + bool isInside(float x) const; + + // transform position to [0,1] + float transform(float x) const; + + // transform [0,1] to position + float inverseTransform(float x) const; + + // compares side and start of interval + bool operator<(const CCellEdge&) const; + + // compares side and interval + bool operator==(const CCellEdge&) const; + bool operator!=(const CCellEdge&) const; + + private: + void init(const CString& name, EDirection side, + const CInterval&); + + private: + CString m_name; + EDirection m_side; + CInterval m_interval; + }; + +private: + class CName { + public: + CName(CConfig*, const CString& name); + + bool operator==(const CString& name) const; + + private: + CConfig* m_config; + CString m_name; + }; + + class CCell { + private: + typedef std::map CEdgeLinks; + + public: + typedef CEdgeLinks::const_iterator const_iterator; + + bool add(const CCellEdge& src, const CCellEdge& dst); + void remove(EDirection side); + void remove(EDirection side, float position); + void remove(const CName& destinationName); + void rename(const CName& oldName, const CString& newName); + + bool hasEdge(const CCellEdge&) const; + bool overlaps(const CCellEdge&) const; + + bool getLink(EDirection side, float position, + const CCellEdge*& src, const CCellEdge*& dst) const; + + bool operator==(const CCell&) const; + bool operator!=(const CCell&) const; + + const_iterator begin() const; + const_iterator end() const; + + private: + CEdgeLinks m_neighbors; + + public: + CScreenOptions m_options; + }; + typedef std::map CCellMap; + typedef std::map CNameMap; + +public: + typedef CCell::const_iterator link_const_iterator; + typedef CCellMap::const_iterator internal_const_iterator; + typedef CNameMap::const_iterator all_const_iterator; + class const_iterator : std::iterator_traits { + public: + explicit const_iterator() : m_i() { } + explicit const_iterator(const internal_const_iterator& i) : m_i(i) { } + + const_iterator& operator=(const const_iterator& i) { + m_i = i.m_i; + return *this; + } + CString operator*() { return m_i->first; } + const CString* operator->() { return &(m_i->first); } + const_iterator& operator++() { ++m_i; return *this; } + const_iterator operator++(int) { return const_iterator(m_i++); } + const_iterator& operator--() { --m_i; return *this; } + const_iterator operator--(int) { return const_iterator(m_i--); } + bool operator==(const const_iterator& i) const { + return (m_i == i.m_i); + } + bool operator!=(const const_iterator& i) const { + return (m_i != i.m_i); + } + + private: + internal_const_iterator m_i; + }; + + CConfig(); + virtual ~CConfig(); + + //! @name manipulators + //@{ + + //! Add screen + /*! + Adds a screen, returning true iff successful. If a screen or + alias with the given name exists then it fails. + */ + bool addScreen(const CString& name); + + //! Rename screen + /*! + Renames a screen. All references to the name are updated. + Returns true iff successful. + */ + bool renameScreen(const CString& oldName, + const CString& newName); + + //! Remove screen + /*! + Removes a screen. This also removes aliases for the screen and + disconnects any connections to the screen. \c name may be an + alias. + */ + void removeScreen(const CString& name); + + //! Remove all screens + /*! + Removes all screens, aliases, and connections. + */ + void removeAllScreens(); + + //! Add alias + /*! + Adds an alias for a screen name. An alias can be used + any place the canonical screen name can (except addScreen()). + Returns false if the alias name already exists or the canonical + name is unknown, otherwise returns true. + */ + bool addAlias(const CString& canonical, + const CString& alias); + + //! Remove alias + /*! + Removes an alias for a screen name. It returns false if the + alias is unknown or a canonical name, otherwise returns true. + */ + bool removeAlias(const CString& alias); + + //! Remove aliases + /*! + Removes all aliases for a canonical screen name. It returns false + if the canonical name is unknown, otherwise returns true. + */ + bool removeAliases(const CString& canonical); + + //! Remove all aliases + /*! + This removes all aliases but not the screens. + */ + void removeAllAliases(); + + //! Connect screens + /*! + Establishes a one-way connection between portions of opposite edges + of two screens. Each portion is described by an interval defined + by two numbers, the start and end of the interval half-open on the + end. The numbers range from 0 to 1, inclusive, for the left/top + to the right/bottom. The user will be able to jump from the + \c srcStart to \c srcSend interval of \c srcSide of screen + \c srcName to the opposite side of screen \c dstName in the interval + \c dstStart and \c dstEnd when both screens are connected to the + server and the user isn't locked to a screen. Returns false if + \c srcName is unknown. \c srcStart must be less than or equal to + \c srcEnd and \c dstStart must be less then or equal to \c dstEnd + and all of \c srcStart, \c srcEnd, \c dstStart, or \c dstEnd must + be inside the range [0,1]. + */ + bool connect(const CString& srcName, + EDirection srcSide, + float srcStart, float srcEnd, + const CString& dstName, + float dstStart, float dstEnd); + + //! Disconnect screens + /*! + Removes all connections created by connect() on side \c srcSide. + Returns false if \c srcName is unknown. + */ + bool disconnect(const CString& srcName, + EDirection srcSide); + + //! Disconnect screens + /*! + Removes the connections created by connect() on side \c srcSide + covering position \c position. Returns false if \c srcName is + unknown. + */ + bool disconnect(const CString& srcName, + EDirection srcSide, float position); + + //! Set server address + /*! + Set the synergy listen addresses. There is no default address so + this must be called to run a server using this configuration. + */ + void setSynergyAddress(const CNetworkAddress&); + + //! Add a screen option + /*! + Adds an option and its value to the named screen. Replaces the + existing option's value if there is one. Returns true iff \c name + is a known screen. + */ + bool addOption(const CString& name, + OptionID option, OptionValue value); + + //! Remove a screen option + /*! + Removes an option and its value from the named screen. Does + nothing if the option doesn't exist on the screen. Returns true + iff \c name is a known screen. + */ + bool removeOption(const CString& name, OptionID option); + + //! Remove a screen options + /*! + Removes all options and values from the named screen. Returns true + iff \c name is a known screen. + */ + bool removeOptions(const CString& name); + + //! Get the hot key input filter + /*! + Returns the hot key input filter. Clients can modify hotkeys using + that object. + */ + CInputFilter* getInputFilter(); + + //@} + //! @name accessors + //@{ + + //! Test screen name validity + /*! + Returns true iff \c name is a valid screen name. + */ + bool isValidScreenName(const CString& name) const; + + //! Get beginning (canonical) screen name iterator + const_iterator begin() const; + //! Get ending (canonical) screen name iterator + const_iterator end() const; + + //! Get beginning screen name iterator + all_const_iterator beginAll() const; + //! Get ending screen name iterator + all_const_iterator endAll() const; + + //! Test for screen name + /*! + Returns true iff \c name names a screen. + */ + bool isScreen(const CString& name) const; + + //! Test for canonical screen name + /*! + Returns true iff \c name is the canonical name of a screen. + */ + bool isCanonicalName(const CString& name) const; + + //! Get canonical name + /*! + Returns the canonical name of a screen or the empty string if + the name is unknown. Returns the canonical name if one is given. + */ + CString getCanonicalName(const CString& name) const; + + //! Get neighbor + /*! + Returns the canonical screen name of the neighbor in the given + direction (set through connect()) at position \c position. Returns + the empty string if there is no neighbor in that direction, otherwise + saves the position on the neighbor in \c positionOut if it's not + \c NULL. + */ + CString getNeighbor(const CString&, EDirection, + float position, float* positionOut) const; + + //! Check for neighbor + /*! + Returns \c true if the screen has a neighbor anywhere along the edge + given by the direction. + */ + bool hasNeighbor(const CString&, EDirection) const; + + //! Check for neighbor + /*! + Returns \c true if the screen has a neighbor in the given range along + the edge given by the direction. + */ + bool hasNeighbor(const CString&, EDirection, + float start, float end) const; + + //! Get beginning neighbor iterator + link_const_iterator beginNeighbor(const CString&) const; + //! Get ending neighbor iterator + link_const_iterator endNeighbor(const CString&) const; + + //! Get the server address + const CNetworkAddress& getSynergyAddress() const; + + //! Get the screen options + /*! + Returns all the added options for the named screen. Returns NULL + if the screen is unknown and an empty collection if there are no + options. + */ + const CScreenOptions* getOptions(const CString& name) const; + + //! Check for lock to screen action + /*! + Returns \c true if this configuration has a lock to screen action. + This is for backwards compatible support of ScrollLock locking. + */ + bool hasLockToScreenAction() const; + + //! Compare configurations + bool operator==(const CConfig&) const; + //! Compare configurations + bool operator!=(const CConfig&) const; + + //! Read configuration + /*! + Reads a configuration from a context. Throws XConfigRead on error + and context is unchanged. + */ + void read(CConfigReadContext& context); + + //! Read configuration + /*! + Reads a configuration from a stream. Throws XConfigRead on error. + */ + friend std::istream& operator>>(std::istream&, CConfig&); + + //! Write configuration + /*! + Writes a configuration to a stream. + */ + friend std::ostream& operator<<(std::ostream&, const CConfig&); + + //! Get direction name + /*! + Returns the name of a direction (for debugging). + */ + static const char* dirName(EDirection); + + //! Get interval as string + /*! + Returns an interval as a parseable string. + */ + static CString formatInterval(const CInterval&); + + //@} + +private: + void readSection(CConfigReadContext&); + void readSectionOptions(CConfigReadContext&); + void readSectionScreens(CConfigReadContext&); + void readSectionLinks(CConfigReadContext&); + void readSectionAliases(CConfigReadContext&); + + CInputFilter::CCondition* + parseCondition(CConfigReadContext&, + const CString& condition, + const std::vector& args); + void parseAction(CConfigReadContext&, + const CString& action, + const std::vector& args, + CInputFilter::CRule&, bool activate); + + void parseScreens(CConfigReadContext&, const CString&, + std::set& screens) const; + static const char* getOptionName(OptionID); + static CString getOptionValue(OptionID, OptionValue); + +private: + CCellMap m_map; + CNameMap m_nameToCanonicalName; + CNetworkAddress m_synergyAddress; + CScreenOptions m_globalOptions; + CInputFilter m_inputFilter; + bool m_hasLockToScreenAction; +}; + +//! Configuration read context +/*! +Maintains a context when reading a configuration from a stream. +*/ +class CConfigReadContext { +public: + typedef std::vector ArgList; + + CConfigReadContext(std::istream&, SInt32 firstLine = 1); + ~CConfigReadContext(); + + bool readLine(CString&); + UInt32 getLineNumber() const; + + operator void*() const; + bool operator!() const; + + OptionValue parseBoolean(const CString&) const; + OptionValue parseInt(const CString&) const; + OptionValue parseModifierKey(const CString&) const; + OptionValue parseCorner(const CString&) const; + OptionValue parseCorners(const CString&) const; + CConfig::CInterval + parseInterval(const ArgList& args) const; + void parseNameWithArgs( + const CString& type, const CString& line, + const CString& delim, CString::size_type& index, + CString& name, ArgList& args) const; + IPlatformScreen::CKeyInfo* + parseKeystroke(const CString& keystroke) const; + IPlatformScreen::CKeyInfo* + parseKeystroke(const CString& keystroke, + const std::set& screens) const; + IPlatformScreen::CButtonInfo* + parseMouse(const CString& mouse) const; + KeyModifierMask parseModifier(const CString& modifiers) const; + +private: + // not implemented + CConfigReadContext& operator=(const CConfigReadContext&); + + static CString concatArgs(const ArgList& args); + +private: + std::istream& m_stream; + SInt32 m_line; +}; + +//! Configuration stream read exception +/*! +Thrown when a configuration stream cannot be parsed. +*/ +class XConfigRead : public XBase { +public: + XConfigRead(const CConfigReadContext& context, const CString&); + XConfigRead(const CConfigReadContext& context, + const char* errorFmt, const CString& arg); + ~XConfigRead(); + +protected: + // XBase overrides + virtual CString getWhat() const throw(); + +private: + CString m_error; +}; + +#endif diff --git a/src/lib/server/CInputFilter.cpp b/src/lib/server/CInputFilter.cpp new file mode 100644 index 00000000..ace55657 --- /dev/null +++ b/src/lib/server/CInputFilter.cpp @@ -0,0 +1,1069 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2005 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CInputFilter.h" +#include "CServer.h" +#include "CPrimaryClient.h" +#include "CKeyMap.h" +#include "CEventQueue.h" +#include "CLog.h" +#include "TMethodEventJob.h" +#include +#include + +// ----------------------------------------------------------------------------- +// Input Filter Condition Classes +// ----------------------------------------------------------------------------- +CInputFilter::CCondition::CCondition() +{ + // do nothing +} + +CInputFilter::CCondition::~CCondition() +{ + // do nothing +} + +void +CInputFilter::CCondition::enablePrimary(CPrimaryClient*) +{ + // do nothing +} + +void +CInputFilter::CCondition::disablePrimary(CPrimaryClient*) +{ + // do nothing +} + +CInputFilter::CKeystrokeCondition::CKeystrokeCondition( + IPlatformScreen::CKeyInfo* info) : + m_id(0), + m_key(info->m_key), + m_mask(info->m_mask) +{ + free(info); +} + +CInputFilter::CKeystrokeCondition::CKeystrokeCondition( + KeyID key, KeyModifierMask mask) : + m_id(0), + m_key(key), + m_mask(mask) +{ + // do nothing +} + +CInputFilter::CKeystrokeCondition::~CKeystrokeCondition() +{ + // do nothing +} + +KeyID +CInputFilter::CKeystrokeCondition::getKey() const +{ + return m_key; +} + +KeyModifierMask +CInputFilter::CKeystrokeCondition::getMask() const +{ + return m_mask; +} + +CInputFilter::CCondition* +CInputFilter::CKeystrokeCondition::clone() const +{ + return new CKeystrokeCondition(m_key, m_mask); +} + +CString +CInputFilter::CKeystrokeCondition::format() const +{ + return CStringUtil::print("keystroke(%s)", + CKeyMap::formatKey(m_key, m_mask).c_str()); +} + +CInputFilter::EFilterStatus +CInputFilter::CKeystrokeCondition::match(const CEvent& event) +{ + EFilterStatus status; + + // check for hotkey events + CEvent::Type type = event.getType(); + if (type == IPrimaryScreen::getHotKeyDownEvent()) { + status = kActivate; + } + else if (type == IPrimaryScreen::getHotKeyUpEvent()) { + status = kDeactivate; + } + else { + return kNoMatch; + } + + // check if it's our hotkey + IPrimaryScreen::CHotKeyInfo* kinfo = + reinterpret_cast(event.getData()); + if (kinfo->m_id != m_id) { + return kNoMatch; + } + + return status; +} + +void +CInputFilter::CKeystrokeCondition::enablePrimary(CPrimaryClient* primary) +{ + m_id = primary->registerHotKey(m_key, m_mask); +} + +void +CInputFilter::CKeystrokeCondition::disablePrimary(CPrimaryClient* primary) +{ + primary->unregisterHotKey(m_id); + m_id = 0; +} + +CInputFilter::CMouseButtonCondition::CMouseButtonCondition( + IPlatformScreen::CButtonInfo* info) : + m_button(info->m_button), + m_mask(info->m_mask) +{ + free(info); +} + +CInputFilter::CMouseButtonCondition::CMouseButtonCondition( + ButtonID button, KeyModifierMask mask) : + m_button(button), + m_mask(mask) +{ + // do nothing +} + +CInputFilter::CMouseButtonCondition::~CMouseButtonCondition() +{ + // do nothing +} + +ButtonID +CInputFilter::CMouseButtonCondition::getButton() const +{ + return m_button; +} + +KeyModifierMask +CInputFilter::CMouseButtonCondition::getMask() const +{ + return m_mask; +} + +CInputFilter::CCondition* +CInputFilter::CMouseButtonCondition::clone() const +{ + return new CMouseButtonCondition(m_button, m_mask); +} + +CString +CInputFilter::CMouseButtonCondition::format() const +{ + CString key = CKeyMap::formatKey(kKeyNone, m_mask); + if (!key.empty()) { + key += "+"; + } + return CStringUtil::print("mousebutton(%s%d)", key.c_str(), m_button); +} + +CInputFilter::EFilterStatus +CInputFilter::CMouseButtonCondition::match(const CEvent& event) +{ + static const KeyModifierMask s_ignoreMask = + KeyModifierAltGr | KeyModifierCapsLock | + KeyModifierNumLock | KeyModifierScrollLock; + + EFilterStatus status; + + // check for hotkey events + CEvent::Type type = event.getType(); + if (type == IPrimaryScreen::getButtonDownEvent()) { + status = kActivate; + } + else if (type == IPrimaryScreen::getButtonUpEvent()) { + status = kDeactivate; + } + else { + return kNoMatch; + } + + // check if it's the right button and modifiers. ignore modifiers + // that cannot be combined with a mouse button. + IPlatformScreen::CButtonInfo* minfo = + reinterpret_cast(event.getData()); + if (minfo->m_button != m_button || + (minfo->m_mask & ~s_ignoreMask) != m_mask) { + return kNoMatch; + } + + return status; +} + +CInputFilter::CScreenConnectedCondition::CScreenConnectedCondition( + const CString& screen) : + m_screen(screen) +{ + // do nothing +} + +CInputFilter::CScreenConnectedCondition::~CScreenConnectedCondition() +{ + // do nothing +} + +CInputFilter::CCondition* +CInputFilter::CScreenConnectedCondition::clone() const +{ + return new CScreenConnectedCondition(m_screen); +} + +CString +CInputFilter::CScreenConnectedCondition::format() const +{ + return CStringUtil::print("connect(%s)", m_screen.c_str()); +} + +CInputFilter::EFilterStatus +CInputFilter::CScreenConnectedCondition::match(const CEvent& event) +{ + if (event.getType() == CServer::getConnectedEvent()) { + CServer::CScreenConnectedInfo* info = + reinterpret_cast(event.getData()); + if (m_screen == info->m_screen || m_screen.empty()) { + return kActivate; + } + } + + return kNoMatch; +} + +// ----------------------------------------------------------------------------- +// Input Filter Action Classes +// ----------------------------------------------------------------------------- +CInputFilter::CAction::CAction() +{ + // do nothing +} + +CInputFilter::CAction::~CAction() +{ + // do nothing +} + +CInputFilter::CLockCursorToScreenAction::CLockCursorToScreenAction(Mode mode) : + m_mode(mode) +{ + // do nothing +} + +CInputFilter::CLockCursorToScreenAction::Mode +CInputFilter::CLockCursorToScreenAction::getMode() const +{ + return m_mode; +} + +CInputFilter::CAction* +CInputFilter::CLockCursorToScreenAction::clone() const +{ + return new CLockCursorToScreenAction(*this); +} + +CString +CInputFilter::CLockCursorToScreenAction::format() const +{ + static const char* s_mode[] = { "off", "on", "toggle" }; + + return CStringUtil::print("lockCursorToScreen(%s)", s_mode[m_mode]); +} + +void +CInputFilter::CLockCursorToScreenAction::perform(const CEvent& event) +{ + static const CServer::CLockCursorToScreenInfo::State s_state[] = { + CServer::CLockCursorToScreenInfo::kOff, + CServer::CLockCursorToScreenInfo::kOn, + CServer::CLockCursorToScreenInfo::kToggle + }; + + // send event + CServer::CLockCursorToScreenInfo* info = + CServer::CLockCursorToScreenInfo::alloc(s_state[m_mode]); + EVENTQUEUE->addEvent(CEvent(CServer::getLockCursorToScreenEvent(), + event.getTarget(), info, + CEvent::kDeliverImmediately)); +} + +CInputFilter::CSwitchToScreenAction::CSwitchToScreenAction( + const CString& screen) : + m_screen(screen) +{ + // do nothing +} + +CString +CInputFilter::CSwitchToScreenAction::getScreen() const +{ + return m_screen; +} + +CInputFilter::CAction* +CInputFilter::CSwitchToScreenAction::clone() const +{ + return new CSwitchToScreenAction(*this); +} + +CString +CInputFilter::CSwitchToScreenAction::format() const +{ + return CStringUtil::print("switchToScreen(%s)", m_screen.c_str()); +} + +void +CInputFilter::CSwitchToScreenAction::perform(const CEvent& event) +{ + // pick screen name. if m_screen is empty then use the screen from + // event if it has one. + CString screen = m_screen; + if (screen.empty() && event.getType() == CServer::getConnectedEvent()) { + CServer::CScreenConnectedInfo* info = + reinterpret_cast(event.getData()); + screen = info->m_screen; + } + + // send event + CServer::CSwitchToScreenInfo* info = + CServer::CSwitchToScreenInfo::alloc(screen); + EVENTQUEUE->addEvent(CEvent(CServer::getSwitchToScreenEvent(), + event.getTarget(), info, + CEvent::kDeliverImmediately)); +} + +CInputFilter::CSwitchInDirectionAction::CSwitchInDirectionAction( + EDirection direction) : + m_direction(direction) +{ + // do nothing +} + +EDirection +CInputFilter::CSwitchInDirectionAction::getDirection() const +{ + return m_direction; +} + +CInputFilter::CAction* +CInputFilter::CSwitchInDirectionAction::clone() const +{ + return new CSwitchInDirectionAction(*this); +} + +CString +CInputFilter::CSwitchInDirectionAction::format() const +{ + static const char* s_names[] = { + "", + "left", + "right", + "up", + "down" + }; + + return CStringUtil::print("switchInDirection(%s)", s_names[m_direction]); +} + +void +CInputFilter::CSwitchInDirectionAction::perform(const CEvent& event) +{ + CServer::CSwitchInDirectionInfo* info = + CServer::CSwitchInDirectionInfo::alloc(m_direction); + EVENTQUEUE->addEvent(CEvent(CServer::getSwitchInDirectionEvent(), + event.getTarget(), info, + CEvent::kDeliverImmediately)); +} + +CInputFilter::CKeyboardBroadcastAction::CKeyboardBroadcastAction(Mode mode) : + m_mode(mode) +{ + // do nothing +} + +CInputFilter::CKeyboardBroadcastAction::CKeyboardBroadcastAction( + Mode mode, + const std::set& screens) : + m_mode(mode), + m_screens(IKeyState::CKeyInfo::join(screens)) +{ + // do nothing +} + +CInputFilter::CKeyboardBroadcastAction::Mode +CInputFilter::CKeyboardBroadcastAction::getMode() const +{ + return m_mode; +} + +std::set +CInputFilter::CKeyboardBroadcastAction::getScreens() const +{ + std::set screens; + IKeyState::CKeyInfo::split(m_screens.c_str(), screens); + return screens; +} + +CInputFilter::CAction* +CInputFilter::CKeyboardBroadcastAction::clone() const +{ + return new CKeyboardBroadcastAction(*this); +} + +CString +CInputFilter::CKeyboardBroadcastAction::format() const +{ + static const char* s_mode[] = { "off", "on", "toggle" }; + static const char* s_name = "keyboardBroadcast"; + + if (m_screens.empty() || m_screens[0] == '*') { + return CStringUtil::print("%s(%s)", s_name, s_mode[m_mode]); + } + else { + return CStringUtil::print("%s(%s,%.*s)", s_name, s_mode[m_mode], + m_screens.size() - 2, + m_screens.c_str() + 1); + } +} + +void +CInputFilter::CKeyboardBroadcastAction::perform(const CEvent& event) +{ + static const CServer::CKeyboardBroadcastInfo::State s_state[] = { + CServer::CKeyboardBroadcastInfo::kOff, + CServer::CKeyboardBroadcastInfo::kOn, + CServer::CKeyboardBroadcastInfo::kToggle + }; + + // send event + CServer::CKeyboardBroadcastInfo* info = + CServer::CKeyboardBroadcastInfo::alloc(s_state[m_mode], m_screens); + EVENTQUEUE->addEvent(CEvent(CServer::getKeyboardBroadcastEvent(), + event.getTarget(), info, + CEvent::kDeliverImmediately)); +} + +CInputFilter::CKeystrokeAction::CKeystrokeAction( + IPlatformScreen::CKeyInfo* info, bool press) : + m_keyInfo(info), + m_press(press) +{ + // do nothing +} + +CInputFilter::CKeystrokeAction::~CKeystrokeAction() +{ + free(m_keyInfo); +} + +void +CInputFilter::CKeystrokeAction::adoptInfo(IPlatformScreen::CKeyInfo* info) +{ + free(m_keyInfo); + m_keyInfo = info; +} + +const IPlatformScreen::CKeyInfo* +CInputFilter::CKeystrokeAction::getInfo() const +{ + return m_keyInfo; +} + +bool +CInputFilter::CKeystrokeAction::isOnPress() const +{ + return m_press; +} + +CInputFilter::CAction* +CInputFilter::CKeystrokeAction::clone() const +{ + IKeyState::CKeyInfo* info = IKeyState::CKeyInfo::alloc(*m_keyInfo); + return new CKeystrokeAction(info, m_press); +} + +CString +CInputFilter::CKeystrokeAction::format() const +{ + const char* type = formatName(); + + if (m_keyInfo->m_screens[0] == '\0') { + return CStringUtil::print("%s(%s)", type, + CKeyMap::formatKey(m_keyInfo->m_key, + m_keyInfo->m_mask).c_str()); + } + else if (m_keyInfo->m_screens[0] == '*') { + return CStringUtil::print("%s(%s,*)", type, + CKeyMap::formatKey(m_keyInfo->m_key, + m_keyInfo->m_mask).c_str()); + } + else { + return CStringUtil::print("%s(%s,%.*s)", type, + CKeyMap::formatKey(m_keyInfo->m_key, + m_keyInfo->m_mask).c_str(), + strlen(m_keyInfo->m_screens + 1) - 1, + m_keyInfo->m_screens + 1); + } +} + +void +CInputFilter::CKeystrokeAction::perform(const CEvent& event) +{ + CEvent::Type type = m_press ? IPlatformScreen::getKeyDownEvent(*EVENTQUEUE) : + IPlatformScreen::getKeyUpEvent(*EVENTQUEUE); + EVENTQUEUE->addEvent(CEvent(IPlatformScreen::getFakeInputBeginEvent(), + event.getTarget(), NULL, + CEvent::kDeliverImmediately)); + EVENTQUEUE->addEvent(CEvent(type, event.getTarget(), m_keyInfo, + CEvent::kDeliverImmediately | + CEvent::kDontFreeData)); + EVENTQUEUE->addEvent(CEvent(IPlatformScreen::getFakeInputEndEvent(), + event.getTarget(), NULL, + CEvent::kDeliverImmediately)); +} + +const char* +CInputFilter::CKeystrokeAction::formatName() const +{ + return (m_press ? "keyDown" : "keyUp"); +} + +CInputFilter::CMouseButtonAction::CMouseButtonAction( + IPlatformScreen::CButtonInfo* info, bool press) : + m_buttonInfo(info), + m_press(press) +{ + // do nothing +} + +CInputFilter::CMouseButtonAction::~CMouseButtonAction() +{ + free(m_buttonInfo); +} + +const IPlatformScreen::CButtonInfo* +CInputFilter::CMouseButtonAction::getInfo() const +{ + return m_buttonInfo; +} + +bool +CInputFilter::CMouseButtonAction::isOnPress() const +{ + return m_press; +} + +CInputFilter::CAction* +CInputFilter::CMouseButtonAction::clone() const +{ + IPlatformScreen::CButtonInfo* info = + IPrimaryScreen::CButtonInfo::alloc(*m_buttonInfo); + return new CMouseButtonAction(info, m_press); +} + +CString +CInputFilter::CMouseButtonAction::format() const +{ + const char* type = formatName(); + + CString key = CKeyMap::formatKey(kKeyNone, m_buttonInfo->m_mask); + return CStringUtil::print("%s(%s%s%d)", type, + key.c_str(), key.empty() ? "" : "+", + m_buttonInfo->m_button); +} + +void +CInputFilter::CMouseButtonAction::perform(const CEvent& event) + +{ + // send modifiers + IPlatformScreen::CKeyInfo* modifierInfo = NULL; + if (m_buttonInfo->m_mask != 0) { + KeyID key = m_press ? kKeySetModifiers : kKeyClearModifiers; + modifierInfo = + IKeyState::CKeyInfo::alloc(key, m_buttonInfo->m_mask, 0, 1); + EVENTQUEUE->addEvent(CEvent(IPlatformScreen::getKeyDownEvent(*EVENTQUEUE), + event.getTarget(), modifierInfo, + CEvent::kDeliverImmediately)); + } + + // send button + CEvent::Type type = m_press ? IPlatformScreen::getButtonDownEvent() : + IPlatformScreen::getButtonUpEvent(); + EVENTQUEUE->addEvent(CEvent(type, event.getTarget(), m_buttonInfo, + CEvent::kDeliverImmediately | + CEvent::kDontFreeData)); +} + +const char* +CInputFilter::CMouseButtonAction::formatName() const +{ + return (m_press ? "mouseDown" : "mouseUp"); +} + +// +// CInputFilter::CRule +// + +CInputFilter::CRule::CRule() : + m_condition(NULL) +{ + // do nothing +} + +CInputFilter::CRule::CRule(CCondition* adoptedCondition) : + m_condition(adoptedCondition) +{ + // do nothing +} + +CInputFilter::CRule::CRule(const CRule& rule) : + m_condition(NULL) +{ + copy(rule); +} + +CInputFilter::CRule::~CRule() +{ + clear(); +} + +CInputFilter::CRule& +CInputFilter::CRule::operator=(const CRule& rule) +{ + if (&rule != this) { + copy(rule); + } + return *this; +} + +void +CInputFilter::CRule::clear() +{ + delete m_condition; + for (CActionList::iterator i = m_activateActions.begin(); + i != m_activateActions.end(); ++i) { + delete *i; + } + for (CActionList::iterator i = m_deactivateActions.begin(); + i != m_deactivateActions.end(); ++i) { + delete *i; + } + + m_condition = NULL; + m_activateActions.clear(); + m_deactivateActions.clear(); +} + +void +CInputFilter::CRule::copy(const CRule& rule) +{ + clear(); + if (rule.m_condition != NULL) { + m_condition = rule.m_condition->clone(); + } + for (CActionList::const_iterator i = rule.m_activateActions.begin(); + i != rule.m_activateActions.end(); ++i) { + m_activateActions.push_back((*i)->clone()); + } + for (CActionList::const_iterator i = rule.m_deactivateActions.begin(); + i != rule.m_deactivateActions.end(); ++i) { + m_deactivateActions.push_back((*i)->clone()); + } +} + +void +CInputFilter::CRule::setCondition(CCondition* adopted) +{ + delete m_condition; + m_condition = adopted; +} + +void +CInputFilter::CRule::adoptAction(CAction* action, bool onActivation) +{ + if (action != NULL) { + if (onActivation) { + m_activateActions.push_back(action); + } + else { + m_deactivateActions.push_back(action); + } + } +} + +void +CInputFilter::CRule::removeAction(bool onActivation, UInt32 index) +{ + if (onActivation) { + delete m_activateActions[index]; + m_activateActions.erase(m_activateActions.begin() + index); + } + else { + delete m_deactivateActions[index]; + m_deactivateActions.erase(m_deactivateActions.begin() + index); + } +} + +void +CInputFilter::CRule::replaceAction(CAction* adopted, + bool onActivation, UInt32 index) +{ + if (adopted == NULL) { + removeAction(onActivation, index); + } + else if (onActivation) { + delete m_activateActions[index]; + m_activateActions[index] = adopted; + } + else { + delete m_deactivateActions[index]; + m_deactivateActions[index] = adopted; + } +} + +void +CInputFilter::CRule::enable(CPrimaryClient* primaryClient) +{ + if (m_condition != NULL) { + m_condition->enablePrimary(primaryClient); + } +} + +void +CInputFilter::CRule::disable(CPrimaryClient* primaryClient) +{ + if (m_condition != NULL) { + m_condition->disablePrimary(primaryClient); + } +} + +bool +CInputFilter::CRule::handleEvent(const CEvent& event) +{ + // NULL condition never matches + if (m_condition == NULL) { + return false; + } + + // match + const CActionList* actions; + switch (m_condition->match(event)) { + default: + // not handled + return false; + + case kActivate: + actions = &m_activateActions; + LOG((CLOG_DEBUG1 "activate actions")); + break; + + case kDeactivate: + actions = &m_deactivateActions; + LOG((CLOG_DEBUG1 "deactivate actions")); + break; + } + + // perform actions + for (CActionList::const_iterator i = actions->begin(); + i != actions->end(); ++i) { + LOG((CLOG_DEBUG1 "hotkey: %s", (*i)->format().c_str())); + (*i)->perform(event); + } + + return true; +} + +CString +CInputFilter::CRule::format() const +{ + CString s; + if (m_condition != NULL) { + // condition + s += m_condition->format(); + s += " = "; + + // activate actions + CActionList::const_iterator i = m_activateActions.begin(); + if (i != m_activateActions.end()) { + s += (*i)->format(); + while (++i != m_activateActions.end()) { + s += ", "; + s += (*i)->format(); + } + } + + // deactivate actions + if (!m_deactivateActions.empty()) { + s += "; "; + i = m_deactivateActions.begin(); + if (i != m_deactivateActions.end()) { + s += (*i)->format(); + while (++i != m_deactivateActions.end()) { + s += ", "; + s += (*i)->format(); + } + } + } + } + return s; +} + +const CInputFilter::CCondition* +CInputFilter::CRule::getCondition() const +{ + return m_condition; +} + +UInt32 +CInputFilter::CRule::getNumActions(bool onActivation) const +{ + if (onActivation) { + return static_cast(m_activateActions.size()); + } + else { + return static_cast(m_deactivateActions.size()); + } +} + +const CInputFilter::CAction& +CInputFilter::CRule::getAction(bool onActivation, UInt32 index) const +{ + if (onActivation) { + return *m_activateActions[index]; + } + else { + return *m_deactivateActions[index]; + } +} + + +// ----------------------------------------------------------------------------- +// Input Filter Class +// ----------------------------------------------------------------------------- +CInputFilter::CInputFilter() : + m_primaryClient(NULL) +{ + // do nothing +} + +CInputFilter::CInputFilter(const CInputFilter& x) : + m_ruleList(x.m_ruleList), + m_primaryClient(NULL) +{ + setPrimaryClient(x.m_primaryClient); +} + +CInputFilter::~CInputFilter() +{ + setPrimaryClient(NULL); +} + +CInputFilter& +CInputFilter::operator=(const CInputFilter& x) +{ + if (&x != this) { + CPrimaryClient* oldClient = m_primaryClient; + setPrimaryClient(NULL); + + m_ruleList = x.m_ruleList; + + setPrimaryClient(oldClient); + } + return *this; +} + +void +CInputFilter::addFilterRule(const CRule& rule) +{ + m_ruleList.push_back(rule); + if (m_primaryClient != NULL) { + m_ruleList.back().enable(m_primaryClient); + } +} + +void +CInputFilter::removeFilterRule(UInt32 index) +{ + if (m_primaryClient != NULL) { + m_ruleList[index].disable(m_primaryClient); + } + m_ruleList.erase(m_ruleList.begin() + index); +} + +CInputFilter::CRule& +CInputFilter::getRule(UInt32 index) +{ + return m_ruleList[index]; +} + +void +CInputFilter::setPrimaryClient(CPrimaryClient* client) +{ + if (m_primaryClient == client) { + return; + } + + if (m_primaryClient != NULL) { + for (CRuleList::iterator rule = m_ruleList.begin(); + rule != m_ruleList.end(); ++rule) { + rule->disable(m_primaryClient); + } + + EVENTQUEUE->removeHandler(IPlatformScreen::getKeyDownEvent(*EVENTQUEUE), + m_primaryClient->getEventTarget()); + EVENTQUEUE->removeHandler(IPlatformScreen::getKeyUpEvent(*EVENTQUEUE), + m_primaryClient->getEventTarget()); + EVENTQUEUE->removeHandler(IPlatformScreen::getKeyRepeatEvent(*EVENTQUEUE), + m_primaryClient->getEventTarget()); + EVENTQUEUE->removeHandler(IPlatformScreen::getButtonDownEvent(), + m_primaryClient->getEventTarget()); + EVENTQUEUE->removeHandler(IPlatformScreen::getButtonUpEvent(), + m_primaryClient->getEventTarget()); + EVENTQUEUE->removeHandler(IPlatformScreen::getHotKeyDownEvent(), + m_primaryClient->getEventTarget()); + EVENTQUEUE->removeHandler(IPlatformScreen::getHotKeyUpEvent(), + m_primaryClient->getEventTarget()); + EVENTQUEUE->removeHandler(CServer::getConnectedEvent(), + m_primaryClient->getEventTarget()); + } + + m_primaryClient = client; + + if (m_primaryClient != NULL) { + EVENTQUEUE->adoptHandler(IPlatformScreen::getKeyDownEvent(*EVENTQUEUE), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CInputFilter::handleEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getKeyUpEvent(*EVENTQUEUE), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CInputFilter::handleEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getKeyRepeatEvent(*EVENTQUEUE), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CInputFilter::handleEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getButtonDownEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CInputFilter::handleEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getButtonUpEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CInputFilter::handleEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getHotKeyDownEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CInputFilter::handleEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getHotKeyUpEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CInputFilter::handleEvent)); + EVENTQUEUE->adoptHandler(CServer::getConnectedEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CInputFilter::handleEvent)); + + for (CRuleList::iterator rule = m_ruleList.begin(); + rule != m_ruleList.end(); ++rule) { + rule->enable(m_primaryClient); + } + } +} + +CString +CInputFilter::format(const CString& linePrefix) const +{ + CString s; + for (CRuleList::const_iterator i = m_ruleList.begin(); + i != m_ruleList.end(); ++i) { + s += linePrefix; + s += i->format(); + s += "\n"; + } + return s; +} + +UInt32 +CInputFilter::getNumRules() const +{ + return static_cast(m_ruleList.size()); +} + +bool +CInputFilter::operator==(const CInputFilter& x) const +{ + // if there are different numbers of rules then we can't be equal + if (m_ruleList.size() != x.m_ruleList.size()) { + return false; + } + + // compare rule lists. the easiest way to do that is to format each + // rule into a string, sort the strings, then compare the results. + std::vector aList, bList; + for (CRuleList::const_iterator i = m_ruleList.begin(); + i != m_ruleList.end(); ++i) { + aList.push_back(i->format()); + } + for (CRuleList::const_iterator i = x.m_ruleList.begin(); + i != x.m_ruleList.end(); ++i) { + bList.push_back(i->format()); + } + std::partial_sort(aList.begin(), aList.end(), aList.end()); + std::partial_sort(bList.begin(), bList.end(), bList.end()); + return (aList == bList); +} + +bool +CInputFilter::operator!=(const CInputFilter& x) const +{ + return !operator==(x); +} + +void +CInputFilter::handleEvent(const CEvent& event, void*) +{ + // copy event and adjust target + CEvent myEvent(event.getType(), this, event.getData(), + event.getFlags() | CEvent::kDontFreeData | + CEvent::kDeliverImmediately); + + // let each rule try to match the event until one does + for (CRuleList::iterator rule = m_ruleList.begin(); + rule != m_ruleList.end(); ++rule) { + if (rule->handleEvent(myEvent)) { + // handled + return; + } + } + + // not handled so pass through + EVENTQUEUE->addEvent(myEvent); +} diff --git a/src/lib/server/CInputFilter.h b/src/lib/server/CInputFilter.h new file mode 100644 index 00000000..066ad4d4 --- /dev/null +++ b/src/lib/server/CInputFilter.h @@ -0,0 +1,347 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2005 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CINPUTFILTER_H +#define CINPUTFILTER_H + +#include "KeyTypes.h" +#include "MouseTypes.h" +#include "ProtocolTypes.h" +#include "IPlatformScreen.h" +#include "CString.h" +#include "stdmap.h" +#include "stdset.h" + +class CPrimaryClient; +class CEvent; + +class CInputFilter { +public: + // ------------------------------------------------------------------------- + // Input Filter Condition Classes + // ------------------------------------------------------------------------- + enum EFilterStatus { + kNoMatch, + kActivate, + kDeactivate + }; + + class CCondition { + public: + CCondition(); + virtual ~CCondition(); + + virtual CCondition* clone() const = 0; + virtual CString format() const = 0; + + virtual EFilterStatus match(const CEvent&) = 0; + + virtual void enablePrimary(CPrimaryClient*); + virtual void disablePrimary(CPrimaryClient*); + }; + + // CKeystrokeCondition + class CKeystrokeCondition : public CCondition { + public: + CKeystrokeCondition(IPlatformScreen::CKeyInfo*); + CKeystrokeCondition(KeyID key, KeyModifierMask mask); + virtual ~CKeystrokeCondition(); + + KeyID getKey() const; + KeyModifierMask getMask() const; + + // CCondition overrides + virtual CCondition* clone() const; + virtual CString format() const; + virtual EFilterStatus match(const CEvent&); + virtual void enablePrimary(CPrimaryClient*); + virtual void disablePrimary(CPrimaryClient*); + + private: + UInt32 m_id; + KeyID m_key; + KeyModifierMask m_mask; + }; + + // CMouseButtonCondition + class CMouseButtonCondition : public CCondition { + public: + CMouseButtonCondition(IPlatformScreen::CButtonInfo*); + CMouseButtonCondition(ButtonID, KeyModifierMask mask); + virtual ~CMouseButtonCondition(); + + ButtonID getButton() const; + KeyModifierMask getMask() const; + + // CCondition overrides + virtual CCondition* clone() const; + virtual CString format() const; + virtual EFilterStatus match(const CEvent&); + + private: + ButtonID m_button; + KeyModifierMask m_mask; + }; + + // CScreenConnectedCondition + class CScreenConnectedCondition : public CCondition { + public: + CScreenConnectedCondition(const CString& screen); + virtual ~CScreenConnectedCondition(); + + // CCondition overrides + virtual CCondition* clone() const; + virtual CString format() const; + virtual EFilterStatus match(const CEvent&); + + private: + CString m_screen; + }; + + // ------------------------------------------------------------------------- + // Input Filter Action Classes + // ------------------------------------------------------------------------- + + class CAction { + public: + CAction(); + virtual ~CAction(); + + virtual CAction* clone() const = 0; + virtual CString format() const = 0; + + virtual void perform(const CEvent&) = 0; + }; + + // CLockCursorToScreenAction + class CLockCursorToScreenAction : public CAction { + public: + enum Mode { kOff, kOn, kToggle }; + + CLockCursorToScreenAction(Mode = kToggle); + + Mode getMode() const; + + // CAction overrides + virtual CAction* clone() const; + virtual CString format() const; + virtual void perform(const CEvent&); + + private: + Mode m_mode; + }; + + // CSwitchToScreenAction + class CSwitchToScreenAction : public CAction { + public: + CSwitchToScreenAction(const CString& screen); + + CString getScreen() const; + + // CAction overrides + virtual CAction* clone() const; + virtual CString format() const; + virtual void perform(const CEvent&); + + private: + CString m_screen; + }; + + // CSwitchInDirectionAction + class CSwitchInDirectionAction : public CAction { + public: + CSwitchInDirectionAction(EDirection); + + EDirection getDirection() const; + + // CAction overrides + virtual CAction* clone() const; + virtual CString format() const; + virtual void perform(const CEvent&); + + private: + EDirection m_direction; + }; + + // CKeyboardBroadcastAction + class CKeyboardBroadcastAction : public CAction { + public: + enum Mode { kOff, kOn, kToggle }; + + CKeyboardBroadcastAction(Mode = kToggle); + CKeyboardBroadcastAction(Mode, const std::set& screens); + + Mode getMode() const; + std::set getScreens() const; + + // CAction overrides + virtual CAction* clone() const; + virtual CString format() const; + virtual void perform(const CEvent&); + + private: + Mode m_mode; + CString m_screens; + }; + + // CKeystrokeAction + class CKeystrokeAction : public CAction { + public: + CKeystrokeAction(IPlatformScreen::CKeyInfo* adoptedInfo, bool press); + ~CKeystrokeAction(); + + void adoptInfo(IPlatformScreen::CKeyInfo*); + const IPlatformScreen::CKeyInfo* + getInfo() const; + bool isOnPress() const; + + // CAction overrides + virtual CAction* clone() const; + virtual CString format() const; + virtual void perform(const CEvent&); + + protected: + virtual const char* formatName() const; + + private: + IPlatformScreen::CKeyInfo* m_keyInfo; + bool m_press; + }; + + // CMouseButtonAction -- modifier combinations not implemented yet + class CMouseButtonAction : public CAction { + public: + CMouseButtonAction(IPlatformScreen::CButtonInfo* adoptedInfo, + bool press); + ~CMouseButtonAction(); + + const IPlatformScreen::CButtonInfo* + getInfo() const; + bool isOnPress() const; + + // CAction overrides + virtual CAction* clone() const; + virtual CString format() const; + virtual void perform(const CEvent&); + + protected: + virtual const char* formatName() const; + + private: + IPlatformScreen::CButtonInfo* m_buttonInfo; + bool m_press; + }; + + class CRule { + public: + CRule(); + CRule(CCondition* adopted); + CRule(const CRule&); + ~CRule(); + + CRule& operator=(const CRule&); + + // replace the condition + void setCondition(CCondition* adopted); + + // add an action to the rule + void adoptAction(CAction*, bool onActivation); + + // remove an action from the rule + void removeAction(bool onActivation, UInt32 index); + + // replace an action in the rule + void replaceAction(CAction* adopted, + bool onActivation, UInt32 index); + + // enable/disable + void enable(CPrimaryClient*); + void disable(CPrimaryClient*); + + // event handling + bool handleEvent(const CEvent&); + + // convert rule to a string + CString format() const; + + // get the rule's condition + const CCondition* + getCondition() const; + + // get number of actions + UInt32 getNumActions(bool onActivation) const; + + // get action by index + const CAction& getAction(bool onActivation, UInt32 index) const; + + private: + void clear(); + void copy(const CRule&); + + private: + typedef std::vector CActionList; + + CCondition* m_condition; + CActionList m_activateActions; + CActionList m_deactivateActions; + }; + + // ------------------------------------------------------------------------- + // Input Filter Class + // ------------------------------------------------------------------------- + typedef std::vector CRuleList; + + CInputFilter(); + CInputFilter(const CInputFilter&); + virtual ~CInputFilter(); + + CInputFilter& operator=(const CInputFilter&); + + // add rule, adopting the condition and the actions + void addFilterRule(const CRule& rule); + + // remove a rule + void removeFilterRule(UInt32 index); + + // get rule by index + CRule& getRule(UInt32 index); + + // enable event filtering using the given primary client. disable + // if client is NULL. + void setPrimaryClient(CPrimaryClient* client); + + // convert rules to a string + CString format(const CString& linePrefix) const; + + // get number of rules + UInt32 getNumRules() const; + + //! Compare filters + bool operator==(const CInputFilter&) const; + //! Compare filters + bool operator!=(const CInputFilter&) const; + +private: + // event handling + void handleEvent(const CEvent&, void*); + +private: + CRuleList m_ruleList; + CPrimaryClient* m_primaryClient; +}; + +#endif diff --git a/src/lib/server/CMakeLists.txt b/src/lib/server/CMakeLists.txt new file mode 100644 index 00000000..b62aae37 --- /dev/null +++ b/src/lib/server/CMakeLists.txt @@ -0,0 +1,79 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(inc + CBaseClientProxy.h + CClientListener.h + CClientProxy.h + CClientProxy1_0.h + CClientProxy1_1.h + CClientProxy1_2.h + CClientProxy1_3.h + CClientProxy1_4.h + CClientProxyUnknown.h + CConfig.h + CInputFilter.h + CPrimaryClient.h + CServer.h +) + +set(src + CBaseClientProxy.cpp + CClientListener.cpp + CClientProxy.cpp + CClientProxy1_0.cpp + CClientProxy1_1.cpp + CClientProxy1_2.cpp + CClientProxy1_3.cpp + CClientProxy1_4.cpp + CClientProxyUnknown.cpp + CConfig.cpp + CInputFilter.cpp + CPrimaryClient.cpp + CServer.cpp +) + +if (WIN32) + list(APPEND src ${inc}) +endif() + +set(inc + ../arch + ../base + ../common + ../io + ../mt + ../net + ../synergy +) + +if (UNIX) + list(APPEND inc + ../../.. + ) +endif() + +include_directories(${inc}) +add_library(server STATIC ${src}) + +if (WIN32) + if (GAME_DEVICE_SUPPORT) + include_directories($ENV{DXSDK_DIR}/Include) + endif() +endif() + +if (UNIX) + target_link_libraries(server synergy) +endif() diff --git a/src/lib/server/CPrimaryClient.cpp b/src/lib/server/CPrimaryClient.cpp new file mode 100644 index 00000000..1a2d88ae --- /dev/null +++ b/src/lib/server/CPrimaryClient.cpp @@ -0,0 +1,284 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CPrimaryClient.h" +#include "CScreen.h" +#include "CClipboard.h" +#include "CLog.h" + +// +// CPrimaryClient +// + +CPrimaryClient::CPrimaryClient(const CString& name, CScreen* screen) : + CBaseClientProxy(name), + m_screen(screen), + m_fakeInputCount(0) +{ + // all clipboards are clean + for (UInt32 i = 0; i < kClipboardEnd; ++i) { + m_clipboardDirty[i] = false; + } +} + +CPrimaryClient::~CPrimaryClient() +{ + // do nothing +} + +void +CPrimaryClient::reconfigure(UInt32 activeSides) +{ + m_screen->reconfigure(activeSides); +} + +UInt32 +CPrimaryClient::registerHotKey(KeyID key, KeyModifierMask mask) +{ + return m_screen->registerHotKey(key, mask); +} + +void +CPrimaryClient::unregisterHotKey(UInt32 id) +{ + m_screen->unregisterHotKey(id); +} + +void +CPrimaryClient::fakeInputBegin() +{ + if (++m_fakeInputCount == 1) { + m_screen->fakeInputBegin(); + } +} + +void +CPrimaryClient::fakeInputEnd() +{ + if (--m_fakeInputCount == 0) { + m_screen->fakeInputEnd(); + } +} + +SInt32 +CPrimaryClient::getJumpZoneSize() const +{ + return m_screen->getJumpZoneSize(); +} + +void +CPrimaryClient::getCursorCenter(SInt32& x, SInt32& y) const +{ + m_screen->getCursorCenter(x, y); +} + +KeyModifierMask +CPrimaryClient::getToggleMask() const +{ + return m_screen->pollActiveModifiers(); +} + +bool +CPrimaryClient::isLockedToScreen() const +{ + return m_screen->isLockedToScreen(); +} + +void* +CPrimaryClient::getEventTarget() const +{ + return m_screen->getEventTarget(); +} + +bool +CPrimaryClient::getClipboard(ClipboardID id, IClipboard* clipboard) const +{ + return m_screen->getClipboard(id, clipboard); +} + +void +CPrimaryClient::getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const +{ + m_screen->getShape(x, y, width, height); +} + +void +CPrimaryClient::getCursorPos(SInt32& x, SInt32& y) const +{ + m_screen->getCursorPos(x, y); +} + +void +CPrimaryClient::enable() +{ + m_screen->enable(); +} + +void +CPrimaryClient::disable() +{ + m_screen->disable(); +} + +void +CPrimaryClient::enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, bool screensaver) +{ + m_screen->setSequenceNumber(seqNum); + if (!screensaver) { + m_screen->warpCursor(xAbs, yAbs); + } + m_screen->enter(mask); +} + +bool +CPrimaryClient::leave() +{ + return m_screen->leave(); +} + +void +CPrimaryClient::setClipboard(ClipboardID id, const IClipboard* clipboard) +{ + // ignore if this clipboard is already clean + if (m_clipboardDirty[id]) { + // this clipboard is now clean + m_clipboardDirty[id] = false; + + // set clipboard + m_screen->setClipboard(id, clipboard); + } +} + +void +CPrimaryClient::grabClipboard(ClipboardID id) +{ + // grab clipboard + m_screen->grabClipboard(id); + + // clipboard is dirty (because someone else owns it now) + m_clipboardDirty[id] = true; +} + +void +CPrimaryClient::setClipboardDirty(ClipboardID id, bool dirty) +{ + m_clipboardDirty[id] = dirty; +} + +void +CPrimaryClient::keyDown(KeyID key, KeyModifierMask mask, KeyButton button) +{ + if (m_fakeInputCount > 0) { +// XXX -- don't forward keystrokes to primary screen for now + (void)key; + (void)mask; + (void)button; +// m_screen->keyDown(key, mask, button); + } +} + +void +CPrimaryClient::keyRepeat(KeyID, KeyModifierMask, SInt32, KeyButton) +{ + // ignore +} + +void +CPrimaryClient::keyUp(KeyID key, KeyModifierMask mask, KeyButton button) +{ + if (m_fakeInputCount > 0) { +// XXX -- don't forward keystrokes to primary screen for now + (void)key; + (void)mask; + (void)button; +// m_screen->keyUp(key, mask, button); + } +} + +void +CPrimaryClient::mouseDown(ButtonID) +{ + // ignore +} + +void +CPrimaryClient::mouseUp(ButtonID) +{ + // ignore +} + +void +CPrimaryClient::mouseMove(SInt32 x, SInt32 y) +{ + m_screen->warpCursor(x, y); +} + +void +CPrimaryClient::mouseRelativeMove(SInt32, SInt32) +{ + // ignore +} + +void +CPrimaryClient::mouseWheel(SInt32, SInt32) +{ + // ignore +} + +void +CPrimaryClient::gameDeviceButtons(GameDeviceID, GameDeviceButton) +{ + // ignore +} + +void +CPrimaryClient::gameDeviceSticks(GameDeviceID, SInt16, SInt16, SInt16, SInt16) +{ + // ignore +} + +void +CPrimaryClient::gameDeviceTriggers(GameDeviceID, UInt8, UInt8) +{ + // ignore +} + +void +CPrimaryClient::gameDeviceTimingReq() +{ + // ignore +} + +void +CPrimaryClient::screensaver(bool) +{ + // ignore +} + +void +CPrimaryClient::resetOptions() +{ + m_screen->resetOptions(); +} + +void +CPrimaryClient::setOptions(const COptionsList& options) +{ + m_screen->setOptions(options); +} diff --git a/src/lib/server/CPrimaryClient.h b/src/lib/server/CPrimaryClient.h new file mode 100644 index 00000000..d7492426 --- /dev/null +++ b/src/lib/server/CPrimaryClient.h @@ -0,0 +1,152 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CPRIMARYCLIENT_H +#define CPRIMARYCLIENT_H + +#include "CBaseClientProxy.h" +#include "ProtocolTypes.h" + +class CScreen; + +//! Primary screen as pseudo-client +/*! +The primary screen does not have a client associated with it. This +class provides a pseudo-client to allow the primary screen to be +treated as if it was a client. +*/ +class CPrimaryClient : public CBaseClientProxy { +public: + /*! + \c name is the name of the server and \p screen is primary screen. + */ + CPrimaryClient(const CString& name, CScreen* screen); + ~CPrimaryClient(); + + //! @name manipulators + //@{ + + //! Update configuration + /*! + Handles reconfiguration of jump zones. + */ + void reconfigure(UInt32 activeSides); + + //! Register a system hotkey + /*! + Registers a system-wide hotkey for key \p key with modifiers \p mask. + Returns an id used to unregister the hotkey. + */ + UInt32 registerHotKey(KeyID key, KeyModifierMask mask); + + //! Unregister a system hotkey + /*! + Unregisters a previously registered hot key. + */ + void unregisterHotKey(UInt32 id); + + //! Prepare to synthesize input on primary screen + /*! + Prepares the primary screen to receive synthesized input. We do not + want to receive this synthesized input as user input so this method + ensures that we ignore it. Calls to \c fakeInputBegin() and + \c fakeInputEnd() may be nested; only the outermost have an effect. + */ + void fakeInputBegin(); + + //! Done synthesizing input on primary screen + /*! + Undoes whatever \c fakeInputBegin() did. + */ + void fakeInputEnd(); + + //@} + //! @name accessors + //@{ + + //! Get jump zone size + /*! + Return the jump zone size, the size of the regions on the edges of + the screen that cause the cursor to jump to another screen. + */ + SInt32 getJumpZoneSize() const; + + //! Get cursor center position + /*! + Return the cursor center position which is where we park the + cursor to compute cursor motion deltas and should be far from + the edges of the screen, typically the center. + */ + void getCursorCenter(SInt32& x, SInt32& y) const; + + //! Get toggle key state + /*! + Returns the primary screen's current toggle modifier key state. + */ + KeyModifierMask getToggleMask() const; + + //! Get screen lock state + /*! + Returns true if the user is locked to the screen. + */ + bool isLockedToScreen() const; + + //@} + + // FIXME -- these probably belong on IScreen + virtual void enable(); + virtual void disable(); + + // IScreen overrides + virtual void* getEventTarget() const; + virtual bool getClipboard(ClipboardID id, IClipboard*) const; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const; + virtual void getCursorPos(SInt32& x, SInt32& y) const; + + // IClient overrides + virtual void enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, + bool forScreensaver); + virtual bool leave(); + virtual void setClipboard(ClipboardID, const IClipboard*); + virtual void grabClipboard(ClipboardID); + virtual void setClipboardDirty(ClipboardID, bool); + virtual void keyDown(KeyID, KeyModifierMask, KeyButton); + virtual void keyRepeat(KeyID, KeyModifierMask, + SInt32 count, KeyButton); + virtual void keyUp(KeyID, KeyModifierMask, KeyButton); + virtual void mouseDown(ButtonID); + virtual void mouseUp(ButtonID); + virtual void mouseMove(SInt32 xAbs, SInt32 yAbs); + virtual void mouseRelativeMove(SInt32 xRel, SInt32 yRel); + virtual void mouseWheel(SInt32 xDelta, SInt32 yDelta); + virtual void gameDeviceButtons(GameDeviceID id, GameDeviceButton buttons); + virtual void gameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2); + virtual void gameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2); + virtual void gameDeviceTimingReq(); + virtual void screensaver(bool activate); + virtual void resetOptions(); + virtual void setOptions(const COptionsList& options); + +private: + CScreen* m_screen; + bool m_clipboardDirty[kClipboardEnd]; + SInt32 m_fakeInputCount; +}; + +#endif diff --git a/src/lib/server/CServer.cpp b/src/lib/server/CServer.cpp new file mode 100644 index 00000000..1eaa1557 --- /dev/null +++ b/src/lib/server/CServer.cpp @@ -0,0 +1,2300 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CServer.h" +#include "CClientProxy.h" +#include "CClientProxyUnknown.h" +#include "CPrimaryClient.h" +#include "IPlatformScreen.h" +#include "OptionTypes.h" +#include "ProtocolTypes.h" +#include "XScreen.h" +#include "XSynergy.h" +#include "IDataSocket.h" +#include "IListenSocket.h" +#include "XSocket.h" +#include "IEventQueue.h" +#include "CLog.h" +#include "TMethodEventJob.h" +#include "CArch.h" +#include "CKeyState.h" +#include +#include +#include "CScreen.h" + +// +// CServer +// + +CEvent::Type CServer::s_errorEvent = CEvent::kUnknown; +CEvent::Type CServer::s_connectedEvent = CEvent::kUnknown; +CEvent::Type CServer::s_disconnectedEvent = CEvent::kUnknown; +CEvent::Type CServer::s_switchToScreen = CEvent::kUnknown; +CEvent::Type CServer::s_switchInDirection = CEvent::kUnknown; +CEvent::Type CServer::s_keyboardBroadcast = CEvent::kUnknown; +CEvent::Type CServer::s_lockCursorToScreen = CEvent::kUnknown; +CEvent::Type CServer::s_screenSwitched = CEvent::kUnknown; + +CServer::CServer(const CConfig& config, CPrimaryClient* primaryClient, CScreen* screen) : + m_primaryClient(primaryClient), + m_active(primaryClient), + m_seqNum(0), + m_xDelta(0), + m_yDelta(0), + m_xDelta2(0), + m_yDelta2(0), + m_config(), + m_inputFilter(m_config.getInputFilter()), + m_activeSaver(NULL), + m_switchDir(kNoDirection), + m_switchScreen(NULL), + m_switchWaitDelay(0.0), + m_switchWaitTimer(NULL), + m_switchTwoTapDelay(0.0), + m_switchTwoTapEngaged(false), + m_switchTwoTapArmed(false), + m_switchTwoTapZone(3), + m_switchNeedsShift(false), + m_switchNeedsControl(false), + m_switchNeedsAlt(false), + m_relativeMoves(false), + m_keyboardBroadcasting(false), + m_lockedToScreen(false), + m_screen(screen) +{ + // must have a primary client and it must have a canonical name + assert(m_primaryClient != NULL); + assert(config.isScreen(primaryClient->getName())); + assert(m_screen != NULL); + + CString primaryName = getName(primaryClient); + + // clear clipboards + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + CClipboardInfo& clipboard = m_clipboards[id]; + clipboard.m_clipboardOwner = primaryName; + clipboard.m_clipboardSeqNum = m_seqNum; + if (clipboard.m_clipboard.open(0)) { + clipboard.m_clipboard.empty(); + clipboard.m_clipboard.close(); + } + clipboard.m_clipboardData = clipboard.m_clipboard.marshall(); + } + + // install event handlers + EVENTQUEUE->adoptHandler(CEvent::kTimer, this, + new TMethodEventJob(this, + &CServer::handleSwitchWaitTimeout)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getKeyDownEvent(*EVENTQUEUE), + m_inputFilter, + new TMethodEventJob(this, + &CServer::handleKeyDownEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getKeyUpEvent(*EVENTQUEUE), + m_inputFilter, + new TMethodEventJob(this, + &CServer::handleKeyUpEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getKeyRepeatEvent(* EVENTQUEUE), + m_inputFilter, + new TMethodEventJob(this, + &CServer::handleKeyRepeatEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getButtonDownEvent(), + m_inputFilter, + new TMethodEventJob(this, + &CServer::handleButtonDownEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getButtonUpEvent(), + m_inputFilter, + new TMethodEventJob(this, + &CServer::handleButtonUpEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getMotionOnPrimaryEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CServer::handleMotionPrimaryEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getMotionOnSecondaryEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CServer::handleMotionSecondaryEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getWheelEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CServer::handleWheelEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getGameDeviceButtonsEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CServer::handleGameDeviceButtons)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getGameDeviceSticksEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CServer::handleGameDeviceSticks)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getGameDeviceTriggersEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CServer::handleGameDeviceTriggers)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getGameDeviceTimingReqEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CServer::handleGameDeviceTimingReq)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getScreensaverActivatedEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CServer::handleScreensaverActivatedEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getScreensaverDeactivatedEvent(), + m_primaryClient->getEventTarget(), + new TMethodEventJob(this, + &CServer::handleScreensaverDeactivatedEvent)); + EVENTQUEUE->adoptHandler(getSwitchToScreenEvent(), + m_inputFilter, + new TMethodEventJob(this, + &CServer::handleSwitchToScreenEvent)); + EVENTQUEUE->adoptHandler(getSwitchInDirectionEvent(), + m_inputFilter, + new TMethodEventJob(this, + &CServer::handleSwitchInDirectionEvent)); + EVENTQUEUE->adoptHandler(getKeyboardBroadcastEvent(), + m_inputFilter, + new TMethodEventJob(this, + &CServer::handleKeyboardBroadcastEvent)); + EVENTQUEUE->adoptHandler(getLockCursorToScreenEvent(), + m_inputFilter, + new TMethodEventJob(this, + &CServer::handleLockCursorToScreenEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getFakeInputBeginEvent(), + m_inputFilter, + new TMethodEventJob(this, + &CServer::handleFakeInputBeginEvent)); + EVENTQUEUE->adoptHandler(IPlatformScreen::getFakeInputEndEvent(), + m_inputFilter, + new TMethodEventJob(this, + &CServer::handleFakeInputEndEvent)); + + // add connection + addClient(m_primaryClient); + + // set initial configuration + setConfig(config); + + // enable primary client + m_primaryClient->enable(); + m_inputFilter->setPrimaryClient(m_primaryClient); + + // Determine if scroll lock is already set. If so, lock the cursor to the primary screen + int keyValue = m_primaryClient->getToggleMask (); + if (m_primaryClient->getToggleMask () & KeyModifierScrollLock) { + LOG((CLOG_DEBUG "scroll lock on initially. locked to screen")); + m_lockedToScreen = true; + } + +} + +CServer::~CServer() +{ + // remove event handlers and timers + EVENTQUEUE->removeHandler(IPlatformScreen::getKeyDownEvent(*EVENTQUEUE), + m_inputFilter); + EVENTQUEUE->removeHandler(IPlatformScreen::getKeyUpEvent(*EVENTQUEUE), + m_inputFilter); + EVENTQUEUE->removeHandler(IPlatformScreen::getKeyRepeatEvent(*EVENTQUEUE), + m_inputFilter); + EVENTQUEUE->removeHandler(IPlatformScreen::getButtonDownEvent(), + m_inputFilter); + EVENTQUEUE->removeHandler(IPlatformScreen::getButtonUpEvent(), + m_inputFilter); + EVENTQUEUE->removeHandler(IPlatformScreen::getMotionOnPrimaryEvent(), + m_primaryClient->getEventTarget()); + EVENTQUEUE->removeHandler(IPlatformScreen::getMotionOnSecondaryEvent(), + m_primaryClient->getEventTarget()); + EVENTQUEUE->removeHandler(IPlatformScreen::getWheelEvent(), + m_primaryClient->getEventTarget()); + EVENTQUEUE->removeHandler(IPlatformScreen::getScreensaverActivatedEvent(), + m_primaryClient->getEventTarget()); + EVENTQUEUE->removeHandler(IPlatformScreen::getScreensaverDeactivatedEvent(), + m_primaryClient->getEventTarget()); + EVENTQUEUE->removeHandler(IPlatformScreen::getFakeInputBeginEvent(), + m_inputFilter); + EVENTQUEUE->removeHandler(IPlatformScreen::getFakeInputEndEvent(), + m_inputFilter); + EVENTQUEUE->removeHandler(CEvent::kTimer, this); + stopSwitch(); + + // force immediate disconnection of secondary clients + disconnect(); + for (COldClients::iterator index = m_oldClients.begin(); + index != m_oldClients.begin(); ++index) { + CBaseClientProxy* client = index->first; + EVENTQUEUE->deleteTimer(index->second); + EVENTQUEUE->removeHandler(CEvent::kTimer, client); + EVENTQUEUE->removeHandler(CClientProxy::getDisconnectedEvent(), client); + delete client; + } + + // remove input filter + m_inputFilter->setPrimaryClient(NULL); + + // disable and disconnect primary client + m_primaryClient->disable(); + removeClient(m_primaryClient); +} + +bool +CServer::setConfig(const CConfig& config) +{ + // refuse configuration if it doesn't include the primary screen + if (!config.isScreen(m_primaryClient->getName())) { + return false; + } + + // close clients that are connected but being dropped from the + // configuration. + closeClients(config); + + // cut over + m_config = config; + processOptions(); + + // add ScrollLock as a hotkey to lock to the screen. this was a + // built-in feature in earlier releases and is now supported via + // the user configurable hotkey mechanism. if the user has already + // registered ScrollLock for something else then that will win but + // we will unfortunately generate a warning. if the user has + // configured a CLockCursorToScreenAction then we don't add + // ScrollLock as a hotkey. + if (!m_config.hasLockToScreenAction()) { + IPlatformScreen::CKeyInfo* key = + IPlatformScreen::CKeyInfo::alloc(kKeyScrollLock, 0, 0, 0); + CInputFilter::CRule rule(new CInputFilter::CKeystrokeCondition(key)); + rule.adoptAction(new CInputFilter::CLockCursorToScreenAction, true); + m_inputFilter->addFilterRule(rule); + } + + // tell primary screen about reconfiguration + m_primaryClient->reconfigure(getActivePrimarySides()); + + // tell all (connected) clients about current options + for (CClientList::const_iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + CBaseClientProxy* client = index->second; + sendOptions(client); + } + + return true; +} + +void +CServer::adoptClient(CBaseClientProxy* client) +{ + assert(client != NULL); + + // watch for client disconnection + EVENTQUEUE->adoptHandler(CClientProxy::getDisconnectedEvent(), client, + new TMethodEventJob(this, + &CServer::handleClientDisconnected, client)); + + // name must be in our configuration + if (!m_config.isScreen(client->getName())) { + LOG((CLOG_WARN "unrecognised client name \"%s\", check server config", client->getName().c_str())); + closeClient(client, kMsgEUnknown); + return; + } + + // add client to client list + if (!addClient(client)) { + // can only have one screen with a given name at any given time + LOG((CLOG_WARN "a client with name \"%s\" is already connected", getName(client).c_str())); + closeClient(client, kMsgEBusy); + return; + } + LOG((CLOG_NOTE "client \"%s\" has connected", getName(client).c_str())); + + // send configuration options to client + sendOptions(client); + + // activate screen saver on new client if active on the primary screen + if (m_activeSaver != NULL) { + client->screensaver(true); + } + + // send notification + CServer::CScreenConnectedInfo* info = + new CServer::CScreenConnectedInfo(getName(client)); + EVENTQUEUE->addEvent(CEvent(CServer::getConnectedEvent(), + m_primaryClient->getEventTarget(), info)); +} + +void +CServer::disconnect() +{ + // close all secondary clients + if (m_clients.size() > 1 || !m_oldClients.empty()) { + CConfig emptyConfig; + closeClients(emptyConfig); + } + else { + EVENTQUEUE->addEvent(CEvent(getDisconnectedEvent(), this)); + } +} + +void +CServer::gameDeviceTimingResp(UInt16 freq) +{ + m_screen->gameDeviceTimingResp(freq); +} + +void +CServer::gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2) +{ + m_screen->gameDeviceFeedback(id, m1, m2); +} + +UInt32 +CServer::getNumClients() const +{ + return (SInt32)m_clients.size(); +} + +void +CServer::getClients(std::vector& list) const +{ + list.clear(); + for (CClientList::const_iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + list.push_back(index->first); + } +} + +CEvent::Type +CServer::getErrorEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_errorEvent, + "CServer::error"); +} + +CEvent::Type +CServer::getConnectedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_connectedEvent, + "CServer::connected"); +} + +CEvent::Type +CServer::getDisconnectedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_disconnectedEvent, + "CServer::disconnected"); +} + +CEvent::Type +CServer::getSwitchToScreenEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_switchToScreen, + "CServer::switchToScreen"); +} + +CEvent::Type +CServer::getSwitchInDirectionEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_switchInDirection, + "CServer::switchInDirection"); +} + +CEvent::Type +CServer::getKeyboardBroadcastEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_keyboardBroadcast, + "CServer:keyboardBroadcast"); +} + +CEvent::Type +CServer::getLockCursorToScreenEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_lockCursorToScreen, + "CServer::lockCursorToScreen"); +} + +CEvent::Type +CServer::getScreenSwitchedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_screenSwitched, + "CServer::screenSwitched"); +} + +CString +CServer::getName(const CBaseClientProxy* client) const +{ + CString name = m_config.getCanonicalName(client->getName()); + if (name.empty()) { + name = client->getName(); + } + return name; +} + +UInt32 +CServer::getActivePrimarySides() const +{ + UInt32 sides = 0; + if (!isLockedToScreenServer()) { + if (hasAnyNeighbor(m_primaryClient, kLeft)) { + sides |= kLeftMask; + } + if (hasAnyNeighbor(m_primaryClient, kRight)) { + sides |= kRightMask; + } + if (hasAnyNeighbor(m_primaryClient, kTop)) { + sides |= kTopMask; + } + if (hasAnyNeighbor(m_primaryClient, kBottom)) { + sides |= kBottomMask; + } + } + return sides; +} + +bool +CServer::isLockedToScreenServer() const +{ + // locked if scroll-lock is toggled on + return m_lockedToScreen; +} + +bool +CServer::isLockedToScreen() const +{ + // locked if we say we're locked + if (isLockedToScreenServer()) { + LOG((CLOG_DEBUG "locked to screen")); + return true; + } + + // locked if primary says we're locked + if (m_primaryClient->isLockedToScreen()) { + return true; + } + + // not locked + return false; +} + +SInt32 +CServer::getJumpZoneSize(CBaseClientProxy* client) const +{ + if (client == m_primaryClient) { + return m_primaryClient->getJumpZoneSize(); + } + else { + return 0; + } +} + +void +CServer::switchScreen(CBaseClientProxy* dst, + SInt32 x, SInt32 y, bool forScreensaver) +{ + assert(dst != NULL); +#ifndef NDEBUG + { + SInt32 dx, dy, dw, dh; + dst->getShape(dx, dy, dw, dh); + assert(x >= dx && y >= dy && x < dx + dw && y < dy + dh); + } +#endif + assert(m_active != NULL); + + LOG((CLOG_INFO "switch from \"%s\" to \"%s\" at %d,%d", getName(m_active).c_str(), getName(dst).c_str(), x, y)); + + // stop waiting to switch + stopSwitch(); + + // record new position + m_x = x; + m_y = y; + m_xDelta = 0; + m_yDelta = 0; + m_xDelta2 = 0; + m_yDelta2 = 0; + + // wrapping means leaving the active screen and entering it again. + // since that's a waste of time we skip that and just warp the + // mouse. + if (m_active != dst) { + // leave active screen + if (!m_active->leave()) { + // cannot leave screen + LOG((CLOG_WARN "can't leave screen")); + return; + } + + // update the primary client's clipboards if we're leaving the + // primary screen. + if (m_active == m_primaryClient) { + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + CClipboardInfo& clipboard = m_clipboards[id]; + if (clipboard.m_clipboardOwner == getName(m_primaryClient)) { + onClipboardChanged(m_primaryClient, + id, clipboard.m_clipboardSeqNum); + } + } + } + + // cut over + m_active = dst; + + // increment enter sequence number + ++m_seqNum; + + // enter new screen + m_active->enter(x, y, m_seqNum, + m_primaryClient->getToggleMask(), + forScreensaver); + + // send the clipboard data to new active screen + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + m_active->setClipboard(id, &m_clipboards[id].m_clipboard); + } + + CServer::CSwitchToScreenInfo* info = + CServer::CSwitchToScreenInfo::alloc(m_active->getName()); + EVENTQUEUE->addEvent(CEvent(CServer::getScreenSwitchedEvent(), this, info)); + } + else { + m_active->mouseMove(x, y); + } +} + +void +CServer::jumpToScreen(CBaseClientProxy* newScreen) +{ + assert(newScreen != NULL); + + // record the current cursor position on the active screen + m_active->setJumpCursorPos(m_x, m_y); + + // get the last cursor position on the target screen + SInt32 x, y; + newScreen->getJumpCursorPos(x, y); + + switchScreen(newScreen, x, y, false); +} + +float +CServer::mapToFraction(CBaseClientProxy* client, + EDirection dir, SInt32 x, SInt32 y) const +{ + SInt32 sx, sy, sw, sh; + client->getShape(sx, sy, sw, sh); + switch (dir) { + case kLeft: + case kRight: + return static_cast(y - sy + 0.5f) / static_cast(sh); + + case kTop: + case kBottom: + return static_cast(x - sx + 0.5f) / static_cast(sw); + + case kNoDirection: + assert(0 && "bad direction"); + break; + } + return 0.0f; +} + +void +CServer::mapToPixel(CBaseClientProxy* client, + EDirection dir, float f, SInt32& x, SInt32& y) const +{ + SInt32 sx, sy, sw, sh; + client->getShape(sx, sy, sw, sh); + switch (dir) { + case kLeft: + case kRight: + y = static_cast(f * sh) + sy; + break; + + case kTop: + case kBottom: + x = static_cast(f * sw) + sx; + break; + + case kNoDirection: + assert(0 && "bad direction"); + break; + } +} + +bool +CServer::hasAnyNeighbor(CBaseClientProxy* client, EDirection dir) const +{ + assert(client != NULL); + + return m_config.hasNeighbor(getName(client), dir); +} + +CBaseClientProxy* +CServer::getNeighbor(CBaseClientProxy* src, + EDirection dir, SInt32& x, SInt32& y) const +{ + // note -- must be locked on entry + + assert(src != NULL); + + // get source screen name + CString srcName = getName(src); + assert(!srcName.empty()); + LOG((CLOG_DEBUG2 "find neighbor on %s of \"%s\"", CConfig::dirName(dir), srcName.c_str())); + + // convert position to fraction + float t = mapToFraction(src, dir, x, y); + + // search for the closest neighbor that exists in direction dir + float tTmp; + for (;;) { + CString dstName(m_config.getNeighbor(srcName, dir, t, &tTmp)); + + // if nothing in that direction then return NULL. if the + // destination is the source then we can make no more + // progress in this direction. since we haven't found a + // connected neighbor we return NULL. + if (dstName.empty()) { + LOG((CLOG_DEBUG2 "no neighbor on %s of \"%s\"", CConfig::dirName(dir), srcName.c_str())); + return NULL; + } + + // look up neighbor cell. if the screen is connected and + // ready then we can stop. + CClientList::const_iterator index = m_clients.find(dstName); + if (index != m_clients.end()) { + LOG((CLOG_DEBUG2 "\"%s\" is on %s of \"%s\" at %f", dstName.c_str(), CConfig::dirName(dir), srcName.c_str(), t)); + mapToPixel(index->second, dir, tTmp, x, y); + return index->second; + } + + // skip over unconnected screen + LOG((CLOG_DEBUG2 "ignored \"%s\" on %s of \"%s\"", dstName.c_str(), CConfig::dirName(dir), srcName.c_str())); + srcName = dstName; + + // use position on skipped screen + t = tTmp; + } +} + +CBaseClientProxy* +CServer::mapToNeighbor(CBaseClientProxy* src, + EDirection srcSide, SInt32& x, SInt32& y) const +{ + // note -- must be locked on entry + + assert(src != NULL); + + // get the first neighbor + CBaseClientProxy* dst = getNeighbor(src, srcSide, x, y); + if (dst == NULL) { + return NULL; + } + + // get the source screen's size + SInt32 dx, dy, dw, dh; + CBaseClientProxy* lastGoodScreen = src; + lastGoodScreen->getShape(dx, dy, dw, dh); + + // find destination screen, adjusting x or y (but not both). the + // searches are done in a sort of canonical screen space where + // the upper-left corner is 0,0 for each screen. we adjust from + // actual to canonical position on entry to and from canonical to + // actual on exit from the search. + switch (srcSide) { + case kLeft: + x -= dx; + while (dst != NULL) { + lastGoodScreen = dst; + lastGoodScreen->getShape(dx, dy, dw, dh); + x += dw; + if (x >= 0) { + break; + } + LOG((CLOG_DEBUG2 "skipping over screen %s", getName(dst).c_str())); + dst = getNeighbor(lastGoodScreen, srcSide, x, y); + } + assert(lastGoodScreen != NULL); + x += dx; + break; + + case kRight: + x -= dx; + while (dst != NULL) { + x -= dw; + lastGoodScreen = dst; + lastGoodScreen->getShape(dx, dy, dw, dh); + if (x < dw) { + break; + } + LOG((CLOG_DEBUG2 "skipping over screen %s", getName(dst).c_str())); + dst = getNeighbor(lastGoodScreen, srcSide, x, y); + } + assert(lastGoodScreen != NULL); + x += dx; + break; + + case kTop: + y -= dy; + while (dst != NULL) { + lastGoodScreen = dst; + lastGoodScreen->getShape(dx, dy, dw, dh); + y += dh; + if (y >= 0) { + break; + } + LOG((CLOG_DEBUG2 "skipping over screen %s", getName(dst).c_str())); + dst = getNeighbor(lastGoodScreen, srcSide, x, y); + } + assert(lastGoodScreen != NULL); + y += dy; + break; + + case kBottom: + y -= dy; + while (dst != NULL) { + y -= dh; + lastGoodScreen = dst; + lastGoodScreen->getShape(dx, dy, dw, dh); + if (y < dh) { + break; + } + LOG((CLOG_DEBUG2 "skipping over screen %s", getName(dst).c_str())); + dst = getNeighbor(lastGoodScreen, srcSide, x, y); + } + assert(lastGoodScreen != NULL); + y += dy; + break; + + case kNoDirection: + assert(0 && "bad direction"); + return NULL; + } + + // save destination screen + assert(lastGoodScreen != NULL); + dst = lastGoodScreen; + + // if entering primary screen then be sure to move in far enough + // to avoid the jump zone. if entering a side that doesn't have + // a neighbor (i.e. an asymmetrical side) then we don't need to + // move inwards because that side can't provoke a jump. + avoidJumpZone(dst, srcSide, x, y); + + return dst; +} + +void +CServer::avoidJumpZone(CBaseClientProxy* dst, + EDirection dir, SInt32& x, SInt32& y) const +{ + // we only need to avoid jump zones on the primary screen + if (dst != m_primaryClient) { + return; + } + + const CString dstName(getName(dst)); + SInt32 dx, dy, dw, dh; + dst->getShape(dx, dy, dw, dh); + float t = mapToFraction(dst, dir, x, y); + SInt32 z = getJumpZoneSize(dst); + + // move in far enough to avoid the jump zone. if entering a side + // that doesn't have a neighbor (i.e. an asymmetrical side) then we + // don't need to move inwards because that side can't provoke a jump. + switch (dir) { + case kLeft: + if (!m_config.getNeighbor(dstName, kRight, t, NULL).empty() && + x > dx + dw - 1 - z) + x = dx + dw - 1 - z; + break; + + case kRight: + if (!m_config.getNeighbor(dstName, kLeft, t, NULL).empty() && + x < dx + z) + x = dx + z; + break; + + case kTop: + if (!m_config.getNeighbor(dstName, kBottom, t, NULL).empty() && + y > dy + dh - 1 - z) + y = dy + dh - 1 - z; + break; + + case kBottom: + if (!m_config.getNeighbor(dstName, kTop, t, NULL).empty() && + y < dy + z) + y = dy + z; + break; + + case kNoDirection: + assert(0 && "bad direction"); + } +} + +bool +CServer::isSwitchOkay(CBaseClientProxy* newScreen, + EDirection dir, SInt32 x, SInt32 y, + SInt32 xActive, SInt32 yActive) +{ + LOG((CLOG_DEBUG1 "try to leave \"%s\" on %s", getName(m_active).c_str(), CConfig::dirName(dir))); + + // is there a neighbor? + if (newScreen == NULL) { + // there's no neighbor. we don't want to switch and we don't + // want to try to switch later. + LOG((CLOG_DEBUG1 "no neighbor %s", CConfig::dirName(dir))); + stopSwitch(); + return false; + } + + // should we switch or not? + bool preventSwitch = false; + bool allowSwitch = false; + + // note if the switch direction has changed. save the new + // direction and screen if so. + bool isNewDirection = (dir != m_switchDir); + if (isNewDirection || m_switchScreen == NULL) { + m_switchDir = dir; + m_switchScreen = newScreen; + } + + // is this a double tap and do we care? + if (!allowSwitch && m_switchTwoTapDelay > 0.0) { + if (isNewDirection || + !isSwitchTwoTapStarted() || !shouldSwitchTwoTap()) { + // tapping a different or new edge or second tap not + // fast enough. prepare for second tap. + preventSwitch = true; + startSwitchTwoTap(); + } + else { + // got second tap + allowSwitch = true; + } + } + + // if waiting before a switch then prepare to switch later + if (!allowSwitch && m_switchWaitDelay > 0.0) { + if (isNewDirection || !isSwitchWaitStarted()) { + startSwitchWait(x, y); + } + preventSwitch = true; + } + + // are we in a locked corner? first check if screen has the option set + // and, if not, check the global options. + const CConfig::CScreenOptions* options = + m_config.getOptions(getName(m_active)); + if (options == NULL || options->count(kOptionScreenSwitchCorners) == 0) { + options = m_config.getOptions(""); + } + if (options != NULL && options->count(kOptionScreenSwitchCorners) > 0) { + // get corner mask and size + CConfig::CScreenOptions::const_iterator i = + options->find(kOptionScreenSwitchCorners); + UInt32 corners = static_cast(i->second); + i = options->find(kOptionScreenSwitchCornerSize); + SInt32 size = 0; + if (i != options->end()) { + size = i->second; + } + + // see if we're in a locked corner + if ((getCorner(m_active, xActive, yActive, size) & corners) != 0) { + // yep, no switching + LOG((CLOG_DEBUG1 "locked in corner")); + preventSwitch = true; + stopSwitch(); + } + } + + // ignore if mouse is locked to screen and don't try to switch later + if (!preventSwitch && isLockedToScreen()) { + LOG((CLOG_DEBUG1 "locked to screen")); + preventSwitch = true; + stopSwitch(); + } + + // check for optional needed modifiers + KeyModifierMask mods = this->m_primaryClient->getToggleMask( ); + + if (!preventSwitch && ( + (this->m_switchNeedsShift && ((mods & KeyModifierShift) != KeyModifierShift)) || + (this->m_switchNeedsControl && ((mods & KeyModifierControl) != KeyModifierControl)) || + (this->m_switchNeedsAlt && ((mods & KeyModifierAlt) != KeyModifierAlt)) + )) { + LOG((CLOG_DEBUG1 "need modifiers to switch")); + preventSwitch = true; + stopSwitch(); + } + + return !preventSwitch; +} + +void +CServer::noSwitch(SInt32 x, SInt32 y) +{ + armSwitchTwoTap(x, y); + stopSwitchWait(); +} + +void +CServer::stopSwitch() +{ + if (m_switchScreen != NULL) { + m_switchScreen = NULL; + m_switchDir = kNoDirection; + stopSwitchTwoTap(); + stopSwitchWait(); + } +} + +void +CServer::startSwitchTwoTap() +{ + m_switchTwoTapEngaged = true; + m_switchTwoTapArmed = false; + m_switchTwoTapTimer.reset(); + LOG((CLOG_DEBUG1 "waiting for second tap")); +} + +void +CServer::armSwitchTwoTap(SInt32 x, SInt32 y) +{ + if (m_switchTwoTapEngaged) { + if (m_switchTwoTapTimer.getTime() > m_switchTwoTapDelay) { + // second tap took too long. disengage. + stopSwitchTwoTap(); + } + else if (!m_switchTwoTapArmed) { + // still time for a double tap. see if we left the tap + // zone and, if so, arm the two tap. + SInt32 ax, ay, aw, ah; + m_active->getShape(ax, ay, aw, ah); + SInt32 tapZone = m_primaryClient->getJumpZoneSize(); + if (tapZone < m_switchTwoTapZone) { + tapZone = m_switchTwoTapZone; + } + if (x >= ax + tapZone && x < ax + aw - tapZone && + y >= ay + tapZone && y < ay + ah - tapZone) { + // win32 can generate bogus mouse events that appear to + // move in the opposite direction that the mouse actually + // moved. try to ignore that crap here. + switch (m_switchDir) { + case kLeft: + m_switchTwoTapArmed = (m_xDelta > 0 && m_xDelta2 > 0); + break; + + case kRight: + m_switchTwoTapArmed = (m_xDelta < 0 && m_xDelta2 < 0); + break; + + case kTop: + m_switchTwoTapArmed = (m_yDelta > 0 && m_yDelta2 > 0); + break; + + case kBottom: + m_switchTwoTapArmed = (m_yDelta < 0 && m_yDelta2 < 0); + break; + + default: + break; + } + } + } + } +} + +void +CServer::stopSwitchTwoTap() +{ + m_switchTwoTapEngaged = false; + m_switchTwoTapArmed = false; +} + +bool +CServer::isSwitchTwoTapStarted() const +{ + return m_switchTwoTapEngaged; +} + +bool +CServer::shouldSwitchTwoTap() const +{ + // this is the second tap if two-tap is armed and this tap + // came fast enough + return (m_switchTwoTapArmed && + m_switchTwoTapTimer.getTime() <= m_switchTwoTapDelay); +} + +void +CServer::startSwitchWait(SInt32 x, SInt32 y) +{ + stopSwitchWait(); + m_switchWaitX = x; + m_switchWaitY = y; + m_switchWaitTimer = EVENTQUEUE->newOneShotTimer(m_switchWaitDelay, this); + LOG((CLOG_DEBUG1 "waiting to switch")); +} + +void +CServer::stopSwitchWait() +{ + if (m_switchWaitTimer != NULL) { + EVENTQUEUE->deleteTimer(m_switchWaitTimer); + m_switchWaitTimer = NULL; + } +} + +bool +CServer::isSwitchWaitStarted() const +{ + return (m_switchWaitTimer != NULL); +} + +UInt32 +CServer::getCorner(CBaseClientProxy* client, + SInt32 x, SInt32 y, SInt32 size) const +{ + assert(client != NULL); + + // get client screen shape + SInt32 ax, ay, aw, ah; + client->getShape(ax, ay, aw, ah); + + // check for x,y on the left or right + SInt32 xSide; + if (x <= ax) { + xSide = -1; + } + else if (x >= ax + aw - 1) { + xSide = 1; + } + else { + xSide = 0; + } + + // check for x,y on the top or bottom + SInt32 ySide; + if (y <= ay) { + ySide = -1; + } + else if (y >= ay + ah - 1) { + ySide = 1; + } + else { + ySide = 0; + } + + // if against the left or right then check if y is within size + if (xSide != 0) { + if (y < ay + size) { + return (xSide < 0) ? kTopLeftMask : kTopRightMask; + } + else if (y >= ay + ah - size) { + return (xSide < 0) ? kBottomLeftMask : kBottomRightMask; + } + } + + // if against the left or right then check if y is within size + if (ySide != 0) { + if (x < ax + size) { + return (ySide < 0) ? kTopLeftMask : kBottomLeftMask; + } + else if (x >= ax + aw - size) { + return (ySide < 0) ? kTopRightMask : kBottomRightMask; + } + } + + return kNoCornerMask; +} + +void +CServer::stopRelativeMoves() +{ + if (m_relativeMoves && m_active != m_primaryClient) { + // warp to the center of the active client so we know where we are + SInt32 ax, ay, aw, ah; + m_active->getShape(ax, ay, aw, ah); + m_x = ax + (aw >> 1); + m_y = ay + (ah >> 1); + m_xDelta = 0; + m_yDelta = 0; + m_xDelta2 = 0; + m_yDelta2 = 0; + LOG((CLOG_DEBUG2 "synchronize move on %s by %d,%d", getName(m_active).c_str(), m_x, m_y)); + m_active->mouseMove(m_x, m_y); + } +} + +void +CServer::sendOptions(CBaseClientProxy* client) const +{ + COptionsList optionsList; + + // look up options for client + const CConfig::CScreenOptions* options = + m_config.getOptions(getName(client)); + if (options != NULL) { + // convert options to a more convenient form for sending + optionsList.reserve(2 * options->size()); + for (CConfig::CScreenOptions::const_iterator index = options->begin(); + index != options->end(); ++index) { + optionsList.push_back(index->first); + optionsList.push_back(static_cast(index->second)); + } + } + + // look up global options + options = m_config.getOptions(""); + if (options != NULL) { + // convert options to a more convenient form for sending + optionsList.reserve(optionsList.size() + 2 * options->size()); + for (CConfig::CScreenOptions::const_iterator index = options->begin(); + index != options->end(); ++index) { + optionsList.push_back(index->first); + optionsList.push_back(static_cast(index->second)); + } + } + + // send the options + client->resetOptions(); + client->setOptions(optionsList); +} + +void +CServer::processOptions() +{ + const CConfig::CScreenOptions* options = m_config.getOptions(""); + if (options == NULL) { + return; + } + + m_switchNeedsShift = false; // it seems if i don't add these + m_switchNeedsControl = false; // lines, the 'reload config' option + m_switchNeedsAlt = false; // doesnt' work correct. + + bool newRelativeMoves = m_relativeMoves; + for (CConfig::CScreenOptions::const_iterator index = options->begin(); + index != options->end(); ++index) { + const OptionID id = index->first; + const OptionValue value = index->second; + if (id == kOptionScreenSwitchDelay) { + m_switchWaitDelay = 1.0e-3 * static_cast(value); + if (m_switchWaitDelay < 0.0) { + m_switchWaitDelay = 0.0; + } + stopSwitchWait(); + } + else if (id == kOptionScreenSwitchTwoTap) { + m_switchTwoTapDelay = 1.0e-3 * static_cast(value); + if (m_switchTwoTapDelay < 0.0) { + m_switchTwoTapDelay = 0.0; + } + stopSwitchTwoTap(); + } + else if (id == kOptionScreenSwitchNeedsControl) { + m_switchNeedsControl = (value != 0); + } + else if (id == kOptionScreenSwitchNeedsShift) { + m_switchNeedsShift = (value != 0); + } + else if (id == kOptionScreenSwitchNeedsAlt) { + m_switchNeedsAlt = (value != 0); + } + else if (id == kOptionRelativeMouseMoves) { + newRelativeMoves = (value != 0); + } + } + if (m_relativeMoves && !newRelativeMoves) { + stopRelativeMoves(); + } + m_relativeMoves = newRelativeMoves; +} + +void +CServer::handleShapeChanged(const CEvent&, void* vclient) +{ + // ignore events from unknown clients + CBaseClientProxy* client = reinterpret_cast(vclient); + if (m_clientSet.count(client) == 0) { + return; + } + + LOG((CLOG_INFO "screen \"%s\" shape changed", getName(client).c_str())); + + // update jump coordinate + SInt32 x, y; + client->getCursorPos(x, y); + client->setJumpCursorPos(x, y); + + // update the mouse coordinates + if (client == m_active) { + m_x = x; + m_y = y; + } + + // handle resolution change to primary screen + if (client == m_primaryClient) { + if (client == m_active) { + onMouseMovePrimary(m_x, m_y); + } + else { + onMouseMoveSecondary(0, 0); + } + } +} + +void +CServer::handleClipboardGrabbed(const CEvent& event, void* vclient) +{ + // ignore events from unknown clients + CBaseClientProxy* grabber = reinterpret_cast(vclient); + if (m_clientSet.count(grabber) == 0) { + return; + } + const IScreen::CClipboardInfo* info = + reinterpret_cast(event.getData()); + + // ignore grab if sequence number is old. always allow primary + // screen to grab. + CClipboardInfo& clipboard = m_clipboards[info->m_id]; + if (grabber != m_primaryClient && + info->m_sequenceNumber < clipboard.m_clipboardSeqNum) { + LOG((CLOG_INFO "ignored screen \"%s\" grab of clipboard %d", getName(grabber).c_str(), info->m_id)); + return; + } + + // mark screen as owning clipboard + LOG((CLOG_INFO "screen \"%s\" grabbed clipboard %d from \"%s\"", getName(grabber).c_str(), info->m_id, clipboard.m_clipboardOwner.c_str())); + clipboard.m_clipboardOwner = getName(grabber); + clipboard.m_clipboardSeqNum = info->m_sequenceNumber; + + // clear the clipboard data (since it's not known at this point) + if (clipboard.m_clipboard.open(0)) { + clipboard.m_clipboard.empty(); + clipboard.m_clipboard.close(); + } + clipboard.m_clipboardData = clipboard.m_clipboard.marshall(); + + // tell all other screens to take ownership of clipboard. tell the + // grabber that it's clipboard isn't dirty. + for (CClientList::iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + CBaseClientProxy* client = index->second; + if (client == grabber) { + client->setClipboardDirty(info->m_id, false); + } + else { + client->grabClipboard(info->m_id); + } + } +} + +void +CServer::handleClipboardChanged(const CEvent& event, void* vclient) +{ + // ignore events from unknown clients + CBaseClientProxy* sender = reinterpret_cast(vclient); + if (m_clientSet.count(sender) == 0) { + return; + } + const IScreen::CClipboardInfo* info = + reinterpret_cast(event.getData()); + onClipboardChanged(sender, info->m_id, info->m_sequenceNumber); +} + +void +CServer::handleKeyDownEvent(const CEvent& event, void*) +{ + IPlatformScreen::CKeyInfo* info = + reinterpret_cast(event.getData()); + onKeyDown(info->m_key, info->m_mask, info->m_button, info->m_screens); +} + +void +CServer::handleKeyUpEvent(const CEvent& event, void*) +{ + IPlatformScreen::CKeyInfo* info = + reinterpret_cast(event.getData()); + onKeyUp(info->m_key, info->m_mask, info->m_button, info->m_screens); +} + +void +CServer::handleKeyRepeatEvent(const CEvent& event, void*) +{ + IPlatformScreen::CKeyInfo* info = + reinterpret_cast(event.getData()); + onKeyRepeat(info->m_key, info->m_mask, info->m_count, info->m_button); +} + +void +CServer::handleButtonDownEvent(const CEvent& event, void*) +{ + IPlatformScreen::CButtonInfo* info = + reinterpret_cast(event.getData()); + onMouseDown(info->m_button); +} + +void +CServer::handleButtonUpEvent(const CEvent& event, void*) +{ + IPlatformScreen::CButtonInfo* info = + reinterpret_cast(event.getData()); + onMouseUp(info->m_button); +} + +void +CServer::handleMotionPrimaryEvent(const CEvent& event, void*) +{ + IPlatformScreen::CMotionInfo* info = + reinterpret_cast(event.getData()); + onMouseMovePrimary(info->m_x, info->m_y); +} + +void +CServer::handleMotionSecondaryEvent(const CEvent& event, void*) +{ + IPlatformScreen::CMotionInfo* info = + reinterpret_cast(event.getData()); + onMouseMoveSecondary(info->m_x, info->m_y); +} + +void +CServer::handleWheelEvent(const CEvent& event, void*) +{ + IPlatformScreen::CWheelInfo* info = + reinterpret_cast(event.getData()); + onMouseWheel(info->m_xDelta, info->m_yDelta); +} + +void +CServer::handleGameDeviceButtons(const CEvent& event, void*) +{ + IPlatformScreen::CGameDeviceButtonInfo* info = + reinterpret_cast(event.getData()); + onGameDeviceButtons(info->m_id, info->m_buttons); +} + +void +CServer::handleGameDeviceSticks(const CEvent& event, void*) +{ + IPlatformScreen::CGameDeviceStickInfo* info = + reinterpret_cast(event.getData()); + onGameDeviceSticks(info->m_id, info->m_x1, info->m_y1, info->m_x2, info->m_y2); +} + +void +CServer::handleGameDeviceTriggers(const CEvent& event, void*) +{ + IPlatformScreen::CGameDeviceTriggerInfo* info = + reinterpret_cast(event.getData()); + onGameDeviceTriggers(info->m_id, info->m_t1, info->m_t2); +} + +void +CServer::handleGameDeviceTimingReq(const CEvent& event, void*) +{ + onGameDeviceTimingReq(); +} + +void +CServer::handleScreensaverActivatedEvent(const CEvent&, void*) +{ + onScreensaver(true); +} + +void +CServer::handleScreensaverDeactivatedEvent(const CEvent&, void*) +{ + onScreensaver(false); +} + +void +CServer::handleSwitchWaitTimeout(const CEvent&, void*) +{ + // ignore if mouse is locked to screen + if (isLockedToScreen()) { + LOG((CLOG_DEBUG1 "locked to screen")); + stopSwitch(); + return; + } + + // switch screen + switchScreen(m_switchScreen, m_switchWaitX, m_switchWaitY, false); +} + +void +CServer::handleClientDisconnected(const CEvent&, void* vclient) +{ + // client has disconnected. it might be an old client or an + // active client. we don't care so just handle it both ways. + CBaseClientProxy* client = reinterpret_cast(vclient); + removeActiveClient(client); + removeOldClient(client); + delete client; +} + +void +CServer::handleClientCloseTimeout(const CEvent&, void* vclient) +{ + // client took too long to disconnect. just dump it. + CBaseClientProxy* client = reinterpret_cast(vclient); + LOG((CLOG_NOTE "forced disconnection of client \"%s\"", getName(client).c_str())); + removeOldClient(client); + delete client; +} + +void +CServer::handleSwitchToScreenEvent(const CEvent& event, void*) +{ + CSwitchToScreenInfo* info = + reinterpret_cast(event.getData()); + + CClientList::const_iterator index = m_clients.find(info->m_screen); + if (index == m_clients.end()) { + LOG((CLOG_DEBUG1 "screen \"%s\" not active", info->m_screen)); + } + else { + jumpToScreen(index->second); + } +} + +void +CServer::handleSwitchInDirectionEvent(const CEvent& event, void*) +{ + CSwitchInDirectionInfo* info = + reinterpret_cast(event.getData()); + + // jump to screen in chosen direction from center of this screen + SInt32 x = m_x, y = m_y; + CBaseClientProxy* newScreen = + getNeighbor(m_active, info->m_direction, x, y); + if (newScreen == NULL) { + LOG((CLOG_DEBUG1 "no neighbor %s", CConfig::dirName(info->m_direction))); + } + else { + jumpToScreen(newScreen); + } +} + +void +CServer::handleKeyboardBroadcastEvent(const CEvent& event, void*) +{ + CKeyboardBroadcastInfo* info = (CKeyboardBroadcastInfo*)event.getData(); + + // choose new state + bool newState; + switch (info->m_state) { + case CKeyboardBroadcastInfo::kOff: + newState = false; + break; + + default: + case CKeyboardBroadcastInfo::kOn: + newState = true; + break; + + case CKeyboardBroadcastInfo::kToggle: + newState = !m_keyboardBroadcasting; + break; + } + + // enter new state + if (newState != m_keyboardBroadcasting || + info->m_screens != m_keyboardBroadcastingScreens) { + m_keyboardBroadcasting = newState; + m_keyboardBroadcastingScreens = info->m_screens; + LOG((CLOG_DEBUG "keyboard broadcasting %s: %s", m_keyboardBroadcasting ? "on" : "off", m_keyboardBroadcastingScreens.c_str())); + } +} + +void +CServer::handleLockCursorToScreenEvent(const CEvent& event, void*) +{ + CLockCursorToScreenInfo* info = (CLockCursorToScreenInfo*)event.getData(); + + // choose new state + bool newState; + switch (info->m_state) { + case CLockCursorToScreenInfo::kOff: + newState = false; + break; + + default: + case CLockCursorToScreenInfo::kOn: + newState = true; + break; + + case CLockCursorToScreenInfo::kToggle: + newState = !m_lockedToScreen; + break; + } + + // enter new state + if (newState != m_lockedToScreen) { + m_lockedToScreen = newState; + LOG((CLOG_DEBUG "cursor %s current screen", m_lockedToScreen ? "locked to" : "unlocked from")); + + m_primaryClient->reconfigure(getActivePrimarySides()); + if (!isLockedToScreenServer()) { + stopRelativeMoves(); + } + } +} + +void +CServer::handleFakeInputBeginEvent(const CEvent&, void*) +{ + m_primaryClient->fakeInputBegin(); +} + +void +CServer::handleFakeInputEndEvent(const CEvent&, void*) +{ + m_primaryClient->fakeInputEnd(); +} + +void +CServer::onClipboardChanged(CBaseClientProxy* sender, + ClipboardID id, UInt32 seqNum) +{ + CClipboardInfo& clipboard = m_clipboards[id]; + + // ignore update if sequence number is old + if (seqNum < clipboard.m_clipboardSeqNum) { + LOG((CLOG_INFO "ignored screen \"%s\" update of clipboard %d (missequenced)", getName(sender).c_str(), id)); + return; + } + + // should be the expected client + assert(sender == m_clients.find(clipboard.m_clipboardOwner)->second); + + // get data + sender->getClipboard(id, &clipboard.m_clipboard); + + // ignore if data hasn't changed + CString data = clipboard.m_clipboard.marshall(); + if (data == clipboard.m_clipboardData) { + LOG((CLOG_DEBUG "ignored screen \"%s\" update of clipboard %d (unchanged)", clipboard.m_clipboardOwner.c_str(), id)); + return; + } + + // got new data + LOG((CLOG_INFO "screen \"%s\" updated clipboard %d", clipboard.m_clipboardOwner.c_str(), id)); + clipboard.m_clipboardData = data; + + // tell all clients except the sender that the clipboard is dirty + for (CClientList::const_iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + CBaseClientProxy* client = index->second; + client->setClipboardDirty(id, client != sender); + } + + // send the new clipboard to the active screen + m_active->setClipboard(id, &clipboard.m_clipboard); +} + +void +CServer::onScreensaver(bool activated) +{ + LOG((CLOG_DEBUG "onScreenSaver %s", activated ? "activated" : "deactivated")); + + if (activated) { + // save current screen and position + m_activeSaver = m_active; + m_xSaver = m_x; + m_ySaver = m_y; + + // jump to primary screen + if (m_active != m_primaryClient) { + switchScreen(m_primaryClient, 0, 0, true); + } + } + else { + // jump back to previous screen and position. we must check + // that the position is still valid since the screen may have + // changed resolutions while the screen saver was running. + if (m_activeSaver != NULL && m_activeSaver != m_primaryClient) { + // check position + CBaseClientProxy* screen = m_activeSaver; + SInt32 x, y, w, h; + screen->getShape(x, y, w, h); + SInt32 zoneSize = getJumpZoneSize(screen); + if (m_xSaver < x + zoneSize) { + m_xSaver = x + zoneSize; + } + else if (m_xSaver >= x + w - zoneSize) { + m_xSaver = x + w - zoneSize - 1; + } + if (m_ySaver < y + zoneSize) { + m_ySaver = y + zoneSize; + } + else if (m_ySaver >= y + h - zoneSize) { + m_ySaver = y + h - zoneSize - 1; + } + + // jump + switchScreen(screen, m_xSaver, m_ySaver, false); + } + + // reset state + m_activeSaver = NULL; + } + + // send message to all clients + for (CClientList::const_iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + CBaseClientProxy* client = index->second; + client->screensaver(activated); + } +} + +void +CServer::onKeyDown(KeyID id, KeyModifierMask mask, KeyButton button, + const char* screens) +{ + LOG((CLOG_DEBUG1 "onKeyDown id=%d mask=0x%04x button=0x%04x", id, mask, button)); + assert(m_active != NULL); + + // relay + if (!m_keyboardBroadcasting && IKeyState::CKeyInfo::isDefault(screens)) { + m_active->keyDown(id, mask, button); + } + else { + if (!screens && m_keyboardBroadcasting) { + screens = m_keyboardBroadcastingScreens.c_str(); + if (IKeyState::CKeyInfo::isDefault(screens)) { + screens = "*"; + } + } + for (CClientList::const_iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + if (IKeyState::CKeyInfo::contains(screens, index->first)) { + index->second->keyDown(id, mask, button); + } + } + } +} + +void +CServer::onKeyUp(KeyID id, KeyModifierMask mask, KeyButton button, + const char* screens) +{ + LOG((CLOG_DEBUG1 "onKeyUp id=%d mask=0x%04x button=0x%04x", id, mask, button)); + assert(m_active != NULL); + + // relay + if (!m_keyboardBroadcasting && IKeyState::CKeyInfo::isDefault(screens)) { + m_active->keyUp(id, mask, button); + } + else { + if (!screens && m_keyboardBroadcasting) { + screens = m_keyboardBroadcastingScreens.c_str(); + if (IKeyState::CKeyInfo::isDefault(screens)) { + screens = "*"; + } + } + for (CClientList::const_iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + if (IKeyState::CKeyInfo::contains(screens, index->first)) { + index->second->keyUp(id, mask, button); + } + } + } +} + +void +CServer::onKeyRepeat(KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton button) +{ + LOG((CLOG_DEBUG1 "onKeyRepeat id=%d mask=0x%04x count=%d button=0x%04x", id, mask, count, button)); + assert(m_active != NULL); + + // relay + m_active->keyRepeat(id, mask, count, button); +} + +void +CServer::onMouseDown(ButtonID id) +{ + LOG((CLOG_DEBUG1 "onMouseDown id=%d", id)); + assert(m_active != NULL); + + // relay + m_active->mouseDown(id); +} + +void +CServer::onMouseUp(ButtonID id) +{ + LOG((CLOG_DEBUG1 "onMouseUp id=%d", id)); + assert(m_active != NULL); + + // relay + m_active->mouseUp(id); +} + +bool +CServer::onMouseMovePrimary(SInt32 x, SInt32 y) +{ + LOG((CLOG_DEBUG4 "onMouseMovePrimary %d,%d", x, y)); + + // mouse move on primary (server's) screen + if (m_active != m_primaryClient) { + // stale event -- we're actually on a secondary screen + return false; + } + + // save last delta + m_xDelta2 = m_xDelta; + m_yDelta2 = m_yDelta; + + // save current delta + m_xDelta = x - m_x; + m_yDelta = y - m_y; + + // save position + m_x = x; + m_y = y; + + // get screen shape + SInt32 ax, ay, aw, ah; + m_active->getShape(ax, ay, aw, ah); + SInt32 zoneSize = getJumpZoneSize(m_active); + + // clamp position to screen + SInt32 xc = x, yc = y; + if (xc < ax + zoneSize) { + xc = ax; + } + else if (xc >= ax + aw - zoneSize) { + xc = ax + aw - 1; + } + if (yc < ay + zoneSize) { + yc = ay; + } + else if (yc >= ay + ah - zoneSize) { + yc = ay + ah - 1; + } + + // see if we should change screens + EDirection dir; + if (x < ax + zoneSize) { + x -= zoneSize; + dir = kLeft; + } + else if (x >= ax + aw - zoneSize) { + x += zoneSize; + dir = kRight; + } + else if (y < ay + zoneSize) { + y -= zoneSize; + dir = kTop; + } + else if (y >= ay + ah - zoneSize) { + y += zoneSize; + dir = kBottom; + } + else { + // still on local screen + noSwitch(x, y); + return false; + } + + // get jump destination + CBaseClientProxy* newScreen = mapToNeighbor(m_active, dir, x, y); + + // should we switch or not? + if (isSwitchOkay(newScreen, dir, x, y, xc, yc)) { + // switch screen + switchScreen(newScreen, x, y, false); + return true; + } + else { + return false; + } +} + +void +CServer::onMouseMoveSecondary(SInt32 dx, SInt32 dy) +{ + LOG((CLOG_DEBUG2 "onMouseMoveSecondary %+d,%+d", dx, dy)); + + // mouse move on secondary (client's) screen + assert(m_active != NULL); + if (m_active == m_primaryClient) { + // stale event -- we're actually on the primary screen + return; + } + + // if doing relative motion on secondary screens and we're locked + // to the screen (which activates relative moves) then send a + // relative mouse motion. when we're doing this we pretend as if + // the mouse isn't actually moving because we're expecting some + // program on the secondary screen to warp the mouse on us, so we + // have no idea where it really is. + if (m_relativeMoves && isLockedToScreenServer()) { + LOG((CLOG_DEBUG2 "relative move on %s by %d,%d", getName(m_active).c_str(), dx, dy)); + m_active->mouseRelativeMove(dx, dy); + return; + } + + // save old position + const SInt32 xOld = m_x; + const SInt32 yOld = m_y; + + // save last delta + m_xDelta2 = m_xDelta; + m_yDelta2 = m_yDelta; + + // save current delta + m_xDelta = dx; + m_yDelta = dy; + + // accumulate motion + m_x += dx; + m_y += dy; + + // get screen shape + SInt32 ax, ay, aw, ah; + m_active->getShape(ax, ay, aw, ah); + + // find direction of neighbor and get the neighbor + bool jump = true; + CBaseClientProxy* newScreen; + do { + // clamp position to screen + SInt32 xc = m_x, yc = m_y; + if (xc < ax) { + xc = ax; + } + else if (xc >= ax + aw) { + xc = ax + aw - 1; + } + if (yc < ay) { + yc = ay; + } + else if (yc >= ay + ah) { + yc = ay + ah - 1; + } + + EDirection dir; + if (m_x < ax) { + dir = kLeft; + } + else if (m_x > ax + aw - 1) { + dir = kRight; + } + else if (m_y < ay) { + dir = kTop; + } + else if (m_y > ay + ah - 1) { + dir = kBottom; + } + else { + // we haven't left the screen + newScreen = m_active; + jump = false; + + // if waiting and mouse is not on the border we're waiting + // on then stop waiting. also if it's not on the border + // then arm the double tap. + if (m_switchScreen != NULL) { + bool clearWait; + SInt32 zoneSize = m_primaryClient->getJumpZoneSize(); + switch (m_switchDir) { + case kLeft: + clearWait = (m_x >= ax + zoneSize); + break; + + case kRight: + clearWait = (m_x <= ax + aw - 1 - zoneSize); + break; + + case kTop: + clearWait = (m_y >= ay + zoneSize); + break; + + case kBottom: + clearWait = (m_y <= ay + ah - 1 + zoneSize); + break; + + default: + clearWait = false; + break; + } + if (clearWait) { + // still on local screen + noSwitch(m_x, m_y); + } + } + + // skip rest of block + break; + } + + // try to switch screen. get the neighbor. + newScreen = mapToNeighbor(m_active, dir, m_x, m_y); + + // see if we should switch + if (!isSwitchOkay(newScreen, dir, m_x, m_y, xc, yc)) { + newScreen = m_active; + jump = false; + } + } while (false); + + if (jump) { + // switch screens + switchScreen(newScreen, m_x, m_y, false); + } + else { + // same screen. clamp mouse to edge. + m_x = xOld + dx; + m_y = yOld + dy; + if (m_x < ax) { + m_x = ax; + LOG((CLOG_DEBUG2 "clamp to left of \"%s\"", getName(m_active).c_str())); + } + else if (m_x > ax + aw - 1) { + m_x = ax + aw - 1; + LOG((CLOG_DEBUG2 "clamp to right of \"%s\"", getName(m_active).c_str())); + } + if (m_y < ay) { + m_y = ay; + LOG((CLOG_DEBUG2 "clamp to top of \"%s\"", getName(m_active).c_str())); + } + else if (m_y > ay + ah - 1) { + m_y = ay + ah - 1; + LOG((CLOG_DEBUG2 "clamp to bottom of \"%s\"", getName(m_active).c_str())); + } + + // warp cursor if it moved. + if (m_x != xOld || m_y != yOld) { + LOG((CLOG_DEBUG2 "move on %s to %d,%d", getName(m_active).c_str(), m_x, m_y)); + m_active->mouseMove(m_x, m_y); + } + } +} + +void +CServer::onMouseWheel(SInt32 xDelta, SInt32 yDelta) +{ + LOG((CLOG_DEBUG1 "onMouseWheel %+d,%+d", xDelta, yDelta)); + assert(m_active != NULL); + + // relay + m_active->mouseWheel(xDelta, yDelta); +} + +void +CServer::onGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) +{ + LOG((CLOG_DEBUG1 "onGameDeviceButtons id=%d buttons=%d", id, buttons)); + m_active->gameDeviceButtons(id, buttons); +} + +void +CServer::onGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) +{ + LOG((CLOG_DEBUG1 "onGameDeviceSticks id=%d s1=%+d,%+d s2=%+d,%+d", id, x1, y1, x2, y2)); + m_active->gameDeviceSticks(id, x1, y1, x2, y2); +} + +void +CServer::onGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) +{ + LOG((CLOG_DEBUG1 "onGameDeviceTriggers id=%d t1=%d t2=%d", id, t1, t2)); + m_active->gameDeviceTriggers(id, t1, t2); +} + +void +CServer::onGameDeviceTimingReq() +{ + LOG((CLOG_DEBUG1 "onGameDeviceTimingReq")); + m_active->gameDeviceTimingReq(); +} + +bool +CServer::addClient(CBaseClientProxy* client) +{ + CString name = getName(client); + if (m_clients.count(name) != 0) { + return false; + } + + // add event handlers + EVENTQUEUE->adoptHandler(IScreen::getShapeChangedEvent(), + client->getEventTarget(), + new TMethodEventJob(this, + &CServer::handleShapeChanged, client)); + EVENTQUEUE->adoptHandler(IScreen::getClipboardGrabbedEvent(), + client->getEventTarget(), + new TMethodEventJob(this, + &CServer::handleClipboardGrabbed, client)); + EVENTQUEUE->adoptHandler(CClientProxy::getClipboardChangedEvent(), + client->getEventTarget(), + new TMethodEventJob(this, + &CServer::handleClipboardChanged, client)); + + // add to list + m_clientSet.insert(client); + m_clients.insert(std::make_pair(name, client)); + + // initialize client data + SInt32 x, y; + client->getCursorPos(x, y); + client->setJumpCursorPos(x, y); + + // tell primary client about the active sides + m_primaryClient->reconfigure(getActivePrimarySides()); + + return true; +} + +bool +CServer::removeClient(CBaseClientProxy* client) +{ + // return false if not in list + CClientSet::iterator i = m_clientSet.find(client); + if (i == m_clientSet.end()) { + return false; + } + + // remove event handlers + EVENTQUEUE->removeHandler(IScreen::getShapeChangedEvent(), + client->getEventTarget()); + EVENTQUEUE->removeHandler(IScreen::getClipboardGrabbedEvent(), + client->getEventTarget()); + EVENTQUEUE->removeHandler(CClientProxy::getClipboardChangedEvent(), + client->getEventTarget()); + + // remove from list + m_clients.erase(getName(client)); + m_clientSet.erase(i); + + return true; +} + +void +CServer::closeClient(CBaseClientProxy* client, const char* msg) +{ + assert(client != m_primaryClient); + assert(msg != NULL); + + // send message to client. this message should cause the client + // to disconnect. we add this client to the closed client list + // and install a timer to remove the client if it doesn't respond + // quickly enough. we also remove the client from the active + // client list since we're not going to listen to it anymore. + // note that this method also works on clients that are not in + // the m_clients list. adoptClient() may call us with such a + // client. + LOG((CLOG_NOTE "disconnecting client \"%s\"", getName(client).c_str())); + + // send message + // FIXME -- avoid type cast (kinda hard, though) + ((CClientProxy*)client)->close(msg); + + // install timer. wait timeout seconds for client to close. + double timeout = 5.0; + CEventQueueTimer* timer = EVENTQUEUE->newOneShotTimer(timeout, NULL); + EVENTQUEUE->adoptHandler(CEvent::kTimer, timer, + new TMethodEventJob(this, + &CServer::handleClientCloseTimeout, client)); + + // move client to closing list + removeClient(client); + m_oldClients.insert(std::make_pair(client, timer)); + + // if this client is the active screen then we have to + // jump off of it + forceLeaveClient(client); +} + +void +CServer::closeClients(const CConfig& config) +{ + // collect the clients that are connected but are being dropped + // from the configuration (or who's canonical name is changing). + typedef std::set CRemovedClients; + CRemovedClients removed; + for (CClientList::iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + if (!config.isCanonicalName(index->first)) { + removed.insert(index->second); + } + } + + // don't close the primary client + removed.erase(m_primaryClient); + + // now close them. we collect the list then close in two steps + // because closeClient() modifies the collection we iterate over. + for (CRemovedClients::iterator index = removed.begin(); + index != removed.end(); ++index) { + closeClient(*index, kMsgCClose); + } +} + +void +CServer::removeActiveClient(CBaseClientProxy* client) +{ + if (removeClient(client)) { + forceLeaveClient(client); + EVENTQUEUE->removeHandler(CClientProxy::getDisconnectedEvent(), client); + if (m_clients.size() == 1 && m_oldClients.empty()) { + EVENTQUEUE->addEvent(CEvent(getDisconnectedEvent(), this)); + } + } +} + +void +CServer::removeOldClient(CBaseClientProxy* client) +{ + COldClients::iterator i = m_oldClients.find(client); + if (i != m_oldClients.end()) { + EVENTQUEUE->removeHandler(CClientProxy::getDisconnectedEvent(), client); + EVENTQUEUE->removeHandler(CEvent::kTimer, i->second); + EVENTQUEUE->deleteTimer(i->second); + m_oldClients.erase(i); + if (m_clients.size() == 1 && m_oldClients.empty()) { + EVENTQUEUE->addEvent(CEvent(getDisconnectedEvent(), this)); + } + } +} + +void +CServer::forceLeaveClient(CBaseClientProxy* client) +{ + CBaseClientProxy* active = + (m_activeSaver != NULL) ? m_activeSaver : m_active; + if (active == client) { + // record new position (center of primary screen) + m_primaryClient->getCursorCenter(m_x, m_y); + + // stop waiting to switch to this client + if (active == m_switchScreen) { + stopSwitch(); + } + + // don't notify active screen since it has probably already + // disconnected. + LOG((CLOG_INFO "jump from \"%s\" to \"%s\" at %d,%d", getName(active).c_str(), getName(m_primaryClient).c_str(), m_x, m_y)); + + // cut over + m_active = m_primaryClient; + + // enter new screen (unless we already have because of the + // screen saver) + if (m_activeSaver == NULL) { + m_primaryClient->enter(m_x, m_y, m_seqNum, + m_primaryClient->getToggleMask(), false); + } + } + + // if this screen had the cursor when the screen saver activated + // then we can't switch back to it when the screen saver + // deactivates. + if (m_activeSaver == client) { + m_activeSaver = NULL; + } + + // tell primary client about the active sides + m_primaryClient->reconfigure(getActivePrimarySides()); +} + + +// +// CServer::CClipboardInfo +// + +CServer::CClipboardInfo::CClipboardInfo() : + m_clipboard(), + m_clipboardData(), + m_clipboardOwner(), + m_clipboardSeqNum(0) +{ + // do nothing +} + + +// +// CServer::CLockCursorToScreenInfo +// + +CServer::CLockCursorToScreenInfo* +CServer::CLockCursorToScreenInfo::alloc(State state) +{ + CLockCursorToScreenInfo* info = + (CLockCursorToScreenInfo*)malloc(sizeof(CLockCursorToScreenInfo)); + info->m_state = state; + return info; +} + + +// +// CServer::CSwitchToScreenInfo +// + +CServer::CSwitchToScreenInfo* +CServer::CSwitchToScreenInfo::alloc(const CString& screen) +{ + CSwitchToScreenInfo* info = + (CSwitchToScreenInfo*)malloc(sizeof(CSwitchToScreenInfo) + + screen.size()); + strcpy(info->m_screen, screen.c_str()); + return info; +} + + +// +// CServer::CSwitchInDirectionInfo +// + +CServer::CSwitchInDirectionInfo* +CServer::CSwitchInDirectionInfo::alloc(EDirection direction) +{ + CSwitchInDirectionInfo* info = + (CSwitchInDirectionInfo*)malloc(sizeof(CSwitchInDirectionInfo)); + info->m_direction = direction; + return info; +} + +// +// CServer::CKeyboardBroadcastInfo +// + +CServer::CKeyboardBroadcastInfo* +CServer::CKeyboardBroadcastInfo::alloc(State state) +{ + CKeyboardBroadcastInfo* info = + (CKeyboardBroadcastInfo*)malloc(sizeof(CKeyboardBroadcastInfo)); + info->m_state = state; + info->m_screens[0] = '\0'; + return info; +} + +CServer::CKeyboardBroadcastInfo* +CServer::CKeyboardBroadcastInfo::alloc(State state, const CString& screens) +{ + CKeyboardBroadcastInfo* info = + (CKeyboardBroadcastInfo*)malloc(sizeof(CKeyboardBroadcastInfo) + + screens.size()); + info->m_state = state; + strcpy(info->m_screens, screens.c_str()); + return info; +} diff --git a/src/lib/server/CServer.h b/src/lib/server/CServer.h new file mode 100644 index 00000000..8f7bb67b --- /dev/null +++ b/src/lib/server/CServer.h @@ -0,0 +1,498 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CSERVER_H +#define CSERVER_H + +#include "CConfig.h" +#include "CClipboard.h" +#include "ClipboardTypes.h" +#include "KeyTypes.h" +#include "MouseTypes.h" +#include "CEvent.h" +#include "CStopwatch.h" +#include "stdmap.h" +#include "stdset.h" +#include "stdvector.h" +#include "INode.h" + +class CBaseClientProxy; +class CEventQueueTimer; +class CPrimaryClient; +class CInputFilter; +class CScreen; + +//! Synergy server +/*! +This class implements the top-level server algorithms for synergy. +*/ +class CServer : public INode { +public: + //! Lock cursor to screen data + class CLockCursorToScreenInfo { + public: + enum State { kOff, kOn, kToggle }; + + static CLockCursorToScreenInfo* alloc(State state = kToggle); + + public: + State m_state; + }; + + //! Switch to screen data + class CSwitchToScreenInfo { + public: + static CSwitchToScreenInfo* alloc(const CString& screen); + + public: + // this is a C-string; this type is a variable size structure + char m_screen[1]; + }; + + //! Switch in direction data + class CSwitchInDirectionInfo { + public: + static CSwitchInDirectionInfo* alloc(EDirection direction); + + public: + EDirection m_direction; + }; + + //! Screen connected data + class CScreenConnectedInfo { + public: + CScreenConnectedInfo(CString screen) : m_screen(screen) { } + + public: + CString m_screen; // was char[1] + }; + + //! Keyboard broadcast data + class CKeyboardBroadcastInfo { + public: + enum State { kOff, kOn, kToggle }; + + static CKeyboardBroadcastInfo* alloc(State state = kToggle); + static CKeyboardBroadcastInfo* alloc(State state, + const CString& screens); + + public: + State m_state; + char m_screens[1]; + }; + + /*! + Start the server with the configuration \p config and the primary + client (local screen) \p primaryClient. The client retains + ownership of \p primaryClient. + */ + CServer(const CConfig& config, CPrimaryClient* primaryClient, CScreen* screen); + ~CServer(); + + //! @name manipulators + //@{ + + //! Set configuration + /*! + Change the server's configuration. Returns true iff the new + configuration was accepted (it must include the server's name). + This will disconnect any clients no longer in the configuration. + */ + bool setConfig(const CConfig&); + + //! Add a client + /*! + Adds \p client to the server. The client is adopted and will be + destroyed when the client disconnects or is disconnected. + */ + void adoptClient(CBaseClientProxy* client); + + //! Disconnect clients + /*! + Disconnect clients. This tells them to disconnect but does not wait + for them to actually do so. The server sends the disconnected event + when they're all disconnected (or immediately if none are connected). + The caller can also just destroy this object to force the disconnection. + */ + void disconnect(); + + //! Notify of game device timing response + void gameDeviceTimingResp(UInt16 freq); + + //! Notify of game device feedback + void gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2); + + //@} + //! @name accessors + //@{ + + //! Get number of connected clients + /*! + Returns the number of connected clients, including the server itself. + */ + UInt32 getNumClients() const; + + //! Get the list of connected clients + /*! + Set the \c list to the names of the currently connected clients. + */ + void getClients(std::vector& list) const; + + //! Get error event type + /*! + Returns the error event type. This is sent when the server fails + for some reason. + */ + static CEvent::Type getErrorEvent(); + + //! Get connected event type + /*! + Returns the connected event type. This is sent when a client screen + has connected. The event data is a \c CScreenConnectedInfo* that + indicates the connected screen. + */ + static CEvent::Type getConnectedEvent(); + + //! Get disconnected event type + /*! + Returns the disconnected event type. This is sent when all the + clients have disconnected. + */ + static CEvent::Type getDisconnectedEvent(); + + //! Get switch to screen event type + /*! + Returns the switch to screen event type. The server responds to this + by switching screens. The event data is a \c CSwitchToScreenInfo* + that indicates the target screen. + */ + static CEvent::Type getSwitchToScreenEvent(); + + //! Get switch in direction event type + /*! + Returns the switch in direction event type. The server responds to this + by switching screens. The event data is a \c CSwitchInDirectionInfo* + that indicates the target direction. + */ + static CEvent::Type getSwitchInDirectionEvent(); + + //! Get keyboard broadcast event type + /*! + Returns the keyboard broadcast event type. The server responds + to this by turning on keyboard broadcasting or turning it off. The + event data is a \c CKeyboardBroadcastInfo*. + */ + static CEvent::Type getKeyboardBroadcastEvent(); + + //! Get lock cursor event type + /*! + Returns the lock cursor event type. The server responds to this + by locking the cursor to the active screen or unlocking it. The + event data is a \c CLockCursorToScreenInfo*. + */ + static CEvent::Type getLockCursorToScreenEvent(); + + //! Get screen switched event type + /*! + Returns the screen switched event type. This is raised when the + screen has been switched to a client. + */ + static CEvent::Type getScreenSwitchedEvent(); + + //@} + +private: + // get canonical name of client + CString getName(const CBaseClientProxy*) const; + + // get the sides of the primary screen that have neighbors + UInt32 getActivePrimarySides() const; + + // returns true iff mouse should be locked to the current screen + // according to this object only, ignoring what the primary client + // says. + bool isLockedToScreenServer() const; + + // returns true iff mouse should be locked to the current screen + // according to this object or the primary client. + bool isLockedToScreen() const; + + // returns the jump zone of the client + SInt32 getJumpZoneSize(CBaseClientProxy*) const; + + // change the active screen + void switchScreen(CBaseClientProxy*, + SInt32 x, SInt32 y, bool forScreenSaver); + + // jump to screen + void jumpToScreen(CBaseClientProxy*); + + // convert pixel position to fraction, using x or y depending on the + // direction. + float mapToFraction(CBaseClientProxy*, EDirection, + SInt32 x, SInt32 y) const; + + // convert fraction to pixel position, writing only x or y depending + // on the direction. + void mapToPixel(CBaseClientProxy*, EDirection, float f, + SInt32& x, SInt32& y) const; + + // returns true if the client has a neighbor anywhere along the edge + // indicated by the direction. + bool hasAnyNeighbor(CBaseClientProxy*, EDirection) const; + + // lookup neighboring screen, mapping the coordinate independent of + // the direction to the neighbor's coordinate space. + CBaseClientProxy* getNeighbor(CBaseClientProxy*, EDirection, + SInt32& x, SInt32& y) const; + + // lookup neighboring screen. given a position relative to the + // source screen, find the screen we should move onto and where. + // if the position is sufficiently far from the source then we + // cross multiple screens. if there is no suitable screen then + // return NULL and x,y are not modified. + CBaseClientProxy* mapToNeighbor(CBaseClientProxy*, EDirection, + SInt32& x, SInt32& y) const; + + // adjusts x and y or neither to avoid ending up in a jump zone + // after entering the client in the given direction. + void avoidJumpZone(CBaseClientProxy*, EDirection, + SInt32& x, SInt32& y) const; + + // test if a switch is permitted. this includes testing user + // options like switch delay and tracking any state required to + // implement them. returns true iff a switch is permitted. + bool isSwitchOkay(CBaseClientProxy* dst, EDirection, + SInt32 x, SInt32 y, SInt32 xActive, SInt32 yActive); + + // update switch state due to a mouse move at \p x, \p y that + // doesn't switch screens. + void noSwitch(SInt32 x, SInt32 y); + + // stop switch timers + void stopSwitch(); + + // start two tap switch timer + void startSwitchTwoTap(); + + // arm the two tap switch timer if \p x, \p y is outside the tap zone + void armSwitchTwoTap(SInt32 x, SInt32 y); + + // stop the two tap switch timer + void stopSwitchTwoTap(); + + // returns true iff the two tap switch timer is started + bool isSwitchTwoTapStarted() const; + + // returns true iff should switch because of two tap + bool shouldSwitchTwoTap() const; + + // start delay switch timer + void startSwitchWait(SInt32 x, SInt32 y); + + // stop delay switch timer + void stopSwitchWait(); + + // returns true iff the delay switch timer is started + bool isSwitchWaitStarted() const; + + // returns the corner (EScreenSwitchCornerMasks) where x,y is on the + // given client. corners have the given size. + UInt32 getCorner(CBaseClientProxy*, + SInt32 x, SInt32 y, SInt32 size) const; + + // stop relative mouse moves + void stopRelativeMoves(); + + // send screen options to \c client + void sendOptions(CBaseClientProxy* client) const; + + // process options from configuration + void processOptions(); + + // event handlers + void handleShapeChanged(const CEvent&, void*); + void handleClipboardGrabbed(const CEvent&, void*); + void handleClipboardChanged(const CEvent&, void*); + void handleKeyDownEvent(const CEvent&, void*); + void handleKeyUpEvent(const CEvent&, void*); + void handleKeyRepeatEvent(const CEvent&, void*); + void handleButtonDownEvent(const CEvent&, void*); + void handleButtonUpEvent(const CEvent&, void*); + void handleMotionPrimaryEvent(const CEvent&, void*); + void handleMotionSecondaryEvent(const CEvent&, void*); + void handleWheelEvent(const CEvent&, void*); + void handleGameDeviceButtons(const CEvent&, void*); + void handleGameDeviceSticks(const CEvent&, void*); + void handleGameDeviceTriggers(const CEvent&, void*); + void handleGameDeviceTimingReq(const CEvent&, void*); + void handleScreensaverActivatedEvent(const CEvent&, void*); + void handleScreensaverDeactivatedEvent(const CEvent&, void*); + void handleSwitchWaitTimeout(const CEvent&, void*); + void handleClientDisconnected(const CEvent&, void*); + void handleClientCloseTimeout(const CEvent&, void*); + void handleSwitchToScreenEvent(const CEvent&, void*); + void handleSwitchInDirectionEvent(const CEvent&, void*); + void handleKeyboardBroadcastEvent(const CEvent&,void*); + void handleLockCursorToScreenEvent(const CEvent&, void*); + void handleFakeInputBeginEvent(const CEvent&, void*); + void handleFakeInputEndEvent(const CEvent&, void*); + + // event processing + void onClipboardChanged(CBaseClientProxy* sender, + ClipboardID id, UInt32 seqNum); + void onScreensaver(bool activated); + void onKeyDown(KeyID, KeyModifierMask, KeyButton, + const char* screens); + void onKeyUp(KeyID, KeyModifierMask, KeyButton, + const char* screens); + void onKeyRepeat(KeyID, KeyModifierMask, SInt32, KeyButton); + void onMouseDown(ButtonID); + void onMouseUp(ButtonID); + bool onMouseMovePrimary(SInt32 x, SInt32 y); + void onMouseMoveSecondary(SInt32 dx, SInt32 dy); + void onMouseWheel(SInt32 xDelta, SInt32 yDelta); + void onGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons); + void onGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2); + void onGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2); + void onGameDeviceTimingReq(); + + // add client to list and attach event handlers for client + bool addClient(CBaseClientProxy*); + + // remove client from list and detach event handlers for client + bool removeClient(CBaseClientProxy*); + + // close a client + void closeClient(CBaseClientProxy*, const char* msg); + + // close clients not in \p config + void closeClients(const CConfig& config); + + // close all clients whether they've completed the handshake or not, + // except the primary client + void closeAllClients(); + + // remove clients from internal state + void removeActiveClient(CBaseClientProxy*); + void removeOldClient(CBaseClientProxy*); + + // force the cursor off of \p client + void forceLeaveClient(CBaseClientProxy* client); + +private: + class CClipboardInfo { + public: + CClipboardInfo(); + + public: + CClipboard m_clipboard; + CString m_clipboardData; + CString m_clipboardOwner; + UInt32 m_clipboardSeqNum; + }; + + // the primary screen client + CPrimaryClient* m_primaryClient; + + // all clients (including the primary client) indexed by name + typedef std::map CClientList; + typedef std::set CClientSet; + CClientList m_clients; + CClientSet m_clientSet; + + // all old connections that we're waiting to hangup + typedef std::map COldClients; + COldClients m_oldClients; + + // the client with focus + CBaseClientProxy* m_active; + + // the sequence number of enter messages + UInt32 m_seqNum; + + // current mouse position (in absolute screen coordinates) on + // whichever screen is active + SInt32 m_x, m_y; + + // last mouse deltas. this is needed to smooth out double tap + // on win32 which reports bogus mouse motion at the edge of + // the screen when using low level hooks, synthesizing motion + // in the opposite direction the mouse actually moved. + SInt32 m_xDelta, m_yDelta; + SInt32 m_xDelta2, m_yDelta2; + + // current configuration + CConfig m_config; + + // input filter (from m_config); + CInputFilter* m_inputFilter; + + // clipboard cache + CClipboardInfo m_clipboards[kClipboardEnd]; + + // state saved when screen saver activates + CBaseClientProxy* m_activeSaver; + SInt32 m_xSaver, m_ySaver; + + // common state for screen switch tests. all tests are always + // trying to reach the same screen in the same direction. + EDirection m_switchDir; + CBaseClientProxy* m_switchScreen; + + // state for delayed screen switching + double m_switchWaitDelay; + CEventQueueTimer* m_switchWaitTimer; + SInt32 m_switchWaitX, m_switchWaitY; + + // state for double-tap screen switching + double m_switchTwoTapDelay; + CStopwatch m_switchTwoTapTimer; + bool m_switchTwoTapEngaged; + bool m_switchTwoTapArmed; + SInt32 m_switchTwoTapZone; + + // modifiers needed before switching + bool m_switchNeedsShift; + bool m_switchNeedsControl; + bool m_switchNeedsAlt; + + // relative mouse move option + bool m_relativeMoves; + + // flag whether or not we have broadcasting enabled and the screens to + // which we should send broadcasted keys. + bool m_keyboardBroadcasting; + CString m_keyboardBroadcastingScreens; + + // screen locking (former scroll lock) + bool m_lockedToScreen; + + // server screen + CScreen* m_screen; + + static CEvent::Type s_errorEvent; + static CEvent::Type s_connectedEvent; + static CEvent::Type s_disconnectedEvent; + static CEvent::Type s_switchToScreen; + static CEvent::Type s_switchInDirection; + static CEvent::Type s_keyboardBroadcast; + static CEvent::Type s_lockCursorToScreen; + static CEvent::Type s_screenSwitched; +}; + +#endif diff --git a/src/lib/synergy/CApp.cpp b/src/lib/synergy/CApp.cpp new file mode 100644 index 00000000..f7f26b62 --- /dev/null +++ b/src/lib/synergy/CApp.cpp @@ -0,0 +1,410 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CApp.h" +#include "CLog.h" +#include "Version.h" +#include "ProtocolTypes.h" +#include "CArch.h" +#include "XBase.h" +#include "XArch.h" +#include "LogOutputters.h" +#include "XSynergy.h" +#include "CArgsBase.h" + +#if SYSAPI_WIN32 +#include "CArchMiscWindows.h" +#include "IEventQueue.h" +#include "TMethodJob.h" +#endif + +#include +#include + +#if WINAPI_CARBON +#include +#endif + +CApp* CApp::s_instance = nullptr; + +CApp::CApp(CreateTaskBarReceiverFunc createTaskBarReceiver, CArgsBase* args) : +m_createTaskBarReceiver(createTaskBarReceiver), +m_args(args), +m_bye(&exit), +m_taskBarReceiver(NULL), +m_suspended(false) +{ + assert(s_instance == nullptr); + s_instance = this; +} + +CApp::~CApp() +{ + delete m_args; +} + +bool +CApp::isArg( + int argi, int argc, const char* const* argv, + const char* name1, const char* name2, + int minRequiredParameters) +{ + if ((name1 != NULL && strcmp(argv[argi], name1) == 0) || + (name2 != NULL && strcmp(argv[argi], name2) == 0)) { + // match. check args left. + if (argi + minRequiredParameters >= argc) { + LOG((CLOG_PRINT "%s: missing arguments for `%s'" BYE, + argsBase().m_pname, argv[argi], argsBase().m_pname)); + m_bye(kExitArgs); + } + return true; + } + + // no match + return false; +} + +bool +CApp::parseArg(const int& argc, const char* const* argv, int& i) +{ + if (appUtil().parseArg(argc, argv, i)) { + // handled by platform util + return true; + } + + else if (isArg(i, argc, argv, "-d", "--debug", 1)) { + // change logging level + argsBase().m_logFilter = argv[++i]; + } + + else if (isArg(i, argc, argv, "-l", "--log", 1)) { + argsBase().m_logFile = argv[++i]; + } + + else if (isArg(i, argc, argv, "-f", "--no-daemon")) { + // not a daemon + argsBase().m_daemon = false; + } + + else if (isArg(i, argc, argv, NULL, "--daemon")) { + // daemonize + argsBase().m_daemon = true; + } + + else if (isArg(i, argc, argv, "-n", "--name", 1)) { + // save screen name + argsBase().m_name = argv[++i]; + } + + else if (isArg(i, argc, argv, "-1", "--no-restart")) { + // don't try to restart + argsBase().m_restartable = false; + } + + else if (isArg(i, argc, argv, NULL, "--restart")) { + // try to restart + argsBase().m_restartable = true; + } + + else if (isArg(i, argc, argv, "-z", NULL)) { + argsBase().m_backend = true; + } + + else if (isArg(i, argc, argv, NULL, "--no-hooks")) { + argsBase().m_noHooks = true; + } + + else if (isArg(i, argc, argv, "-h", "--help")) { + help(); + m_bye(kExitSuccess); + } + + else if (isArg(i, argc, argv, NULL, "--version")) { + version(); + m_bye(kExitSuccess); + } + + else if (isArg(i, argc, argv, NULL, "--no-tray")) { + argsBase().m_disableTray = true; + } + +#if VNC_SUPPORT + else if (isArg(i, argc, argv, NULL, "--vnc")) { + argsBase().m_enableVnc = true; + } +#endif + + else { + // option not supported here + return false; + } + + return true; +} + +void +CApp::parseArgs(int argc, const char* const* argv, int& i) +{ + // about these use of assert() here: + // previously an /analyze warning was displayed if we only used assert and + // did not return on failure. however, this warning does not appear to show + // any more (could be because new compiler args have been added). + // the asserts are programmer benefit only; the os should never pass 0 args, + // because the first is always the binary name. the only way assert would + // evaluate to true, is if this parse function were implemented incorrectly, + // which is unlikely because it's old code and has a specific use. + // we should avoid using anything other than assert here, because it will + // look like important code, which it's not really. + assert(argsBase().m_pname != NULL); + assert(argv != NULL); + assert(argc >= 1); + + // set defaults + argsBase().m_name = ARCH->getHostName(); + + // parse options + for (i = 1; i < argc; ++i) { + + if (parseArg(argc, argv, i)) { + continue; + } + + else if (isArg(i, argc, argv, "--", NULL)) { + // remaining arguments are not options + ++i; + break; + } + + else if (argv[i][0] == '-') { + std::cerr << "Unrecognized option: " << argv[i] << std::endl; + m_bye(kExitArgs); + } + + else { + // this and remaining arguments are not options + break; + } + } + +#if SYSAPI_WIN32 + // suggest that user installs as a windows service. when launched as + // service, process should automatically detect that it should run in + // daemon mode. + if (argsBase().m_daemon) { + LOG((CLOG_ERR + "the --daemon argument is not supported on windows. " + "instead, install %s as a service (--service install)", + argsBase().m_pname)); + m_bye(kExitArgs); + } +#endif +} + +void +CApp::version() +{ + char buffer[500]; + sprintf( + buffer, + "%s %s, protocol version %d.%d\n%s", + argsBase().m_pname, + kVersion, + kProtocolMajorVersion, + kProtocolMinorVersion, + kCopyright + ); + + std::cout << buffer << std::endl; +} + +int +CApp::run(int argc, char** argv) +{ +#if SYSAPI_WIN32 + // record window instance for tray icon, etc + CArchMiscWindows::setInstanceWin32(GetModuleHandle(NULL)); +#endif + +#if MAC_OS_X_VERSION_10_7 + // dock hide only supported on lion :( + ProcessSerialNumber psn = { 0, kCurrentProcess }; + GetCurrentProcess(&psn); + TransformProcessType(&psn, kProcessTransformToBackgroundApplication); +#endif + + // install application in to arch + appUtil().adoptApp(this); + + // HACK: fail by default (saves us setting result in each catch) + int result = kExitFailed; + + try { + result = appUtil().run(argc, argv); + } + catch (XExitApp& e) { + // instead of showing a nasty error, just exit with the error code. + // not sure if i like this behaviour, but it's probably better than + // using the exit(int) function! + result = e.getCode(); + } + catch (XBase& e) { + LOG((CLOG_CRIT "Exception: %s\n", e.what())); + } + catch (XArch& e) { + LOG((CLOG_CRIT "Init failed: %s" BYE, e.what().c_str(), argsBase().m_pname)); + } + catch (std::exception& e) { + LOG((CLOG_CRIT "Exception: %s\n", e.what())); + } + catch (...) { + LOG((CLOG_CRIT "An unexpected exception occurred.\n")); + } + + appUtil().beforeAppExit(); + + return result; +} + +int +CApp::daemonMainLoop(int, const char**) +{ +#if SYSAPI_WIN32 + CSystemLogger sysLogger(daemonName(), false); +#else + CSystemLogger sysLogger(daemonName(), true); +#endif + return mainLoop(); +} + +void +CApp::setupFileLogging() +{ + if (argsBase().m_logFile != NULL) { + m_fileLog = new CFileLogOutputter(argsBase().m_logFile); + CLOG->insert(m_fileLog); + LOG((CLOG_DEBUG1 "logging to file (%s) enabled", argsBase().m_logFile)); + } +} + +void +CApp::loggingFilterWarning() +{ + if (CLOG->getFilter() > CLOG->getConsoleMaxLevel()) { + if (argsBase().m_logFile == NULL) { + LOG((CLOG_WARN "log messages above %s are NOT sent to console (use file logging)", + CLOG->getFilterName(CLOG->getConsoleMaxLevel()))); + } + } +} + +void +CApp::initApp(int argc, const char** argv) +{ + // parse command line + parseArgs(argc, argv); + +#if SYSAPI_WIN32 + CThread pipeThread(new TMethodJob( + this, &CApp::pipeThread, nullptr)); +#endif + + // setup file logging after parsing args + setupFileLogging(); + + // load configuration + loadConfig(); + + if (!argsBase().m_disableTray) { + + // create a log buffer so we can show the latest message + // as a tray icon tooltip + CBufferedLogOutputter* logBuffer = new CBufferedLogOutputter(1000); + CLOG->insert(logBuffer, true); + + // make the task bar receiver. the user can control this app + // through the task bar. + m_taskBarReceiver = m_createTaskBarReceiver(logBuffer); + } +} + +#ifdef SYSAPI_WIN32 + +void +CApp::pipeThread(void*) +{ + // TODO: move this to an IPC server class. + while (true) { + + // grant access to everyone. + SECURITY_DESCRIPTOR sd; + InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION); + SetSecurityDescriptorDacl(&sd, TRUE, static_cast(0), FALSE); + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.lpSecurityDescriptor = &sd; + + HANDLE pipe = CreateNamedPipe( + _T("\\\\.\\pipe\\SynergyNode"), + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, + PIPE_UNLIMITED_INSTANCES, + 1024, 1024, 0, &sa); + + if (pipe == INVALID_HANDLE_VALUE) + XArch("could not create named pipe."); + + LOG((CLOG_DEBUG "opened node pipe: %d", pipe)); + BOOL connectResult = ConnectNamedPipe(pipe, NULL); + + char buffer[1024]; + DWORD bytesRead; + + while (true) { + if (!ReadFile(pipe, buffer, sizeof(buffer), &bytesRead, NULL)) { + break; + } + + buffer[bytesRead] = '\0'; + LOG((CLOG_DEBUG "ipc node server read: %s", buffer)); + + handlePipeMessage(buffer); + } + + DisconnectNamedPipe(pipe); + CloseHandle(pipe); + } +} + +void +CApp::handlePipeMessage(char* buffer) +{ + switch (buffer[0]) { + case kIpcShutdown: + { + LOG((CLOG_INFO "queueing quit event")); + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); + } + break; + + default: + LOG((CLOG_WARN "unrecognized ipc message: %d", buffer[0])); + break; + } +} + +#endif diff --git a/src/lib/synergy/CApp.h b/src/lib/synergy/CApp.h new file mode 100644 index 00000000..36ec2ec7 --- /dev/null +++ b/src/lib/synergy/CApp.h @@ -0,0 +1,182 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "common.h" +#include "CString.h" +#include "IApp.h" + +#if SYSAPI_WIN32 +#include "CAppUtilWindows.h" +#elif SYSAPI_UNIX +#include "CAppUtilUnix.h" +#endif + +class IArchTaskBarReceiver; +class CBufferedLogOutputter; +class ILogOutputter; +class CFileLogOutputter; +class CScreen; + +typedef IArchTaskBarReceiver* (*CreateTaskBarReceiverFunc)(const CBufferedLogOutputter*); + +class CApp : public IApp { +public: + CApp(CreateTaskBarReceiverFunc createTaskBarReceiver, CArgsBase* args); + virtual ~CApp(); + + // Returns args that are common between server and client. + CArgsBase& argsBase() const { return *m_args; } + + // Prints the current compiled version. + virtual void version(); + + // Prints help specific to client or server. + virtual void help() = 0; + + // Parse command line arguments. + virtual void parseArgs(int argc, const char* const* argv) = 0; + + int run(int argc, char** argv); + + int daemonMainLoop(int, const char**); + + virtual void loadConfig() = 0; + virtual bool loadConfig(const CString& pathname) = 0; + + // A description of the daemon (used only on Windows). + virtual const char* daemonInfo() const = 0; + + // Function pointer for function to exit immediately. + // TODO: this is old C code - use inheritance to normalize + void (*m_bye)(int); + + // Returns true if argv[argi] is equal to name1 or name2. + bool isArg(int argi, int argc, const char* const* argv, + const char* name1, const char* name2, + int minRequiredParameters = 0); + + static CApp& instance() { assert(s_instance != nullptr); return *s_instance; } + + // If --log was specified in args, then add a file logger. + void setupFileLogging(); + + // If messages will be hidden (to improve performance), warn user. + void loggingFilterWarning(); + + // Parses args, sets up file logging, and loads the config. + void initApp(int argc, const char** argv); + + // HACK: accept non-const, but make it const anyway + void initApp(int argc, char** argv) { initApp(argc, (const char**)argv); } + + ARCH_APP_UTIL& appUtil() { return m_appUtil; } + + virtual IArchTaskBarReceiver* taskBarReceiver() const { return m_taskBarReceiver; } + + virtual void setByeFunc(void(*bye)(int)) { m_bye = bye; } + virtual void bye(int error) { m_bye(error); } + +protected: + virtual void parseArgs(int argc, const char* const* argv, int &i); + virtual bool parseArg(const int& argc, const char* const* argv, int& i); + + IArchTaskBarReceiver* m_taskBarReceiver; + bool m_suspended; + +private: + CArgsBase* m_args; + static CApp* s_instance; + CFileLogOutputter* m_fileLog; + CreateTaskBarReceiverFunc m_createTaskBarReceiver; + ARCH_APP_UTIL m_appUtil; + +#if SYSAPI_WIN32 + void pipeThread(void*); + void handlePipeMessage(char* buffer); +#endif +}; + +#define BYE "\nTry `%s --help' for more information." + +#if WINAPI_MSWINDOWS +#define DAEMON_RUNNING(running_) CArchMiscWindows::daemonRunning(running_) +#else +#define DAEMON_RUNNING(running_) +#endif + +#define HELP_COMMON_INFO_1 \ + " -d, --debug filter out log messages with priority below level.\n" \ + " level may be: FATAL, ERROR, WARNING, NOTE, INFO,\n" \ + " DEBUG, DEBUGn (1-5).\n" \ + " -n, --name use screen-name instead the hostname to identify\n" \ + " this screen in the configuration.\n" \ + " -1, --no-restart do not try to restart on failure.\n" \ + "* --restart restart the server automatically if it fails.\n" \ + " -l --log write log messages to file.\n" \ + " --no-tray disable the system tray icon.\n" + +#define HELP_COMMON_INFO_2 \ + " -h, --help display this help and exit.\n" \ + " --version display version information and exit.\n" + +#define HELP_COMMON_ARGS \ + " [--name ]" \ + " [--restart|--no-restart]" \ + " [--debug ]" + +// system args (windows/unix) +#if SYSAPI_UNIX + +// unix daemon mode args +# define HELP_SYS_ARGS \ + " [--daemon|--no-daemon]" +# define HELP_SYS_INFO \ + " -f, --no-daemon run in the foreground.\n" \ + "* --daemon run as a daemon.\n" + +#elif SYSAPI_WIN32 + +#if GAME_DEVICE_SUPPORT +# define HELP_GAME_DEVICE \ + " --game-mode enable game device support. valid modes:\n" \ + " xinput, joyinfoex\n" \ + " --game-poll game polling mode. valid modes:\n" \ + " dynamic, static\n" \ + " --game-poll-freq frequency for static polling.\n" +#else +# define HELP_GAME_DEVICE "" +#endif + +// windows args +# define HELP_SYS_ARGS \ + " [--service ] [--relaunch] [--exit-pause]" +# define HELP_SYS_INFO \ + " --service manage the windows service, valid options are:\n" \ + " install/uninstall/start/stop\n" \ + " --relaunch persistently relaunches process in current user \n" \ + " session (useful for vista and upward).\n" \ + " --exit-pause wait for key press on exit, can be useful for\n" \ + " reading error messages that occur on exit.\n" \ + HELP_GAME_DEVICE + +#endif + +enum { + kIpcShutdown = 1 +}; diff --git a/src/lib/synergy/CAppUtil.cpp b/src/lib/synergy/CAppUtil.cpp new file mode 100644 index 00000000..1365e857 --- /dev/null +++ b/src/lib/synergy/CAppUtil.cpp @@ -0,0 +1,58 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CAppUtil.h" + +CAppUtil* CAppUtil::s_instance = nullptr; + +CAppUtil::CAppUtil() : +m_app(nullptr) +{ + s_instance = this; +} + +CAppUtil::~CAppUtil() +{ +} + +bool +CAppUtil::parseArg(const int& argc, const char* const* argv, int& i) +{ + // no common platform args (yet) + return false; +} + +void +CAppUtil::adoptApp(IApp* app) +{ + app->setByeFunc(&exitAppStatic); + m_app = app; +} + +IApp& +CAppUtil::app() const +{ + assert(m_app != nullptr); + return *m_app; +} + +CAppUtil& +CAppUtil::instance() +{ + assert(s_instance != nullptr); + return *s_instance; +} diff --git a/src/lib/synergy/CAppUtil.h b/src/lib/synergy/CAppUtil.h new file mode 100644 index 00000000..f6e1ddb1 --- /dev/null +++ b/src/lib/synergy/CAppUtil.h @@ -0,0 +1,40 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IAppUtil.h" +#include "XSynergy.h" + +class CAppUtil : public IAppUtil { +public: + CAppUtil(); + virtual ~CAppUtil(); + + virtual bool parseArg(const int& argc, const char* const* argv, int& i); + virtual void adoptApp(IApp* app); + IApp& app() const; + virtual void exitApp(int code) { throw XExitApp(code); } + + static CAppUtil& instance(); + static void exitAppStatic(int code) { instance().exitApp(code); } + virtual void beforeAppExit() {} + +private: + IApp* m_app; + static CAppUtil* s_instance; +}; diff --git a/src/lib/synergy/CAppUtilUnix.cpp b/src/lib/synergy/CAppUtilUnix.cpp new file mode 100644 index 00000000..b0884c5f --- /dev/null +++ b/src/lib/synergy/CAppUtilUnix.cpp @@ -0,0 +1,70 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CAppUtilUnix.h" +#include "CArgsBase.h" + +CAppUtilUnix::CAppUtilUnix() +{ +} + +CAppUtilUnix::~CAppUtilUnix() +{ +} + +bool +CAppUtilUnix::parseArg(const int& argc, const char* const* argv, int& i) +{ +#if WINAPI_XWINDOWS + if (app().isArg(i, argc, argv, "-display", "--display", 1)) { + // use alternative display + app().argsBase().m_display = argv[++i]; + } + + else if (app().isArg(i, argc, argv, NULL, "--no-xinitthreads")) { + app().argsBase().m_disableXInitThreads = true; + } + + else { + // option not supported here + return false; + } + + return true; +#else + // no options for carbon + return false; +#endif +} + +int +standardStartupStatic(int argc, char** argv) +{ + return CAppUtil::instance().app().standardStartup(argc, argv); +} + +int +CAppUtilUnix::run(int argc, char** argv) +{ + return app().runInner(argc, argv, NULL, &standardStartupStatic); +} + +void +CAppUtilUnix::startNode() +{ + app().startNode(); +} diff --git a/src/lib/synergy/CAppUtilUnix.h b/src/lib/synergy/CAppUtilUnix.h new file mode 100644 index 00000000..297c1b44 --- /dev/null +++ b/src/lib/synergy/CAppUtilUnix.h @@ -0,0 +1,32 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CAppUtil.h" + +#define ARCH_APP_UTIL CAppUtilUnix + +class CAppUtilUnix : public CAppUtil { +public: + CAppUtilUnix(); + virtual ~CAppUtilUnix(); + + bool parseArg(const int& argc, const char* const* argv, int& i); + int run(int argc, char** argv); + void startNode(); +}; diff --git a/src/lib/synergy/CAppUtilWindows.cpp b/src/lib/synergy/CAppUtilWindows.cpp new file mode 100644 index 00000000..32048b86 --- /dev/null +++ b/src/lib/synergy/CAppUtilWindows.cpp @@ -0,0 +1,230 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CAppUtilWindows.h" +#include "Version.h" +#include "CLog.h" +#include "XArchWindows.h" +#include "CArchMiscWindows.h" +#include "CApp.h" +#include "LogOutputters.h" +#include "CMSWindowsScreen.h" +#include "XSynergy.h" +#include "IArchTaskBarReceiver.h" +#include "CMSWindowsRelauncher.h" +#include "CScreen.h" +#include "CArgsBase.h" +#include "IEventQueue.h" +#include "CEvent.h" + +#include +#include +#include + +CAppUtilWindows::CAppUtilWindows() : +m_exitMode(kExitModeNormal) +{ + if (SetConsoleCtrlHandler((PHANDLER_ROUTINE)consoleHandler, TRUE) == FALSE) + { + throw XArch(new XArchEvalWindows()); + } +} + +CAppUtilWindows::~CAppUtilWindows() +{ +} + +BOOL WINAPI CAppUtilWindows::consoleHandler(DWORD) +{ + LOG((CLOG_INFO "got shutdown signal")); + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); + return TRUE; +} + +bool +CAppUtilWindows::parseArg(const int& argc, const char* const* argv, int& i) +{ + if (app().isArg(i, argc, argv, NULL, "--service")) { + + LOG((CLOG_WARN "obsolete argument --service, use synergyd instead.")); + app().bye(kExitFailed); + } + else if (app().isArg(i, argc, argv, NULL, "--exit-pause")) { + + app().argsBase().m_pauseOnExit = true; + } + else if (app().isArg(i, argc, argv, NULL, "--game-mode")) { + CString mode(argv[++i]); + if (mode == "xinput") { + app().argsBase().m_gameDevice.m_mode = CGameDeviceInfo::kGameModeXInput; + } + else if (mode == "joyinfoex") { + app().argsBase().m_gameDevice.m_mode = CGameDeviceInfo::kGameModeJoyInfoEx; + } + } + else if (app().isArg(i, argc, argv, NULL, "--game-poll")) { + CString mode(argv[++i]); + if (mode == "dynamic") { + app().argsBase().m_gameDevice.m_poll = CGameDeviceInfo::kGamePollDynamic; + } + else if (mode == "static") { + app().argsBase().m_gameDevice.m_poll = CGameDeviceInfo::kGamePollStatic; + } + } + else if (app().isArg(i, argc, argv, NULL, "--game-poll-freq")) { + app().argsBase().m_gameDevice.m_pollFreq = atoi(argv[++i]); + } + else { + // option not supported here + return false; + } + + return true; +} + +static +int +mainLoopStatic() +{ + return CAppUtil::instance().app().mainLoop(); +} + +int +CAppUtilWindows::daemonNTMainLoop(int argc, const char** argv) +{ + app().initApp(argc, argv); + debugServiceWait(); + + // NB: what the hell does this do?! + app().argsBase().m_backend = false; + + return CArchMiscWindows::runDaemon(mainLoopStatic); +} + +void +CAppUtilWindows::exitApp(int code) +{ + switch (m_exitMode) { + + case kExitModeDaemon: + CArchMiscWindows::daemonFailed(code); + break; + + default: + throw XExitApp(code); + } +} + +int daemonNTMainLoopStatic(int argc, const char** argv) +{ + return CAppUtilWindows::instance().daemonNTMainLoop(argc, argv); +} + +int +CAppUtilWindows::daemonNTStartup(int, char**) +{ + CSystemLogger sysLogger(app().daemonName(), false); + m_exitMode = kExitModeDaemon; + return ARCH->daemonize(app().daemonName(), daemonNTMainLoopStatic); +} + +static +int +daemonNTStartupStatic(int argc, char** argv) +{ + return CAppUtilWindows::instance().daemonNTStartup(argc, argv); +} + +static +int +foregroundStartupStatic(int argc, char** argv) +{ + return CAppUtil::instance().app().foregroundStartup(argc, argv); +} + +void +CAppUtilWindows::beforeAppExit() +{ + // this can be handy for debugging, since the application is launched in + // a new console window, and will normally close on exit (making it so + // that we can't see error messages). + if (app().argsBase().m_pauseOnExit) { + std::cout << std::endl << "press any key to exit..." << std::endl; + int c = _getch(); + } +} + +int +CAppUtilWindows::run(int argc, char** argv) +{ + // record window instance for tray icon, etc + CArchMiscWindows::setInstanceWin32(GetModuleHandle(NULL)); + + CMSWindowsScreen::init(CArchMiscWindows::instanceWin32()); + CThread::getCurrentThread().setPriority(-14); + + StartupFunc startup; + if (CArchMiscWindows::wasLaunchedAsService()) { + startup = &daemonNTStartupStatic; + } else { + startup = &foregroundStartupStatic; + app().argsBase().m_daemon = false; + } + + return app().runInner(argc, argv, NULL, startup); +} + +CAppUtilWindows& +CAppUtilWindows::instance() +{ + return (CAppUtilWindows&)CAppUtil::instance(); +} + +void +CAppUtilWindows::debugServiceWait() +{ + if (app().argsBase().m_debugServiceWait) + { + while(true) + { + // this code is only executed when the process is launched via the + // windows service controller (and --debug-service-wait arg is + // used). to debug, set a breakpoint on this line so that + // execution is delayed until the debugger is attached. + ARCH->sleep(1); + LOG((CLOG_INFO "waiting for debugger to attach")); + } + } +} + +void +CAppUtilWindows::startNode() +{ + if (app().argsBase().m_relaunchMode) { + + LOG((CLOG_DEBUG1 "entering relaunch mode")); + CMSWindowsRelauncher relauncher(true); + relauncher.startAsync(); + + // HACK: create a dummy screen, which can handle system events + // (such as a stop request from the service controller). + CScreen* dummyScreen = app().createScreen(); + } + else { + app().startNode(); + } +} diff --git a/src/lib/synergy/CAppUtilWindows.h b/src/lib/synergy/CAppUtilWindows.h new file mode 100644 index 00000000..cf5cecaf --- /dev/null +++ b/src/lib/synergy/CAppUtilWindows.h @@ -0,0 +1,59 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CAppUtil.h" +#include "CString.h" + +#define WIN32_LEAN_AND_MEAN +#include "Windows.h" + +#define ARCH_APP_UTIL CAppUtilWindows + +enum AppExitMode { + kExitModeNormal, + kExitModeDaemon +}; + +class CAppUtilWindows : public CAppUtil { +public: + CAppUtilWindows(); + virtual ~CAppUtilWindows(); + + bool parseArg(const int& argc, const char* const* argv, int& i); + + int daemonNTStartup(int, char**); + + int daemonNTMainLoop(int argc, const char** argv); + + void debugServiceWait(); + + int run(int argc, char** argv); + + void exitApp(int code); + + void beforeAppExit(); + + static CAppUtilWindows& instance(); + + void startNode(); + +private: + AppExitMode m_exitMode; + static BOOL WINAPI consoleHandler(DWORD CEvent); +}; diff --git a/src/lib/synergy/CArgsBase.cpp b/src/lib/synergy/CArgsBase.cpp new file mode 100644 index 00000000..0871047a --- /dev/null +++ b/src/lib/synergy/CArgsBase.cpp @@ -0,0 +1,46 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CArgsBase.h" + +CArgsBase::CArgsBase() : +#if SYSAPI_WIN32 +m_daemon(false), // daemon mode not supported on windows (use --service) +m_debugServiceWait(false), +m_relaunchMode(false), +m_pauseOnExit(false), +#else +m_daemon(true), // backward compatibility for unix (daemon by default) +#endif +#if WINAPI_XWINDOWS +m_disableXInitThreads(false), +#endif +m_backend(false), +m_restartable(true), +m_noHooks(false), +m_disableTray(false), +m_pname(NULL), +m_logFilter(NULL), +m_logFile(NULL), +m_display(NULL), +m_enableVnc(false) +{ +} + +CArgsBase::~CArgsBase() +{ +} diff --git a/src/lib/synergy/CArgsBase.h b/src/lib/synergy/CArgsBase.h new file mode 100644 index 00000000..f3f5b587 --- /dev/null +++ b/src/lib/synergy/CArgsBase.h @@ -0,0 +1,47 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CString.h" +#include "CGameDevice.h" + +class CArgsBase { +public: + CArgsBase(); + virtual ~CArgsBase(); + bool m_daemon; + bool m_backend; + bool m_restartable; + bool m_noHooks; + const char* m_pname; + const char* m_logFilter; + const char* m_logFile; + const char* m_display; + CString m_name; + bool m_disableTray; + bool m_enableVnc; +#if SYSAPI_WIN32 + bool m_relaunchMode; + bool m_debugServiceWait; + bool m_pauseOnExit; + CGameDeviceInfo m_gameDevice; +#endif +#if WINAPI_XWINDOWS + bool m_disableXInitThreads; +#endif +}; diff --git a/src/lib/synergy/CClientApp.cpp b/src/lib/synergy/CClientApp.cpp new file mode 100644 index 00000000..51ded42a --- /dev/null +++ b/src/lib/synergy/CClientApp.cpp @@ -0,0 +1,637 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClientApp.h" +#include "CLog.h" +#include "CArch.h" +#include "XSocket.h" +#include "Version.h" +#include "ProtocolTypes.h" +#include "CString.h" +#include "CScreen.h" +#include "CEvent.h" +#include "CClient.h" +#include "CNetworkAddress.h" +#include "IArchTaskBarReceiver.h" +#include "IEventQueue.h" +#include "TMethodEventJob.h" +#include "CTCPSocketFactory.h" +#include "XScreen.h" +#include "LogOutputters.h" +#include "CSocketMultiplexer.h" +#include "CEventQueue.h" +#include "CThread.h" +#include "TMethodJob.h" + +#if SYSAPI_WIN32 +#include "CArchMiscWindows.h" +#if VNC_SUPPORT +#include "vnc/win/winvnc/winvnc.h" +#endif +#endif + +#if SYSAPI_WIN32 && GAME_DEVICE_SUPPORT +#include +#include "XInputHook.h" +#endif + +#if WINAPI_MSWINDOWS +#include "CMSWindowsScreen.h" +#elif WINAPI_XWINDOWS +#include "CXWindowsScreen.h" +#elif WINAPI_CARBON +#include "COSXScreen.h" +#endif + +#include +#include + +#define RETRY_TIME 1.0 + +CClientApp::CClientApp(CreateTaskBarReceiverFunc createTaskBarReceiver) : +CApp(createTaskBarReceiver, new CArgs()), +s_client(NULL), +s_clientScreen(NULL), +m_vncThread(NULL) +{ +} + +CClientApp::~CClientApp() +{ + if (m_vncThread) + delete m_vncThread; +} + +CClientApp::CArgs::CArgs() : +m_yscroll(0), +m_serverAddress(NULL) +{ +} + +CClientApp::CArgs::~CArgs() +{ +} + +bool +CClientApp::parseArg(const int& argc, const char* const* argv, int& i) +{ + if (CApp::parseArg(argc, argv, i)) { + // found common arg + return true; + } + + else if (isArg(i, argc, argv, NULL, "--camp")) { + // ignore -- included for backwards compatibility + } + + else if (isArg(i, argc, argv, NULL, "--no-camp")) { + // ignore -- included for backwards compatibility + } + + else if (isArg(i, argc, argv, NULL, "--yscroll", 1)) { + // define scroll + args().m_yscroll = atoi(argv[++i]); + } + + else { + // option not supported here + return false; + } + + // argument was valid + return true; +} + +void +CClientApp::parseArgs(int argc, const char* const* argv) +{ + // asserts values, sets defaults, and parses args + int i; + CApp::parseArgs(argc, argv, i); + + // exactly one non-option argument (server-address) + if (i == argc) { + LOG((CLOG_PRINT "%s: a server address or name is required" BYE, + args().m_pname, args().m_pname)); + m_bye(kExitArgs); + } + if (i + 1 != argc) { + LOG((CLOG_PRINT "%s: unrecognized option `%s'" BYE, + args().m_pname, argv[i], args().m_pname)); + m_bye(kExitArgs); + } + + // save server address + try { + *args().m_serverAddress = CNetworkAddress(argv[i], kDefaultPort); + args().m_serverAddress->resolve(); + } + catch (XSocketAddress& e) { + // allow an address that we can't look up if we're restartable. + // we'll try to resolve the address each time we connect to the + // server. a bad port will never get better. patch by Brent + // Priddy. + if (!args().m_restartable || e.getError() == XSocketAddress::kBadPort) { + LOG((CLOG_PRINT "%s: %s" BYE, + args().m_pname, e.what(), args().m_pname)); + m_bye(kExitFailed); + } + } + + // set log filter + if (!CLOG->setFilter(args().m_logFilter)) { + LOG((CLOG_PRINT "%s: unrecognized log level `%s'" BYE, + args().m_pname, args().m_logFilter, args().m_pname)); + m_bye(kExitArgs); + } + + // identify system + LOG((CLOG_INFO "%s Client on %s %s", kAppVersion, ARCH->getOSName().c_str(), ARCH->getPlatformName().c_str())); + + loggingFilterWarning(); +} + +void +CClientApp::help() +{ +#if WINAPI_XWINDOWS +# define WINAPI_ARG \ + " [--display ] [--no-xinitthreads]" +# define WINAPI_INFO \ + " --display connect to the X server at \n" \ + " --no-xinitthreads do not call XInitThreads()\n" +#else +# define WINAPI_ARG +# define WINAPI_INFO +#endif + + char buffer[2000]; + sprintf( + buffer, + "Usage: %s" + " [--yscroll ]" + WINAPI_ARG + HELP_SYS_ARGS + HELP_COMMON_ARGS + " " + "\n\n" + "Connect to a synergy mouse/keyboard sharing server.\n" + "\n" + HELP_COMMON_INFO_1 + WINAPI_INFO + HELP_SYS_INFO + " --yscroll defines the vertical scrolling delta, which is\n" + " 120 by default.\n" + HELP_COMMON_INFO_2 + "\n" + "* marks defaults.\n" + "\n" + "The server address is of the form: [][:]. The hostname\n" + "must be the address or hostname of the server. The port overrides the\n" + "default port, %d.\n", + args().m_pname, kDefaultPort + ); + + std::cout << buffer << std::endl; +} + +const char* +CClientApp::daemonName() const +{ +#if SYSAPI_WIN32 + return "Synergy Client"; +#elif SYSAPI_UNIX + return "synergyc"; +#endif +} + +const char* +CClientApp::daemonInfo() const +{ +#if SYSAPI_WIN32 + return "Allows another computer to share it's keyboard and mouse with this computer."; +#elif SYSAPI_UNIX + return ""; +#endif +} + +CScreen* +CClientApp::createScreen() +{ +#if WINAPI_MSWINDOWS + return new CScreen(new CMSWindowsScreen(false, args().m_noHooks, args().m_gameDevice)); +#elif WINAPI_XWINDOWS + return new CScreen(new CXWindowsScreen( + args().m_display, false, args().m_disableXInitThreads, + args().m_yscroll, *EVENTQUEUE)); +#elif WINAPI_CARBON + return new CScreen(new COSXScreen(false)); +#endif +} + +void +CClientApp::updateStatus() +{ + updateStatus(""); +} + + +void +CClientApp::updateStatus(const CString& msg) +{ + if (m_taskBarReceiver) + { + m_taskBarReceiver->updateStatus(s_client, msg); + } +} + + +void +CClientApp::resetRestartTimeout() +{ + // retry time can nolonger be changed + //s_retryTime = 0.0; +} + + +double +CClientApp::nextRestartTimeout() +{ + // retry at a constant rate (Issue 52) + return RETRY_TIME; + + /* + // choose next restart timeout. we start with rapid retries + // then slow down. + if (s_retryTime < 1.0) { + s_retryTime = 1.0; + } + else if (s_retryTime < 3.0) { + s_retryTime = 3.0; + } + else { + s_retryTime = 5.0; + } + return s_retryTime; + */ +} + + +void +CClientApp::handleScreenError(const CEvent&, void*) +{ + LOG((CLOG_CRIT "error on screen")); + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); +} + + +CScreen* +CClientApp::openClientScreen() +{ + CScreen* screen = createScreen(); + EVENTQUEUE->adoptHandler(IScreen::getErrorEvent(), + screen->getEventTarget(), + new TMethodEventJob( + this, &CClientApp::handleScreenError)); + return screen; +} + + +void +CClientApp::closeClientScreen(CScreen* screen) +{ + if (screen != NULL) { + EVENTQUEUE->removeHandler(IScreen::getErrorEvent(), + screen->getEventTarget()); + delete screen; + } +} + + +void +CClientApp::handleClientRestart(const CEvent&, void* vtimer) +{ + // discard old timer + CEventQueueTimer* timer = reinterpret_cast(vtimer); + EVENTQUEUE->deleteTimer(timer); + EVENTQUEUE->removeHandler(CEvent::kTimer, timer); + + // reconnect + startClient(); +} + + +void +CClientApp::scheduleClientRestart(double retryTime) +{ + // install a timer and handler to retry later + LOG((CLOG_DEBUG "retry in %.0f seconds", retryTime)); + CEventQueueTimer* timer = EVENTQUEUE->newOneShotTimer(retryTime, NULL); + EVENTQUEUE->adoptHandler(CEvent::kTimer, timer, + new TMethodEventJob(this, &CClientApp::handleClientRestart, timer)); +} + + +void +CClientApp::handleClientConnected(const CEvent&, void*) +{ + LOG((CLOG_NOTE "connected to server")); + resetRestartTimeout(); + updateStatus(); +} + + +void +CClientApp::handleClientFailed(const CEvent& e, void*) +{ + CClient::CFailInfo* info = + reinterpret_cast(e.getData()); + + updateStatus(CString("Failed to connect to server: ") + info->m_what); + if (!args().m_restartable || !info->m_retry) { + LOG((CLOG_ERR "failed to connect to server: %s", info->m_what.c_str())); + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); + } + else { + LOG((CLOG_WARN "failed to connect to server: %s", info->m_what.c_str())); + if (!m_suspended) { + scheduleClientRestart(nextRestartTimeout()); + } + } + delete info; +} + + +void +CClientApp::handleClientDisconnected(const CEvent&, void*) +{ + LOG((CLOG_NOTE "disconnected from server")); + if (!args().m_restartable) { + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); + } + else if (!m_suspended) { + s_client->connect(); + } + updateStatus(); +} + + +CClient* +CClientApp::openClient(const CString& name, const CNetworkAddress& address, CScreen* screen) +{ + CClient* client = new CClient( + *EVENTQUEUE, name, address, new CTCPSocketFactory, NULL, screen); + + try { + EVENTQUEUE->adoptHandler( + CClient::getConnectedEvent(), + client->getEventTarget(), + new TMethodEventJob(this, &CClientApp::handleClientConnected)); + + EVENTQUEUE->adoptHandler( + CClient::getConnectionFailedEvent(), + client->getEventTarget(), + new TMethodEventJob(this, &CClientApp::handleClientFailed)); + + EVENTQUEUE->adoptHandler( + CClient::getDisconnectedEvent(), + client->getEventTarget(), + new TMethodEventJob(this, &CClientApp::handleClientDisconnected)); + + } catch (std::bad_alloc &ba) { + delete client; + throw ba; + } + + return client; +} + + +void +CClientApp::closeClient(CClient* client) +{ + if (client == NULL) { + return; + } + + EVENTQUEUE->removeHandler(CClient::getConnectedEvent(), client); + EVENTQUEUE->removeHandler(CClient::getConnectionFailedEvent(), client); + EVENTQUEUE->removeHandler(CClient::getDisconnectedEvent(), client); + delete client; +} + +int +CClientApp::foregroundStartup(int argc, char** argv) +{ + initApp(argc, argv); + + // never daemonize + return mainLoop(); +} + +bool +CClientApp::startClient() +{ + double retryTime; + CScreen* clientScreen = NULL; + try { + if (s_clientScreen == NULL) { + clientScreen = openClientScreen(); + s_client = openClient(args().m_name, + *args().m_serverAddress, clientScreen); + s_clientScreen = clientScreen; + LOG((CLOG_NOTE "started client")); + } + +#if SYSAPI_WIN32 && GAME_DEVICE_SUPPORT + if (args().m_gameDevice.m_mode == CGameDeviceInfo::kGameModeXInput) + { + // TODO: currently this is failing because we're not + // forcing compile with the DX XInput.h (so the win + // SDK is being used)... we need to figure out how to + // tell cmake to prefer the DX include path. + LOG((CLOG_DEBUG "installing xinput hook")); + InstallXInputHook(); + } +#endif + + s_client->connect(); + updateStatus(); + return true; + } + catch (XScreenUnavailable& e) { + LOG((CLOG_WARN "secondary screen unavailable: %s", e.what())); + closeClientScreen(clientScreen); + updateStatus(CString("secondary screen unavailable: ") + e.what()); + retryTime = e.getRetryTime(); + } + catch (XScreenOpenFailure& e) { + LOG((CLOG_CRIT "failed to start client: %s", e.what())); + closeClientScreen(clientScreen); + return false; + } + catch (XBase& e) { + LOG((CLOG_CRIT "failed to start client: %s", e.what())); + closeClientScreen(clientScreen); + return false; + } + + if (args().m_restartable) { + scheduleClientRestart(retryTime); + return true; + } + else { + // don't try again + return false; + } +} + + +void +CClientApp::stopClient() +{ +#if SYSAPI_WIN32 && GAME_DEVICE_SUPPORT + if (args().m_gameDevice.m_mode == CGameDeviceInfo::kGameModeXInput) + { + LOG((CLOG_DEBUG "removing xinput hook")); + RemoveXInputHook(); + } +#endif + + closeClient(s_client); + closeClientScreen(s_clientScreen); + s_client = NULL; + s_clientScreen = NULL; +} + + +int +CClientApp::mainLoop() +{ + // create socket multiplexer. this must happen after daemonization + // on unix because threads evaporate across a fork(). + CSocketMultiplexer multiplexer; + + // create the event queue + CEventQueue eventQueue; + + // start client, etc + appUtil().startNode(); + + // load all available plugins. + ARCH->plugin().init(s_clientScreen->getEventTarget()); + + // run event loop. if startClient() failed we're supposed to retry + // later. the timer installed by startClient() will take care of + // that. + CEvent event; + DAEMON_RUNNING(true); + EVENTQUEUE->getEvent(event); + while (event.getType() != CEvent::kQuit) { + EVENTQUEUE->dispatchEvent(event); + CEvent::deleteData(event); + EVENTQUEUE->getEvent(event); + } + DAEMON_RUNNING(false); + + // close down + LOG((CLOG_DEBUG1 "stopping client")); + stopClient(); + updateStatus(); + LOG((CLOG_NOTE "stopped client")); + + return kExitSuccess; +} + +static +int +daemonMainLoopStatic(int argc, const char** argv) +{ + return CClientApp::instance().daemonMainLoop(argc, argv); +} + +int +CClientApp::standardStartup(int argc, char** argv) +{ + initApp(argc, argv); + + // daemonize if requested + if (args().m_daemon) { + return ARCH->daemonize(daemonName(), &daemonMainLoopStatic); + } + else { + return mainLoop(); + } +} + +int +CClientApp::runInner(int argc, char** argv, ILogOutputter* outputter, StartupFunc startup) +{ + // general initialization + args().m_serverAddress = new CNetworkAddress; + args().m_pname = ARCH->getBasename(argv[0]); + + // install caller's output filter + if (outputter != NULL) { + CLOG->insert(outputter); + } + + int result; + try + { + // run + result = startup(argc, argv); + } + catch (...) + { + if (m_taskBarReceiver) + { + // done with task bar receiver + delete m_taskBarReceiver; + } + + delete args().m_serverAddress; + + throw; + } + + return result; +} + +void +CClientApp::startNode() +{ + if (args().m_enableVnc) { + m_vncThread = new CThread(new TMethodJob( + this, &CClientApp::vncThread, NULL)); + } + + // start the client. if this return false then we've failed and + // we shouldn't retry. + LOG((CLOG_DEBUG1 "starting client")); + if (!startClient()) { + m_bye(kExitFailed); + } +} + +void +CClientApp::vncThread(void*) +{ +#if VNC_SUPPORT + vncServerMain(0, NULL); +#endif +} diff --git a/src/lib/synergy/CClientApp.h b/src/lib/synergy/CClientApp.h new file mode 100644 index 00000000..09aeec8e --- /dev/null +++ b/src/lib/synergy/CClientApp.h @@ -0,0 +1,93 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CApp.h" +#include "CArgsBase.h" + +class CScreen; +class CEvent; +class CClient; +class CNetworkAddress; +class CThread; + +class CClientApp : public CApp { +public: + class CArgs : public CArgsBase { + public: + CArgs(); + ~CArgs(); + + public: + int m_yscroll; + CNetworkAddress* m_serverAddress; + }; + + CClientApp(CreateTaskBarReceiverFunc createTaskBarReceiver); + virtual ~CClientApp(); + + // Parse client specific command line arguments. + void parseArgs(int argc, const char* const* argv); + + // Prints help specific to client. + void help(); + + // Returns arguments that are common and for client. + CArgs& args() const { return (CArgs&)argsBase(); } + + const char* daemonName() const; + const char* daemonInfo() const; + + // TODO: move to server only (not supported on client) + void loadConfig() { } + bool loadConfig(const CString& pathname) { return false; } + + int foregroundStartup(int argc, char** argv); + int standardStartup(int argc, char** argv); + int runInner(int argc, char** argv, ILogOutputter* outputter, StartupFunc startup); + CScreen* createScreen(); + void updateStatus(); + void updateStatus(const CString& msg); + void resetRestartTimeout(); + double nextRestartTimeout(); + void handleScreenError(const CEvent&, void*); + CScreen* openClientScreen(); + void closeClientScreen(CScreen* screen); + void handleClientRestart(const CEvent&, void* vtimer); + void scheduleClientRestart(double retryTime); + void handleClientConnected(const CEvent&, void*); + void handleClientFailed(const CEvent& e, void*); + void handleClientDisconnected(const CEvent&, void*); + CClient* openClient(const CString& name, const CNetworkAddress& address, CScreen* screen); + void closeClient(CClient* client); + bool startClient(); + void stopClient(); + int mainLoop(); + void startNode(); + + static CClientApp& instance() { return (CClientApp&)CApp::instance(); } + +private: + virtual bool parseArg(const int& argc, const char* const* argv, int& i); + void vncThread(void*); + +private: + CClient* s_client; + CScreen* s_clientScreen; + CThread* m_vncThread; +}; diff --git a/src/lib/synergy/CClientTaskBarReceiver.cpp b/src/lib/synergy/CClientTaskBarReceiver.cpp new file mode 100644 index 00000000..8f12b079 --- /dev/null +++ b/src/lib/synergy/CClientTaskBarReceiver.cpp @@ -0,0 +1,139 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClientTaskBarReceiver.h" +#include "CClient.h" +#include "CLock.h" +#include "CStringUtil.h" +#include "IEventQueue.h" +#include "CArch.h" +#include "Version.h" + +// +// CClientTaskBarReceiver +// + +CClientTaskBarReceiver::CClientTaskBarReceiver() : + m_state(kNotRunning) +{ + // do nothing +} + +CClientTaskBarReceiver::~CClientTaskBarReceiver() +{ + // do nothing +} + +void +CClientTaskBarReceiver::updateStatus(CClient* client, const CString& errorMsg) +{ + { + // update our status + m_errorMessage = errorMsg; + if (client == NULL) { + if (m_errorMessage.empty()) { + m_state = kNotRunning; + } + else { + m_state = kNotWorking; + } + } + else { + m_server = client->getServerAddress().getHostname(); + + if (client->isConnected()) { + m_state = kConnected; + } + else if (client->isConnecting()) { + m_state = kConnecting; + } + else { + m_state = kNotConnected; + } + } + + // let subclasses have a go + onStatusChanged(client); + } + + // tell task bar + ARCH->updateReceiver(this); +} + +CClientTaskBarReceiver::EState +CClientTaskBarReceiver::getStatus() const +{ + return m_state; +} + +const CString& +CClientTaskBarReceiver::getErrorMessage() const +{ + return m_errorMessage; +} + +void +CClientTaskBarReceiver::quit() +{ + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); +} + +void +CClientTaskBarReceiver::onStatusChanged(CClient*) +{ + // do nothing +} + +void +CClientTaskBarReceiver::lock() const +{ + // do nothing +} + +void +CClientTaskBarReceiver::unlock() const +{ + // do nothing +} + +std::string +CClientTaskBarReceiver::getToolTip() const +{ + switch (m_state) { + case kNotRunning: + return CStringUtil::print("%s: Not running", kAppVersion); + + case kNotWorking: + return CStringUtil::print("%s: %s", + kAppVersion, m_errorMessage.c_str()); + + case kNotConnected: + return CStringUtil::print("%s: Not connected: %s", + kAppVersion, m_errorMessage.c_str()); + + case kConnecting: + return CStringUtil::print("%s: Connecting to %s...", + kAppVersion, m_server.c_str()); + + case kConnected: + return CStringUtil::print("%s: Connected to %s", + kAppVersion, m_server.c_str()); + + default: + return ""; + } +} diff --git a/src/lib/synergy/CClientTaskBarReceiver.h b/src/lib/synergy/CClientTaskBarReceiver.h new file mode 100644 index 00000000..03416f71 --- /dev/null +++ b/src/lib/synergy/CClientTaskBarReceiver.h @@ -0,0 +1,91 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CCLIENTTASKBARRECEIVER_H +#define CCLIENTTASKBARRECEIVER_H + +#include "CString.h" +#include "IArchTaskBarReceiver.h" +#include "LogOutputters.h" +#include "CClient.h" + +//! Implementation of IArchTaskBarReceiver for the synergy server +class CClientTaskBarReceiver : public IArchTaskBarReceiver { +public: + CClientTaskBarReceiver(); + virtual ~CClientTaskBarReceiver(); + + //! @name manipulators + //@{ + + //! Update status + /*! + Determine the status and query required information from the client. + */ + void updateStatus(CClient*, const CString& errorMsg); + + void updateStatus(INode* n, const CString& errorMsg) { updateStatus((CClient*)n, errorMsg); } + + //@} + + // IArchTaskBarReceiver overrides + virtual void showStatus() = 0; + virtual void runMenu(int x, int y) = 0; + virtual void primaryAction() = 0; + virtual void lock() const; + virtual void unlock() const; + virtual const Icon getIcon() const = 0; + virtual std::string getToolTip() const; + virtual void cleanup() {} + +protected: + enum EState { + kNotRunning, + kNotWorking, + kNotConnected, + kConnecting, + kConnected, + kMaxState + }; + + //! Get status + EState getStatus() const; + + //! Get error message + const CString& getErrorMessage() const; + + //! Quit app + /*! + Causes the application to quit gracefully + */ + void quit(); + + //! Status change notification + /*! + Called when status changes. The default implementation does nothing. + */ + virtual void onStatusChanged(CClient* client); + +private: + EState m_state; + CString m_errorMessage; + CString m_server; +}; + +IArchTaskBarReceiver* createTaskBarReceiver(const CBufferedLogOutputter* logBuffer); + +#endif diff --git a/src/lib/synergy/CClipboard.cpp b/src/lib/synergy/CClipboard.cpp new file mode 100644 index 00000000..9469f0cc --- /dev/null +++ b/src/lib/synergy/CClipboard.cpp @@ -0,0 +1,117 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CClipboard.h" + +// +// CClipboard +// + +CClipboard::CClipboard() : + m_open(false), + m_owner(false) +{ + open(0); + empty(); + close(); +} + +CClipboard::~CClipboard() +{ + // do nothing +} + +bool +CClipboard::empty() +{ + assert(m_open); + + // clear all data + for (SInt32 index = 0; index < kNumFormats; ++index) { + m_data[index] = ""; + m_added[index] = false; + } + + // save time + m_timeOwned = m_time; + + // we're the owner now + m_owner = true; + + return true; +} + +void +CClipboard::add(EFormat format, const CString& data) +{ + assert(m_open); + assert(m_owner); + + m_data[format] = data; + m_added[format] = true; +} + +bool +CClipboard::open(Time time) const +{ + assert(!m_open); + + m_open = true; + m_time = time; + + return true; +} + +void +CClipboard::close() const +{ + assert(m_open); + + m_open = false; +} + +CClipboard::Time +CClipboard::getTime() const +{ + return m_timeOwned; +} + +bool +CClipboard::has(EFormat format) const +{ + assert(m_open); + return m_added[format]; +} + +CString +CClipboard::get(EFormat format) const +{ + assert(m_open); + return m_data[format]; +} + +void +CClipboard::unmarshall(const CString& data, Time time) +{ + IClipboard::unmarshall(this, data, time); +} + +CString +CClipboard::marshall() const +{ + return IClipboard::marshall(this); +} diff --git a/src/lib/synergy/CClipboard.h b/src/lib/synergy/CClipboard.h new file mode 100644 index 00000000..cdc14d31 --- /dev/null +++ b/src/lib/synergy/CClipboard.h @@ -0,0 +1,73 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CCLIPBOARD_H +#define CCLIPBOARD_H + +#include "IClipboard.h" + +//! Memory buffer clipboard +/*! +This class implements a clipboard that stores data in memory. +*/ +class CClipboard : public IClipboard { +public: + CClipboard(); + virtual ~CClipboard(); + + //! @name manipulators + //@{ + + //! Unmarshall clipboard data + /*! + Extract marshalled clipboard data and store it in this clipboard. + Sets the clipboard time to \c time. + */ + void unmarshall(const CString& data, Time time); + + //@} + //! @name accessors + //@{ + + //! Marshall clipboard data + /*! + Merge this clipboard's data into a single buffer that can be later + unmarshalled to restore the clipboard and return the buffer. + */ + CString marshall() const; + + //@} + + // IClipboard overrides + virtual bool empty(); + virtual void add(EFormat, const CString& data); + virtual bool open(Time) const; + virtual void close() const; + virtual Time getTime() const; + virtual bool has(EFormat) const; + virtual CString get(EFormat) const; + +private: + mutable bool m_open; + mutable Time m_time; + bool m_owner; + Time m_timeOwned; + bool m_added[kNumFormats]; + CString m_data[kNumFormats]; +}; + +#endif diff --git a/src/lib/synergy/CDaemonApp.cpp b/src/lib/synergy/CDaemonApp.cpp new file mode 100644 index 00000000..dd6ea8ad --- /dev/null +++ b/src/lib/synergy/CDaemonApp.cpp @@ -0,0 +1,326 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 . + */ + +// TODO: split this class into windows and unix to get rid +// of all the #ifdefs! + +#include "CDaemonApp.h" +#include "CEventQueue.h" +#include "LogOutputters.h" +#include "CLog.h" +#include "XArch.h" +#include "CApp.h" + +#include +#include +#include + +#if SYSAPI_WIN32 + +#include "CArchMiscWindows.h" +#include "XArchWindows.h" +#include "CScreen.h" +#include "CMSWindowsScreen.h" +#include "CMSWindowsRelauncher.h" +#include "CMSWindowsDebugOutputter.h" +#include "TMethodJob.h" + +#define WIN32_LEAN_AND_MEAN +#include + +#endif + +using namespace std; + +CDaemonApp* CDaemonApp::s_instance = NULL; + +int +mainLoopStatic() +{ + CDaemonApp::s_instance->mainLoop(true); + return kExitSuccess; +} + +int +unixMainLoopStatic(int, const char**) +{ + return mainLoopStatic(); +} + +#if SYSAPI_WIN32 +int +winMainLoopStatic(int, const char**) +{ + return CArchMiscWindows::runDaemon(mainLoopStatic); +} +#endif + +CDaemonApp::CDaemonApp() +#if SYSAPI_WIN32 + : m_relauncher(false) +#endif +{ + s_instance = this; +} + +CDaemonApp::~CDaemonApp() +{ +} + +int +CDaemonApp::run(int argc, char** argv) +{ + try + { +#if SYSAPI_WIN32 + // win32 instance needed for threading, etc. + CArchMiscWindows::setInstanceWin32(GetModuleHandle(NULL)); +#endif + + // send logging to gui via ipc + CLOG->insert(new CIpcLogOutputter()); + +#if SYSAPI_WIN32 + // sends debug messages to visual studio console window. + CLOG->insert(new CMSWindowsDebugOutputter()); + + CThread pipeThread(new TMethodJob( + this, &CDaemonApp::pipeThread, nullptr)); +#endif + + // default log level to system setting. + string logLevel = ARCH->setting("LogLevel"); + if (logLevel != "") + CLOG->setFilter(logLevel.c_str()); + + bool foreground = false; + + for (int i = 1; i < argc; ++i) { + string arg(argv[i]); + + if (arg == "/f" || arg == "-f") { + foreground = true; + } +#if SYSAPI_WIN32 + else if (arg == "/install") { + ARCH->installDaemon(); + return kExitSuccess; + } + else if (arg == "/uninstall") { + ARCH->uninstallDaemon(); + return kExitSuccess; + } +#endif + else { + stringstream ss; + ss << "Unrecognized argument: " << arg; + foregroundError(ss.str().c_str()); + return kExitArgs; + } + } + + if (foreground) { + // run process in foreground instead of daemonizing. + // useful for debugging. + mainLoop(false); + } + else { +#if SYSAPI_WIN32 + ARCH->daemonize("Synergy", winMainLoopStatic); +#elif SYSAPI_UNIX + ARCH->daemonize("Synergy", unixMainLoopStatic); +#endif + } + + return kExitSuccess; + } + catch (XArch& e) { + foregroundError(e.what().c_str()); + return kExitFailed; + } + catch (std::exception& e) { + foregroundError(e.what()); + return kExitFailed; + } + catch (...) { + foregroundError("Unrecognized error."); + return kExitFailed; + } +} + +void +CDaemonApp::mainLoop(bool logToFile) +{ + try + { + DAEMON_RUNNING(true); + + if (logToFile) + CLOG->insert(new CFileLogOutputter(logPath().c_str())); + + CEventQueue eventQueue; + +#if SYSAPI_WIN32 + // HACK: create a dummy screen, which can handle system events + // (such as a stop request from the service controller). + CMSWindowsScreen::init(CArchMiscWindows::instanceWin32()); + CGameDeviceInfo gameDevice; + CScreen dummyScreen(new CMSWindowsScreen(false, true, gameDevice)); + + string command = ARCH->setting("Command"); + if (command != "") { + LOG((CLOG_INFO "using last known command: %s", command.c_str())); + m_relauncher.command(command); + } + + m_relauncher.startAsync(); +#endif + + CEvent event; + EVENTQUEUE->getEvent(event); + while (event.getType() != CEvent::kQuit) { + EVENTQUEUE->dispatchEvent(event); + CEvent::deleteData(event); + EVENTQUEUE->getEvent(event); + } + +#if SYSAPI_WIN32 + m_relauncher.stop(); +#endif + + DAEMON_RUNNING(false); + } + catch (XArch& e) { + LOG((CLOG_ERR, e.what().c_str())); + } + catch (std::exception& e) { + LOG((CLOG_ERR, e.what())); + } + catch (...) { + LOG((CLOG_ERR, "Unrecognized error.")); + } +} + +void +CDaemonApp::foregroundError(const char* message) +{ +#if SYSAPI_WIN32 + MessageBox(NULL, message, "Synergy Service", MB_OK | MB_ICONERROR); +#elif SYSAPI_UNIX + cerr << message << endl; +#endif +} + +std::string +CDaemonApp::logPath() +{ +#ifdef SYSAPI_WIN32 + // TODO: move to CArchMiscWindows + // on windows, log to the same dir as the binary. + char fileNameBuffer[MAX_PATH]; + GetModuleFileName(NULL, fileNameBuffer, MAX_PATH); + string fileName(fileNameBuffer); + size_t lastSlash = fileName.find_last_of("\\"); + string path(fileName.substr(0, lastSlash)); + path.append("\\").append(LOG_FILENAME); + return path; +#elif SYSAPI_UNIX + return "/var/log/" LOG_FILENAME; +#endif +} + +#ifdef SYSAPI_WIN32 + +void +CDaemonApp::pipeThread(void*) +{ + // TODO: move this to an IPC server class. + while (true) { + + // grant access to everyone. + SECURITY_DESCRIPTOR sd; + InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION); + SetSecurityDescriptorDacl(&sd, TRUE, static_cast(0), FALSE); + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.lpSecurityDescriptor = &sd; + + HANDLE pipe = CreateNamedPipe( + _T("\\\\.\\pipe\\Synergy"), + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, + PIPE_UNLIMITED_INSTANCES, + 1024, 1024, 0, &sa); + + if (pipe == INVALID_HANDLE_VALUE) + XArch("could not create named pipe."); + + LOG((CLOG_DEBUG "opened daemon pipe: %d", pipe)); + BOOL connectResult = ConnectNamedPipe(pipe, NULL); + + char buffer[1024]; + DWORD bytesRead; + + while (true) { + if (!ReadFile(pipe, buffer, sizeof(buffer), &bytesRead, NULL)) { + break; + } + + buffer[bytesRead] = '\0'; + LOG((CLOG_DEBUG "ipc daemon server read: %s", buffer)); + + try { + handlePipeMessage(buffer); + } + catch (XArch& ex) { + LOG((CLOG_ERR "handle message failed: %s", ex.what().c_str())); + } + } + + DisconnectNamedPipe(pipe); + CloseHandle(pipe); + } +} + +void +CDaemonApp::handlePipeMessage(char* buffer) +{ + switch (buffer[0]) { + case kIpcCommand: + { + string command(++buffer); + + // store command in system settings. this is used when the daemon + // next starts. + ARCH->setting("Command", command); + + // tell the relauncher about the new command. this causes the + // relauncher to stop the existing command and start the new + // command. + m_relauncher.command(command); + } + break; + + default: + LOG((CLOG_WARN "unrecognized ipc message: %d", buffer[0])); + break; + } +} + + +#endif diff --git a/src/lib/synergy/CDaemonApp.h b/src/lib/synergy/CDaemonApp.h new file mode 100644 index 00000000..a87d5d2d --- /dev/null +++ b/src/lib/synergy/CDaemonApp.h @@ -0,0 +1,56 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CArch.h" + +#if SYSAPI_WIN32 +#include "CMSWindowsRelauncher.h" +#endif + +#include + +class CDaemonApp { + +public: + CDaemonApp(); + virtual ~CDaemonApp(); + int run(int argc, char** argv); + void mainLoop(bool logToFile); + +private: + void daemonize(); + void foregroundError(const char* message); + std::string logPath(); +#if SYSAPI_WIN32 + void pipeThread(void*); + void handlePipeMessage(char* buffer); +#endif + +public: + static CDaemonApp* s_instance; +#if SYSAPI_WIN32 + CMSWindowsRelauncher m_relauncher; +#endif +}; + +#define LOG_FILENAME "synergyd.log" + +enum { + kIpcCommand = 1 +}; diff --git a/src/lib/synergy/CEventGameDevice.cpp b/src/lib/synergy/CEventGameDevice.cpp new file mode 100644 index 00000000..82de89e2 --- /dev/null +++ b/src/lib/synergy/CEventGameDevice.cpp @@ -0,0 +1,57 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CEventGameDevice.h" + +CEventGameDevice::CEventGameDevice(void* eventTarget) : + m_eventTarget(eventTarget) +{ +} + +CEventGameDevice::~CEventGameDevice() +{ +} + +void +CEventGameDevice::gameDeviceTimingResp(UInt16 freq) +{ +} + +void +CEventGameDevice::gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2) +{ +} + +void +CEventGameDevice::fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const +{ +} + +void +CEventGameDevice::fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const +{ +} + +void +CEventGameDevice::fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const +{ +} + +void +CEventGameDevice::queueGameDeviceTimingReq() const +{ +} diff --git a/src/lib/synergy/CEventGameDevice.h b/src/lib/synergy/CEventGameDevice.h new file mode 100644 index 00000000..700b2035 --- /dev/null +++ b/src/lib/synergy/CEventGameDevice.h @@ -0,0 +1,36 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CGameDevice.h" + +class CEventGameDevice : public CGameDevice +{ +public: + CEventGameDevice(void* eventTarget); + virtual ~CEventGameDevice(); + + void gameDeviceTimingResp(UInt16 freq); + void gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2); + void fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const; + void fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const; + void fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const; + void queueGameDeviceTimingReq() const; +private: + void* m_eventTarget; +}; diff --git a/src/lib/synergy/CGameDevice.cpp b/src/lib/synergy/CGameDevice.cpp new file mode 100644 index 00000000..3a42b5e9 --- /dev/null +++ b/src/lib/synergy/CGameDevice.cpp @@ -0,0 +1,37 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CGameDevice.h" + +CGameDeviceInfo::CGameDeviceInfo() : +m_mode(CGameDeviceInfo::kGameModeNone), +m_poll(CGameDeviceInfo::kGamePollDynamic), +m_pollFreq(60) +{ +} + +CGameDeviceInfo::~CGameDeviceInfo() +{ +} + +CGameDevice::CGameDevice() +{ +} + +CGameDevice::~CGameDevice() +{ +} diff --git a/src/lib/synergy/CGameDevice.h b/src/lib/synergy/CGameDevice.h new file mode 100644 index 00000000..1f47d58b --- /dev/null +++ b/src/lib/synergy/CGameDevice.h @@ -0,0 +1,56 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "GameDeviceTypes.h" + +class CGameDeviceInfo +{ +public: + + enum EGameMode { + kGameModeNone, + kGameModeXInput, + kGameModeJoyInfoEx + }; + + enum EGamePoll { + kGamePollDynamic, + kGamePollStatic + }; + + CGameDeviceInfo(); + virtual ~CGameDeviceInfo(); + EGameMode m_mode; + EGamePoll m_poll; + int m_pollFreq; +}; + +class CGameDevice +{ +public: + CGameDevice(); + virtual ~CGameDevice(); + + virtual void gameDeviceTimingResp(UInt16 freq) = 0; + virtual void gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2) = 0; + virtual void fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const = 0; + virtual void fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const = 0; + virtual void fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const = 0; + virtual void queueGameDeviceTimingReq() const = 0; +}; diff --git a/src/lib/synergy/CKeyMap.cpp b/src/lib/synergy/CKeyMap.cpp new file mode 100644 index 00000000..8683ad9a --- /dev/null +++ b/src/lib/synergy/CKeyMap.cpp @@ -0,0 +1,1334 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2005 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CKeyMap.h" +#include "KeyTypes.h" +#include "CLog.h" +#include +#include +#include + +CKeyMap::CNameToKeyMap* CKeyMap::s_nameToKeyMap = NULL; +CKeyMap::CNameToModifierMap* CKeyMap::s_nameToModifierMap = NULL; +CKeyMap::CKeyToNameMap* CKeyMap::s_keyToNameMap = NULL; +CKeyMap::CModifierToNameMap* CKeyMap::s_modifierToNameMap = NULL; + +CKeyMap::CKeyMap() : + m_numGroups(0), + m_composeAcrossGroups(false) +{ + m_modifierKeyItem.m_id = kKeyNone; + m_modifierKeyItem.m_group = 0; + m_modifierKeyItem.m_button = 0; + m_modifierKeyItem.m_required = 0; + m_modifierKeyItem.m_sensitive = 0; + m_modifierKeyItem.m_generates = 0; + m_modifierKeyItem.m_dead = false; + m_modifierKeyItem.m_lock = false; + m_modifierKeyItem.m_client = 0; +} + +CKeyMap::~CKeyMap() +{ + // do nothing +} + +void +CKeyMap::swap(CKeyMap& x) +{ + m_keyIDMap.swap(x.m_keyIDMap); + m_modifierKeys.swap(x.m_modifierKeys); + m_halfDuplex.swap(x.m_halfDuplex); + m_halfDuplexMods.swap(x.m_halfDuplexMods); + SInt32 tmp1 = m_numGroups; + m_numGroups = x.m_numGroups; + x.m_numGroups = tmp1; + bool tmp2 = m_composeAcrossGroups; + m_composeAcrossGroups = x.m_composeAcrossGroups; + x.m_composeAcrossGroups = tmp2; +} + +void +CKeyMap::addKeyEntry(const KeyItem& item) +{ + // ignore kKeyNone + if (item.m_id == kKeyNone) { + return; + } + + // resize number of groups for key + SInt32 numGroups = item.m_group + 1; + if (getNumGroups() > numGroups) { + numGroups = getNumGroups(); + } + KeyGroupTable& groupTable = m_keyIDMap[item.m_id]; + if (groupTable.size() < static_cast(numGroups)) { + groupTable.resize(numGroups); + } + + // make a list from the item + KeyItemList items; + items.push_back(item); + + // set group and dead key flag on the item + KeyItem& newItem = items.back(); + newItem.m_dead = isDeadKey(item.m_id); + + // mask the required bits with the sensitive bits + newItem.m_required &= newItem.m_sensitive; + + // see if we already have this item; just return if so + KeyEntryList& entries = groupTable[item.m_group]; + for (size_t i = 0, n = entries.size(); i < n; ++i) { + if (entries[i].size() == 1 && newItem == entries[i][0]) { + return; + } + } + + // add item list + entries.push_back(items); + LOG((CLOG_DEBUG3 "add key: %04x %d %03x %04x (%04x %04x %04x)%s", newItem.m_id, newItem.m_group, newItem.m_button, newItem.m_client, newItem.m_required, newItem.m_sensitive, newItem.m_generates, newItem.m_dead ? " dead" : "")); +} + +void +CKeyMap::addKeyAliasEntry(KeyID targetID, SInt32 group, + KeyModifierMask targetRequired, + KeyModifierMask targetSensitive, + KeyID sourceID, + KeyModifierMask sourceRequired, + KeyModifierMask sourceSensitive) +{ + // if we can already generate the target as desired then we're done. + if (findCompatibleKey(targetID, group, targetRequired, + targetSensitive) != NULL) { + return; + } + + // find a compatible source, preferably in the same group + for (SInt32 gd = 0, n = getNumGroups(); gd < n; ++gd) { + SInt32 eg = getEffectiveGroup(group, gd); + const KeyItemList* sourceEntry = + findCompatibleKey(sourceID, eg, + sourceRequired, sourceSensitive); + if (sourceEntry != NULL && sourceEntry->size() == 1) { + CKeyMap::KeyItem targetItem = sourceEntry->back(); + targetItem.m_id = targetID; + targetItem.m_group = eg; + addKeyEntry(targetItem); + break; + } + } +} + +bool +CKeyMap::addKeyCombinationEntry(KeyID id, SInt32 group, + const KeyID* keys, UInt32 numKeys) +{ + // disallow kKeyNone + if (id == kKeyNone) { + return false; + } + + SInt32 numGroups = group + 1; + if (getNumGroups() > numGroups) { + numGroups = getNumGroups(); + } + KeyGroupTable& groupTable = m_keyIDMap[id]; + if (groupTable.size() < static_cast(numGroups)) { + groupTable.resize(numGroups); + } + if (!groupTable[group].empty()) { + // key is already in the table + return false; + } + + // convert to buttons + KeyItemList items; + for (UInt32 i = 0; i < numKeys; ++i) { + KeyIDMap::const_iterator gtIndex = m_keyIDMap.find(keys[i]); + if (gtIndex == m_keyIDMap.end()) { + return false; + } + const KeyGroupTable& groupTable = gtIndex->second; + + // if we allow group switching during composition then search all + // groups for keys, otherwise search just the given group. + SInt32 n = 1; + if (m_composeAcrossGroups) { + n = (SInt32)groupTable.size(); + } + + bool found = false; + for (SInt32 gd = 0; gd < n && !found; ++gd) { + SInt32 eg = (group + gd) % getNumGroups(); + const KeyEntryList& entries = groupTable[eg]; + for (size_t j = 0; j < entries.size(); ++j) { + if (entries[j].size() == 1) { + found = true; + items.push_back(entries[j][0]); + break; + } + } + } + if (!found) { + // required key is not in keyboard group + return false; + } + } + + // add key + groupTable[group].push_back(items); + return true; +} + +void +CKeyMap::allowGroupSwitchDuringCompose() +{ + m_composeAcrossGroups = true; +} + +void +CKeyMap::addHalfDuplexButton(KeyButton button) +{ + m_halfDuplex.insert(button); +} + +void +CKeyMap::clearHalfDuplexModifiers() +{ + m_halfDuplexMods.clear(); +} + +void +CKeyMap::addHalfDuplexModifier(KeyID key) +{ + m_halfDuplexMods.insert(key); +} + +void +CKeyMap::finish() +{ + m_numGroups = findNumGroups(); + + // make sure every key has the same number of groups + for (KeyIDMap::iterator i = m_keyIDMap.begin(); + i != m_keyIDMap.end(); ++i) { + i->second.resize(m_numGroups); + } + + // compute keys that generate each modifier + setModifierKeys(); +} + +void +CKeyMap::foreachKey(ForeachKeyCallback cb, void* userData) +{ + for (KeyIDMap::iterator i = m_keyIDMap.begin(); + i != m_keyIDMap.end(); ++i) { + KeyGroupTable& groupTable = i->second; + for (size_t group = 0; group < groupTable.size(); ++group) { + KeyEntryList& entryList = groupTable[group]; + for (size_t j = 0; j < entryList.size(); ++j) { + KeyItemList& itemList = entryList[j]; + for (size_t k = 0; k < itemList.size(); ++k) { + (*cb)(i->first, static_cast(group), + itemList[k], userData); + } + } + } + } +} + +const CKeyMap::KeyItem* +CKeyMap::mapKey(Keystrokes& keys, KeyID id, SInt32 group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask desiredMask, + bool isAutoRepeat) const +{ + LOG((CLOG_DEBUG1 "mapKey %04x (%d) with mask %04x, start state: %04x", id, id, desiredMask, currentState)); + + // handle group change + if (id == kKeyNextGroup) { + keys.push_back(Keystroke(1, false, false)); + return NULL; + } + else if (id == kKeyPrevGroup) { + keys.push_back(Keystroke(-1, false, false)); + return NULL; + } + + const KeyItem* item; + switch (id) { + case kKeyShift_L: + case kKeyShift_R: + case kKeyControl_L: + case kKeyControl_R: + case kKeyAlt_L: + case kKeyAlt_R: + case kKeyMeta_L: + case kKeyMeta_R: + case kKeySuper_L: + case kKeySuper_R: + case kKeyAltGr: + case kKeyCapsLock: + case kKeyNumLock: + case kKeyScrollLock: + item = mapModifierKey(keys, id, group, activeModifiers, + currentState, desiredMask, isAutoRepeat); + break; + + case kKeySetModifiers: + if (!keysForModifierState(0, group, activeModifiers, currentState, + desiredMask, desiredMask, 0, keys)) { + LOG((CLOG_DEBUG1 "unable to set modifiers %04x", desiredMask)); + return NULL; + } + return &m_modifierKeyItem; + + case kKeyClearModifiers: + if (!keysForModifierState(0, group, activeModifiers, currentState, + currentState & ~desiredMask, + desiredMask, 0, keys)) { + LOG((CLOG_DEBUG1 "unable to clear modifiers %04x", desiredMask)); + return NULL; + } + return &m_modifierKeyItem; + + default: + if (isCommand(desiredMask)) { + item = mapCommandKey(keys, id, group, activeModifiers, + currentState, desiredMask, isAutoRepeat); + } + else { + item = mapCharacterKey(keys, id, group, activeModifiers, + currentState, desiredMask, isAutoRepeat); + } + break; + } + + if (item != NULL) { + LOG((CLOG_DEBUG1 "mapped to %03x, new state %04x", item->m_button, currentState)); + } + return item; +} + +SInt32 +CKeyMap::getNumGroups() const +{ + return m_numGroups; +} + +SInt32 +CKeyMap::getEffectiveGroup(SInt32 group, SInt32 offset) const +{ + return (group + offset + getNumGroups()) % getNumGroups(); +} + +const CKeyMap::KeyItemList* +CKeyMap::findCompatibleKey(KeyID id, SInt32 group, + KeyModifierMask required, KeyModifierMask sensitive) const +{ + assert(group >= 0 && group < getNumGroups()); + + KeyIDMap::const_iterator i = m_keyIDMap.find(id); + if (i == m_keyIDMap.end()) { + return NULL; + } + + const KeyEntryList& entries = i->second[group]; + for (size_t j = 0; j < entries.size(); ++j) { + if ((entries[j].back().m_sensitive & sensitive) == 0 || + (entries[j].back().m_required & sensitive) == + (required & sensitive)) { + return &entries[j]; + } + } + + return NULL; +} + +bool +CKeyMap::isHalfDuplex(KeyID key, KeyButton button) const +{ + return (m_halfDuplex.count(button) + m_halfDuplexMods.count(key) > 0); +} + +bool +CKeyMap::isCommand(KeyModifierMask mask) const +{ + return ((mask & getCommandModifiers()) != 0); +} + +KeyModifierMask +CKeyMap::getCommandModifiers() const +{ + // we currently treat ctrl, alt, meta and super as command modifiers. + // some platforms may have a more limited set (OS X only needs Alt) + // but this works anyway. + return KeyModifierControl | + KeyModifierAlt | + KeyModifierAltGr | + KeyModifierMeta | + KeyModifierSuper; +} + +void +CKeyMap::collectButtons(const ModifierToKeys& mods, ButtonToKeyMap& keys) +{ + keys.clear(); + for (ModifierToKeys::const_iterator i = mods.begin(); + i != mods.end(); ++i) { + keys.insert(std::make_pair(i->second.m_button, &i->second)); + } +} + +void +CKeyMap::initModifierKey(KeyItem& item) +{ + item.m_generates = 0; + item.m_lock = false; + switch (item.m_id) { + case kKeyShift_L: + case kKeyShift_R: + item.m_generates = KeyModifierShift; + break; + + case kKeyControl_L: + case kKeyControl_R: + item.m_generates = KeyModifierControl; + break; + + case kKeyAlt_L: + case kKeyAlt_R: + item.m_generates = KeyModifierAlt; + break; + + case kKeyMeta_L: + case kKeyMeta_R: + item.m_generates = KeyModifierMeta; + break; + + case kKeySuper_L: + case kKeySuper_R: + item.m_generates = KeyModifierSuper; + break; + + case kKeyAltGr: + item.m_generates = KeyModifierAltGr; + break; + + case kKeyCapsLock: + item.m_generates = KeyModifierCapsLock; + item.m_lock = true; + break; + + case kKeyNumLock: + item.m_generates = KeyModifierNumLock; + item.m_lock = true; + break; + + case kKeyScrollLock: + item.m_generates = KeyModifierScrollLock; + item.m_lock = true; + break; + + default: + // not a modifier + break; + } +} + +SInt32 +CKeyMap::findNumGroups() const +{ + size_t max = 0; + for (KeyIDMap::const_iterator i = m_keyIDMap.begin(); + i != m_keyIDMap.end(); ++i) { + if (i->second.size() > max) { + max = i->second.size(); + } + } + return static_cast(max); +} + +void +CKeyMap::setModifierKeys() +{ + m_modifierKeys.clear(); + m_modifierKeys.resize(kKeyModifierNumBits * getNumGroups()); + for (KeyIDMap::const_iterator i = m_keyIDMap.begin(); + i != m_keyIDMap.end(); ++i) { + const KeyGroupTable& groupTable = i->second; + for (size_t g = 0; g < groupTable.size(); ++g) { + const KeyEntryList& entries = groupTable[g]; + for (size_t j = 0; j < entries.size(); ++j) { + // skip multi-key sequences + if (entries[j].size() != 1) { + continue; + } + + // skip keys that don't generate a modifier + const KeyItem& item = entries[j].back(); + if (item.m_generates == 0) { + continue; + } + + // add key to each indicated modifier in this group + for (SInt32 b = 0; b < kKeyModifierNumBits; ++b) { + // skip if item doesn't generate bit b + if (((1u << b) & item.m_generates) != 0) { + SInt32 mIndex = (SInt32)g * kKeyModifierNumBits + b; + m_modifierKeys[mIndex].push_back(&item); + } + } + } + } + } +} + +const CKeyMap::KeyItem* +CKeyMap::mapCommandKey(Keystrokes& keys, KeyID id, SInt32 group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask desiredMask, + bool isAutoRepeat) const +{ + static const KeyModifierMask s_overrideModifiers = 0xffffu; + + // find KeySym in table + KeyIDMap::const_iterator i = m_keyIDMap.find(id); + if (i == m_keyIDMap.end()) { + // unknown key + LOG((CLOG_DEBUG1 "key %04x is not on keyboard", id)); + return NULL; + } + const KeyGroupTable& keyGroupTable = i->second; + + // find the first key that generates this KeyID + const KeyItem* keyItem = NULL; + SInt32 numGroups = getNumGroups(); + for (SInt32 groupOffset = 0; groupOffset < numGroups; ++groupOffset) { + SInt32 effectiveGroup = getEffectiveGroup(group, groupOffset); + const KeyEntryList& entryList = keyGroupTable[effectiveGroup]; + for (size_t i = 0; i < entryList.size(); ++i) { + if (entryList[i].size() != 1) { + // ignore multikey entries + continue; + } + + // only match based on shift; we're after the right button + // not the right character. we'll use desiredMask as-is, + // overriding the key's required modifiers, when synthesizing + // this button. + const KeyItem& item = entryList[i].back(); + if ((item.m_required & KeyModifierShift & desiredMask) == + (item.m_sensitive & KeyModifierShift & desiredMask)) { + LOG((CLOG_DEBUG1 "found key in group %d", effectiveGroup)); + keyItem = &item; + break; + } + } + if (keyItem != NULL) { + break; + } + } + if (keyItem == NULL) { + // no mapping for this keysym + LOG((CLOG_DEBUG1 "no mapping for key %04x", id)); + return NULL; + } + + // make working copy of modifiers + ModifierToKeys newModifiers = activeModifiers; + KeyModifierMask newState = currentState; + SInt32 newGroup = group; + + // don't try to change CapsLock + desiredMask = (desiredMask & ~KeyModifierCapsLock) | + (currentState & KeyModifierCapsLock); + + // add the key + if (!keysForKeyItem(*keyItem, newGroup, newModifiers, + newState, desiredMask, + s_overrideModifiers, isAutoRepeat, keys)) { + LOG((CLOG_DEBUG1 "can't map key")); + keys.clear(); + return NULL; + } + + // add keystrokes to restore modifier keys + if (!keysToRestoreModifiers(*keyItem, group, newModifiers, newState, + activeModifiers, keys)) { + LOG((CLOG_DEBUG1 "failed to restore modifiers")); + keys.clear(); + return NULL; + } + + // add keystrokes to restore group + if (newGroup != group) { + keys.push_back(Keystroke(group, true, true)); + } + + // save new modifiers + activeModifiers = newModifiers; + currentState = newState; + + return keyItem; +} + +const CKeyMap::KeyItem* +CKeyMap::mapCharacterKey(Keystrokes& keys, KeyID id, SInt32 group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask desiredMask, + bool isAutoRepeat) const +{ + // find KeySym in table + KeyIDMap::const_iterator i = m_keyIDMap.find(id); + if (i == m_keyIDMap.end()) { + // unknown key + LOG((CLOG_DEBUG1 "key %04x is not on keyboard", id)); + return NULL; + } + const KeyGroupTable& keyGroupTable = i->second; + + // find best key in any group, starting with the active group + SInt32 keyIndex = -1; + SInt32 numGroups = getNumGroups(); + SInt32 groupOffset; + LOG((CLOG_DEBUG1 "find best: %04x %04x", currentState, desiredMask)); + for (groupOffset = 0; groupOffset < numGroups; ++groupOffset) { + SInt32 effectiveGroup = getEffectiveGroup(group, groupOffset); + keyIndex = findBestKey(keyGroupTable[effectiveGroup], + currentState, desiredMask); + if (keyIndex != -1) { + LOG((CLOG_DEBUG1 "found key in group %d", effectiveGroup)); + break; + } + } + if (keyIndex == -1) { + // no mapping for this keysym + LOG((CLOG_DEBUG1 "no mapping for key %04x", id)); + return NULL; + } + + // get keys to press for key + SInt32 effectiveGroup = getEffectiveGroup(group, groupOffset); + const KeyItemList& itemList = keyGroupTable[effectiveGroup][keyIndex]; + if (itemList.empty()) { + return NULL; + } + const KeyItem& keyItem = itemList.back(); + + // make working copy of modifiers + ModifierToKeys newModifiers = activeModifiers; + KeyModifierMask newState = currentState; + SInt32 newGroup = group; + + // add each key + for (size_t j = 0; j < itemList.size(); ++j) { + if (!keysForKeyItem(itemList[j], newGroup, newModifiers, + newState, desiredMask, + 0, isAutoRepeat, keys)) { + LOG((CLOG_DEBUG1 "can't map key")); + keys.clear(); + return NULL; + } + } + + // add keystrokes to restore modifier keys + if (!keysToRestoreModifiers(keyItem, group, newModifiers, newState, + activeModifiers, keys)) { + LOG((CLOG_DEBUG1 "failed to restore modifiers")); + keys.clear(); + return NULL; + } + + // add keystrokes to restore group + if (newGroup != group) { + keys.push_back(Keystroke(group, true, true)); + } + + // save new modifiers + activeModifiers = newModifiers; + currentState = newState; + + return &keyItem; +} + +const CKeyMap::KeyItem* +CKeyMap::mapModifierKey(Keystrokes& keys, KeyID id, SInt32 group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask desiredMask, + bool isAutoRepeat) const +{ + return mapCharacterKey(keys, id, group, activeModifiers, + currentState, desiredMask, isAutoRepeat); +} + +SInt32 +CKeyMap::findBestKey(const KeyEntryList& entryList, + KeyModifierMask /*currentState*/, + KeyModifierMask desiredState) const +{ + // check for an item that can accommodate the desiredState exactly + for (SInt32 i = 0; i < (SInt32)entryList.size(); ++i) { + const KeyItem& item = entryList[i].back(); + if ((item.m_required & desiredState) == + (item.m_sensitive & desiredState)) { + LOG((CLOG_DEBUG1 "best key index %d of %d (exact)", i, entryList.size())); + return i; + } + } + + // choose the item that requires the fewest modifier changes + SInt32 bestCount = 32; + SInt32 bestIndex = -1; + for (SInt32 i = 0; i < (SInt32)entryList.size(); ++i) { + const KeyItem& item = entryList[i].back(); + KeyModifierMask change = + ((item.m_required ^ desiredState) & item.m_sensitive); + SInt32 n = getNumModifiers(change); + if (n < bestCount) { + bestCount = n; + bestIndex = i; + } + } + if (bestIndex != -1) { + LOG((CLOG_DEBUG1 "best key index %d of %d (%d modifiers)", bestIndex, entryList.size(), bestCount)); + } + + return bestIndex; +} + + +const CKeyMap::KeyItem* +CKeyMap::keyForModifier(KeyButton button, SInt32 group, + SInt32 modifierBit) const +{ + assert(modifierBit >= 0 && modifierBit < kKeyModifierNumBits); + assert(group >= 0 && group < getNumGroups()); + + // find a key that generates the given modifier in the given group + // but doesn't use the given button, presumably because we're trying + // to generate a KeyID that's only bound the the given button. + // this is important when a shift button is modified by shift; we + // must use the other shift button to do the shifting. + const ModifierKeyItemList& items = + m_modifierKeys[group * kKeyModifierNumBits + modifierBit]; + for (ModifierKeyItemList::const_iterator i = items.begin(); + i != items.end(); ++i) { + if ((*i)->m_button != button) { + return (*i); + } + } + return NULL; +} + +bool +CKeyMap::keysForKeyItem(const KeyItem& keyItem, SInt32& group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, KeyModifierMask desiredState, + KeyModifierMask overrideModifiers, + bool isAutoRepeat, + Keystrokes& keystrokes) const +{ + static const KeyModifierMask s_notRequiredMask = + KeyModifierAltGr | KeyModifierNumLock | KeyModifierScrollLock; + + // add keystrokes to adjust the group + if (group != keyItem.m_group) { + group = keyItem.m_group; + keystrokes.push_back(Keystroke(group, true, false)); + } + + EKeystroke type; + if (keyItem.m_dead) { + // adjust modifiers for dead key + if (!keysForModifierState(keyItem.m_button, group, + activeModifiers, currentState, + keyItem.m_required, keyItem.m_sensitive, + 0, keystrokes)) { + LOG((CLOG_DEBUG1 "unable to match modifier state for dead key %d", keyItem.m_button)); + return false; + } + + // press and release the dead key + type = kKeystrokeClick; + } + else { + // if this a command key then we don't have to match some of the + // key's required modifiers. + KeyModifierMask sensitive = keyItem.m_sensitive & ~overrideModifiers; + + // XXX -- must handle pressing a modifier. in particular, if we want + // to synthesize a KeyID on level 1 of a KeyButton that has Shift_L + // mapped to level 0 then we must release that button if it's down + // (in order to satisfy a shift modifier) then press a different + // button (any other button) mapped to the shift modifier and then + // the Shift_L button. + // match key's required state + LOG((CLOG_DEBUG1 "state: %04x,%04x,%04x", currentState, keyItem.m_required, sensitive)); + if (!keysForModifierState(keyItem.m_button, group, + activeModifiers, currentState, + keyItem.m_required, sensitive, + 0, keystrokes)) { + LOG((CLOG_DEBUG1 "unable to match modifier state (%04x,%04x) for key %d", keyItem.m_required, keyItem.m_sensitive, keyItem.m_button)); + return false; + } + + // match desiredState as closely as possible. we must not + // change any modifiers in keyItem.m_sensitive. and if the key + // is a modifier, we don't want to change that modifier. + LOG((CLOG_DEBUG1 "desired state: %04x %04x,%04x,%04x", desiredState, currentState, keyItem.m_required, keyItem.m_sensitive)); + if (!keysForModifierState(keyItem.m_button, group, + activeModifiers, currentState, + desiredState, + ~(sensitive | keyItem.m_generates), + s_notRequiredMask, keystrokes)) { + LOG((CLOG_DEBUG1 "unable to match desired modifier state (%04x,%04x) for key %d", desiredState, ~keyItem.m_sensitive & 0xffffu, keyItem.m_button)); + return false; + } + + // repeat or press of key + type = isAutoRepeat ? kKeystrokeRepeat : kKeystrokePress; + } + addKeystrokes(type, keyItem, activeModifiers, currentState, keystrokes); + + return true; +} + +bool +CKeyMap::keysToRestoreModifiers(const KeyItem& keyItem, SInt32, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + const ModifierToKeys& desiredModifiers, + Keystrokes& keystrokes) const +{ + // XXX -- we're not considering modified modifiers here + + ModifierToKeys oldModifiers = activeModifiers; + + // get the pressed modifier buttons before and after + ButtonToKeyMap oldKeys, newKeys; + collectButtons(oldModifiers, oldKeys); + collectButtons(desiredModifiers, newKeys); + + // release unwanted keys + for (ModifierToKeys::const_iterator i = oldModifiers.begin(); + i != oldModifiers.end(); ++i) { + KeyButton button = i->second.m_button; + if (button != keyItem.m_button && newKeys.count(button) == 0) { + EKeystroke type = kKeystrokeRelease; + if (i->second.m_lock) { + type = kKeystrokeUnmodify; + } + addKeystrokes(type, i->second, + activeModifiers, currentState, keystrokes); + } + } + + // press wanted keys + for (ModifierToKeys::const_iterator i = desiredModifiers.begin(); + i != desiredModifiers.end(); ++i) { + KeyButton button = i->second.m_button; + if (button != keyItem.m_button && oldKeys.count(button) == 0) { + EKeystroke type = kKeystrokePress; + if (i->second.m_lock) { + type = kKeystrokeModify; + } + addKeystrokes(type, i->second, + activeModifiers, currentState, keystrokes); + } + } + + return true; +} + +bool +CKeyMap::keysForModifierState(KeyButton button, SInt32 group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask requiredState, KeyModifierMask sensitiveMask, + KeyModifierMask notRequiredMask, + Keystrokes& keystrokes) const +{ + // compute which modifiers need changing + KeyModifierMask flipMask = ((currentState ^ requiredState) & sensitiveMask); + // if a modifier is not required then don't even try to match it. if + // we don't mask out notRequiredMask then we'll try to match those + // modifiers but succeed if we can't. however, this is known not + // to work if the key itself is a modifier (the numlock toggle can + // interfere) so we don't try to match at all. + flipMask &= ~notRequiredMask; + LOG((CLOG_DEBUG1 "flip: %04x (%04x vs %04x in %04x - %04x)", flipMask, currentState, requiredState, sensitiveMask & 0xffffu, notRequiredMask & 0xffffu)); + if (flipMask == 0) { + return true; + } + + // fix modifiers. this is complicated by the fact that a modifier may + // be sensitive to other modifiers! (who thought that up?) + // + // we'll assume that modifiers with higher bits are affected by modifiers + // with lower bits. there's not much basis for that assumption except + // that we're pretty sure shift isn't changed by other modifiers. + for (SInt32 bit = kKeyModifierNumBits; bit-- > 0; ) { + KeyModifierMask mask = (1u << bit); + if ((flipMask & mask) == 0) { + // modifier is already correct + continue; + } + + // do we want the modifier active or inactive? + bool active = ((requiredState & mask) != 0); + + // get the KeyItem for the modifier in the group + const KeyItem* keyItem = keyForModifier(button, group, bit); + if (keyItem == NULL) { + if ((mask & notRequiredMask) == 0) { + LOG((CLOG_DEBUG1 "no key for modifier %04x", mask)); + return false; + } + else { + continue; + } + } + + // if this modifier is sensitive to modifiers then adjust those + // modifiers. also check if our assumption was correct. note + // that we only need to adjust the modifiers on key down. + KeyModifierMask sensitive = keyItem->m_sensitive; + if ((sensitive & mask) != 0) { + // modifier is sensitive to itself. that makes no sense + // so ignore it. + LOG((CLOG_DEBUG1 "modifier %04x modified by itself", mask)); + sensitive &= ~mask; + } + if (sensitive != 0) { + if (sensitive > mask) { + // our assumption is incorrect + LOG((CLOG_DEBUG1 "modifier %04x modified by %04x", mask, sensitive)); + return false; + } + if (active && !keysForModifierState(button, group, + activeModifiers, currentState, + keyItem->m_required, sensitive, + notRequiredMask, keystrokes)) { + return false; + } + else if (!active) { + // release the modifier + // XXX -- this doesn't work! if Alt and Meta are mapped + // to one key and we want to release Meta we can't do + // that without also releasing Alt. + // need to think about support for modified modifiers. + } + } + + // current state should match required state + if ((currentState & sensitive) != (keyItem->m_required & sensitive)) { + LOG((CLOG_DEBUG1 "unable to match modifier state for modifier %04x (%04x vs %04x in %04x)", mask, currentState, keyItem->m_required, sensitive)); + return false; + } + + // add keystrokes + EKeystroke type = active ? kKeystrokeModify : kKeystrokeUnmodify; + addKeystrokes(type, *keyItem, activeModifiers, currentState, + keystrokes); + } + + return true; +} + +void +CKeyMap::addKeystrokes(EKeystroke type, const KeyItem& keyItem, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + Keystrokes& keystrokes) const +{ + KeyButton button = keyItem.m_button; + UInt32 data = keyItem.m_client; + switch (type) { + case kKeystrokePress: + keystrokes.push_back(Keystroke(button, true, false, data)); + if (keyItem.m_generates != 0) { + if (!keyItem.m_lock || (currentState & keyItem.m_generates) == 0) { + // add modifier key and activate modifier + activeModifiers.insert(std::make_pair( + keyItem.m_generates, keyItem)); + currentState |= keyItem.m_generates; + } + else { + // deactivate locking modifier + activeModifiers.erase(keyItem.m_generates); + currentState &= ~keyItem.m_generates; + } + } + break; + + case kKeystrokeRelease: + keystrokes.push_back(Keystroke(button, false, false, data)); + if (keyItem.m_generates != 0 && !keyItem.m_lock) { + // remove key from active modifiers + std::pair range = + activeModifiers.equal_range(keyItem.m_generates); + for (ModifierToKeys::iterator i = range.first; + i != range.second; ++i) { + if (i->second.m_button == button) { + activeModifiers.erase(i); + break; + } + } + + // if no more keys for this modifier then deactivate modifier + if (activeModifiers.count(keyItem.m_generates) == 0) { + currentState &= ~keyItem.m_generates; + } + } + break; + + case kKeystrokeRepeat: + keystrokes.push_back(Keystroke(button, false, true, data)); + keystrokes.push_back(Keystroke(button, true, true, data)); + // no modifier changes on key repeat + break; + + case kKeystrokeClick: + keystrokes.push_back(Keystroke(button, true, false, data)); + keystrokes.push_back(Keystroke(button, false, false, data)); + // no modifier changes on key click + break; + + case kKeystrokeModify: + case kKeystrokeUnmodify: + if (keyItem.m_lock) { + // we assume there's just one button for this modifier + if (m_halfDuplex.count(button) > 0) { + if (type == kKeystrokeModify) { + // turn half-duplex toggle on (press) + keystrokes.push_back(Keystroke(button, true, false, data)); + } + else { + // turn half-duplex toggle off (release) + keystrokes.push_back(Keystroke(button, false, false, data)); + } + } + else { + // toggle (click) + keystrokes.push_back(Keystroke(button, true, false, data)); + keystrokes.push_back(Keystroke(button, false, false, data)); + } + } + else if (type == kKeystrokeModify) { + // press modifier + keystrokes.push_back(Keystroke(button, true, false, data)); + } + else { + // release all the keys that generate the modifier that are + // currently down + std::pair range = + activeModifiers.equal_range(keyItem.m_generates); + for (ModifierToKeys::const_iterator i = range.first; + i != range.second; ++i) { + keystrokes.push_back(Keystroke(i->second.m_button, + false, false, i->second.m_client)); + } + } + + if (type == kKeystrokeModify) { + activeModifiers.insert(std::make_pair( + keyItem.m_generates, keyItem)); + currentState |= keyItem.m_generates; + } + else { + activeModifiers.erase(keyItem.m_generates); + currentState &= ~keyItem.m_generates; + } + break; + } +} + +SInt32 +CKeyMap::getNumModifiers(KeyModifierMask state) +{ + SInt32 n = 0; + for (; state != 0; state >>= 1) { + if ((state & 1) != 0) { + ++n; + } + } + return n; +} + +bool +CKeyMap::isDeadKey(KeyID key) +{ + return (key == kKeyCompose || (key >= 0x0300 && key <= 0x036f)); +} + +KeyID +CKeyMap::getDeadKey(KeyID key) +{ + if (isDeadKey(key)) { + // already dead + return key; + } + + switch (key) { + case '`': + return kKeyDeadGrave; + + case 0xb4u: + return kKeyDeadAcute; + + case '^': + case 0x2c6: + return kKeyDeadCircumflex; + + case '~': + case 0x2dcu: + return kKeyDeadTilde; + + case 0xafu: + return kKeyDeadMacron; + + case 0x2d8u: + return kKeyDeadBreve; + + case 0x2d9u: + return kKeyDeadAbovedot; + + case 0xa8u: + return kKeyDeadDiaeresis; + + case 0xb0u: + case 0x2dau: + return kKeyDeadAbovering; + + case '\"': + case 0x2ddu: + return kKeyDeadDoubleacute; + + case 0x2c7u: + return kKeyDeadCaron; + + case 0xb8u: + return kKeyDeadCedilla; + + case 0x2dbu: + return kKeyDeadOgonek; + + default: + // unknown + return kKeyNone; + } +} + +CString +CKeyMap::formatKey(KeyID key, KeyModifierMask mask) +{ + // initialize tables + initKeyNameMaps(); + + CString x; + for (SInt32 i = 0; i < kKeyModifierNumBits; ++i) { + KeyModifierMask mod = (1u << i); + if ((mask & mod) != 0 && s_modifierToNameMap->count(mod) > 0) { + x += s_modifierToNameMap->find(mod)->second; + x += "+"; + } + } + if (key != kKeyNone) { + if (s_keyToNameMap->count(key) > 0) { + x += s_keyToNameMap->find(key)->second; + } + // XXX -- we're assuming ASCII here + else if (key >= 33 && key < 127) { + x += (char)key; + } + else { + x += CStringUtil::print("\\u%04x", key); + } + } + else if (!x.empty()) { + // remove trailing '+' + x.erase(x.size() - 1); + } + return x; +} + +bool +CKeyMap::parseKey(const CString& x, KeyID& key) +{ + // initialize tables + initKeyNameMaps(); + + // parse the key + key = kKeyNone; + if (s_nameToKeyMap->count(x) > 0) { + key = s_nameToKeyMap->find(x)->second; + } + // XXX -- we're assuming ASCII encoding here + else if (x.size() == 1) { + if (!isgraph(x[0])) { + // unknown key + return false; + } + key = (KeyID)x[0]; + } + else if (x.size() == 6 && x[0] == '\\' && x[1] == 'u') { + // escaped unicode (\uXXXX where XXXX is a hex number) + char* end; + key = (KeyID)strtol(x.c_str() + 2, &end, 16); + if (*end != '\0') { + return false; + } + } + else if (!x.empty()) { + // unknown key + return false; + } + + return true; +} + +bool +CKeyMap::parseModifiers(CString& x, KeyModifierMask& mask) +{ + // initialize tables + initKeyNameMaps(); + + mask = 0; + CString::size_type tb = x.find_first_not_of(" \t", 0); + while (tb != CString::npos) { + // get next component + CString::size_type te = x.find_first_of(" \t+)", tb); + if (te == CString::npos) { + te = x.size(); + } + CString c = x.substr(tb, te - tb); + if (c.empty()) { + // missing component + return false; + } + + if (s_nameToModifierMap->count(c) > 0) { + KeyModifierMask mod = s_nameToModifierMap->find(c)->second; + if ((mask & mod) != 0) { + // modifier appears twice + return false; + } + mask |= mod; + } + else { + // unknown string + x.erase(0, tb); + CString::size_type tb = x.find_first_not_of(" \t"); + CString::size_type te = x.find_last_not_of(" \t"); + if (tb == CString::npos) { + x = ""; + } + else { + x = x.substr(tb, te - tb + 1); + } + return true; + } + + // check for '+' or end of string + tb = x.find_first_not_of(" \t", te); + if (tb != CString::npos) { + if (x[tb] != '+') { + // expected '+' + return false; + } + tb = x.find_first_not_of(" \t", tb + 1); + } + } + + // parsed the whole thing + x = ""; + return true; +} + +void +CKeyMap::initKeyNameMaps() +{ + // initialize tables + if (s_nameToKeyMap == NULL) { + s_nameToKeyMap = new CNameToKeyMap; + s_keyToNameMap = new CKeyToNameMap; + for (const KeyNameMapEntry* i = kKeyNameMap; i->m_name != NULL; ++i) { + (*s_nameToKeyMap)[i->m_name] = i->m_id; + (*s_keyToNameMap)[i->m_id] = i->m_name; + } + } + if (s_nameToModifierMap == NULL) { + s_nameToModifierMap = new CNameToModifierMap; + s_modifierToNameMap = new CModifierToNameMap; + for (const KeyModifierNameMapEntry* i = kModifierNameMap; + i->m_name != NULL; ++i) { + (*s_nameToModifierMap)[i->m_name] = i->m_mask; + (*s_modifierToNameMap)[i->m_mask] = i->m_name; + } + } +} + + +// +// CKeyMap::KeyItem +// + +bool +CKeyMap::KeyItem::operator==(const KeyItem& x) const +{ + return (m_id == x.m_id && + m_group == x.m_group && + m_button == x.m_button && + m_required == x.m_required && + m_sensitive == x.m_sensitive && + m_generates == x.m_generates && + m_dead == x.m_dead && + m_lock == x.m_lock && + m_client == x.m_client); +} + + +// +// CKeyMap::Keystroke +// + +CKeyMap::Keystroke::Keystroke(KeyButton button, + bool press, bool repeat, UInt32 data) : + m_type(kButton) +{ + m_data.m_button.m_button = button; + m_data.m_button.m_press = press; + m_data.m_button.m_repeat = repeat; + m_data.m_button.m_client = data; +} + +CKeyMap::Keystroke::Keystroke(SInt32 group, bool absolute, bool restore) : + m_type(kGroup) +{ + m_data.m_group.m_group = group; + m_data.m_group.m_absolute = absolute; + m_data.m_group.m_restore = restore; +} diff --git a/src/lib/synergy/CKeyMap.h b/src/lib/synergy/CKeyMap.h new file mode 100644 index 00000000..de5961fe --- /dev/null +++ b/src/lib/synergy/CKeyMap.h @@ -0,0 +1,494 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2005 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CKEYMAP_H +#define CKEYMAP_H + +#include "KeyTypes.h" +#include "CString.h" +#include "CStringUtil.h" +#include "stdmap.h" +#include "stdset.h" +#include "stdvector.h" + +//! Key map +/*! +This class provides a keyboard mapping. +*/ +class CKeyMap { +public: + CKeyMap(); + ~CKeyMap(); + + //! KeyID synthesis info + /*! + This structure contains the information necessary to synthesize a + keystroke that generates a KeyID (stored elsewhere). \c m_sensitive + lists the modifiers that the key is affected by and must therefore + be in the correct state, which is listed in \c m_required. If the + key is mapped to a modifier, that modifier is in \c m_generates and + is not in \c m_sensitive. + */ + struct KeyItem { + public: + KeyID m_id; //!< KeyID + SInt32 m_group; //!< Group for key + KeyButton m_button; //!< Button to generate KeyID + KeyModifierMask m_required; //!< Modifiers required for KeyID + KeyModifierMask m_sensitive; //!< Modifiers key is sensitive to + KeyModifierMask m_generates; //!< Modifiers key is mapped to + bool m_dead; //!< \c true if this is a dead KeyID + bool m_lock; //!< \c true if this locks a modifier + UInt32 m_client; //!< Client data + + public: + bool operator==(const KeyItem&) const; + }; + + //! The KeyButtons needed to synthesize a KeyID + /*! + An ordered list of \c KeyItems produces a particular KeyID. If + the KeyID can be synthesized directly then there is one entry in + the list. If dead keys are required then they're listed first. + A list is the minimal set of keystrokes necessary to synthesize + the KeyID, so it doesn't include no-ops. A list does not include + any modifier keys unless the KeyID is a modifier, in which case + it has exactly one KeyItem for the modifier itself. + */ + typedef std::vector KeyItemList; + + //! A keystroke + class Keystroke { + public: + enum EType { + kButton, //!< Synthesize button + kGroup //!< Set new group + }; + + Keystroke(KeyButton, bool press, bool repeat, UInt32 clientData); + Keystroke(SInt32 group, bool absolute, bool restore); + + public: + struct CButton { + public: + KeyButton m_button; //!< Button to synthesize + bool m_press; //!< \c true iff press + bool m_repeat; //!< \c true iff for an autorepeat + UInt32 m_client; //!< Client data + }; + struct CGroup { + public: + SInt32 m_group; //!< Group/offset to change to/by + bool m_absolute; //!< \c true iff change to, else by + bool m_restore; //!< \c true iff for restoring state + }; + union CData { + public: + CButton m_button; + CGroup m_group; + }; + + EType m_type; + CData m_data; + }; + + //! A sequence of keystrokes + typedef std::vector Keystrokes; + + //! A mapping of a modifier to keys for that modifier + typedef std::multimap ModifierToKeys; + + //! A set of buttons + typedef std::map ButtonToKeyMap; + + //! Callback type for \c foreachKey + typedef void (*ForeachKeyCallback)(KeyID, SInt32 group, + KeyItem&, void* userData); + + //! @name manipulators + //@{ + + //! Swap with another \c CKeyMap + virtual void swap(CKeyMap&); + + //! Add a key entry + /*! + Adds \p item to the entries for the item's id and group. The + \c m_dead member is set automatically. + */ + void addKeyEntry(const KeyItem& item); + + //! Add an alias key entry + /*! + If \p targetID with the modifiers given by \p targetRequired and + \p targetSensitive is not available in group \p group then find an + entry for \p sourceID with modifiers given by \p sourceRequired and + \p sourceSensitive in any group with exactly one item and, if found, + add a new item just like it except using id \p targetID. This + effectively makes the \p sourceID an alias for \p targetID (i.e. we + can generate \p targetID using \p sourceID). + */ + void addKeyAliasEntry(KeyID targetID, SInt32 group, + KeyModifierMask targetRequired, + KeyModifierMask targetSensitive, + KeyID sourceID, + KeyModifierMask sourceRequired, + KeyModifierMask sourceSensitive); + + //! Add a key sequence entry + /*! + Adds the sequence of keys \p keys (\p numKeys elements long) to + synthesize key \p id in group \p group. This looks up in the + map each key in \p keys. If all are found then each key is + converted to the button for that key and the buttons are added + as the entry for \p id. If \p id is already in the map or at + least one key in \p keys is not in the map then nothing is added + and this returns \c false, otherwise it returns \c true. + */ + bool addKeyCombinationEntry(KeyID id, SInt32 group, + const KeyID* keys, UInt32 numKeys); + + //! Enable composition across groups + /*! + If called then the keyboard map will allow switching between groups + during key composition. Not all systems allow that. + */ + void allowGroupSwitchDuringCompose(); + + //! Add a half-duplex button + /*! + Records that button \p button is a half-duplex key. This is called + when translating the system's keyboard map. It's independent of the + half-duplex modifier calls. + */ + void addHalfDuplexButton(KeyButton button); + + //! Remove all half-duplex modifiers + /*! + Removes all half-duplex modifiers. This is called to set user + configurable half-duplex settings. + */ + void clearHalfDuplexModifiers(); + + //! Add a half-duplex modifier + /*! + Records that modifier key \p key is half-duplex. This is called to + set user configurable half-duplex settings. + */ + virtual void addHalfDuplexModifier(KeyID key); + + //! Finish adding entries + /*! + Called after adding entries, this does some internal housekeeping. + */ + virtual void finish(); + + //! Iterate over all added keys items + /*! + Calls \p cb for every key item. + */ + virtual void foreachKey(ForeachKeyCallback cb, void* userData); + + //@} + //! @name accessors + //@{ + + //! Map key press/repeat to keystrokes. + /*! + Converts press/repeat of key \p id in group \p group with current + modifiers as given in \p currentState and the desired modifiers in + \p desiredMask into the keystrokes necessary to synthesize that key + event in \p keys. It returns the \c KeyItem of the key being + pressed/repeated, or NULL if the key cannot be mapped. + */ + virtual const KeyItem* mapKey(Keystrokes& keys, KeyID id, SInt32 group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask desiredMask, + bool isAutoRepeat) const; + + //! Get number of groups + /*! + Returns the number of keyboard groups (independent layouts) in the map. + */ + SInt32 getNumGroups() const; + + //! Compute a group number + /*! + Returns the number of the group \p offset groups after group \p group. + */ + SInt32 getEffectiveGroup(SInt32 group, SInt32 offset) const; + + //! Find key entry compatible with modifiers + /*! + Returns the \c KeyItemList for the first entry for \p id in group + \p group that is compatible with the given modifiers, or NULL + if there isn't one. A button list is compatible with a modifiers + if it is either insensitive to all modifiers in \p sensitive or + it requires the modifiers to be in the state indicated by \p required + for every modifier indicated by \p sensitive. + */ + const KeyItemList* findCompatibleKey(KeyID id, SInt32 group, + KeyModifierMask required, + KeyModifierMask sensitive) const; + + //! Test if modifier is half-duplex + /*! + Returns \c true iff modifier key \p key or button \p button is + half-duplex. + */ + virtual bool isHalfDuplex(KeyID key, KeyButton button) const; + + //! Test if modifiers indicate a command + /*! + Returns \c true iff the modifiers in \p mask contain any command + modifiers. A command modifier is used for keyboard shortcuts and + hotkeys, Rather than trying to synthesize a character, a command + is trying to synthesize a particular set of buttons. So it's not + important to match the shift or AltGr state to achieve a character + but it is important to match the modifier state exactly. + */ + bool isCommand(KeyModifierMask mask) const; + + // Get the modifiers that indicate a command + /*! + Returns the modifiers that when combined with other keys indicate + a command (e.g. shortcut or hotkey). + */ + KeyModifierMask getCommandModifiers() const; + + //! Get buttons from modifier map + /*! + Put all the keys in \p modifiers into \p keys. + */ + static void collectButtons(const ModifierToKeys& modifiers, + ButtonToKeyMap& keys); + + //! Set modifier key state + /*! + Sets the modifier key state (\c m_generates and \c m_lock) in \p item + based on the \c m_id in \p item. + */ + static void initModifierKey(KeyItem& item); + + //! Test for a dead key + /*! + Returns \c true if \p key is a dead key. + */ + static bool isDeadKey(KeyID key); + + //! Get corresponding dead key + /*! + Returns the dead key corresponding to \p key if one exists, otherwise + return \c kKeyNone. This returns \p key if it's already a dead key. + */ + static KeyID getDeadKey(KeyID key); + + //! Get string for a key and modifier mask + /*! + Converts a key and modifier mask into a string representing the + combination. + */ + static CString formatKey(KeyID key, KeyModifierMask); + + //! Parse a string into a key + /*! + Converts a string into a key. Returns \c true on success and \c false + if the string cannot be parsed. + */ + static bool parseKey(const CString&, KeyID&); + + //! Parse a string into a modifier mask + /*! + Converts a string into a modifier mask. Returns \c true on success + and \c false if the string cannot be parsed. The modifiers plus any + remaining leading and trailing whitespace is stripped from the input + string. + */ + static bool parseModifiers(CString&, KeyModifierMask&); + + //@} + +private: + //! Ways to synthesize a key + enum EKeystroke { + kKeystrokePress, //!< Synthesize a press + kKeystrokeRelease, //!< Synthesize a release + kKeystrokeRepeat, //!< Synthesize an autorepeat + kKeystrokeClick, //!< Synthesize a press and release + kKeystrokeModify, //!< Synthesize pressing a modifier + kKeystrokeUnmodify //!< Synthesize releasing a modifier + }; + + // A list of ways to synthesize a KeyID + typedef std::vector KeyEntryList; + + // computes the number of groups + SInt32 findNumGroups() const; + + // computes the map of modifiers to the keys that generate the modifiers + void setModifierKeys(); + + // maps a command key. a command key is a keyboard shortcut and we're + // trying to synthesize a button press with an exact sets of modifiers, + // not trying to synthesize a character. so we just need to find the + // right button and synthesize the requested modifiers without regard + // to what character they would synthesize. we disallow multikey + // entries since they don't make sense as hotkeys. + const KeyItem* mapCommandKey(Keystrokes& keys, + KeyID id, SInt32 group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask desiredMask, + bool isAutoRepeat) const; + + // maps a character key. a character key is trying to synthesize a + // particular KeyID and isn't entirely concerned with the modifiers + // used to do it. + const KeyItem* mapCharacterKey(Keystrokes& keys, + KeyID id, SInt32 group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask desiredMask, + bool isAutoRepeat) const; + + // maps a modifier key + const KeyItem* mapModifierKey(Keystrokes& keys, + KeyID id, SInt32 group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask desiredMask, + bool isAutoRepeat) const; + + // returns the index into \p entryList of the KeyItemList requiring + // the fewest modifier changes between \p currentState and + // \p desiredState. + SInt32 findBestKey(const KeyEntryList& entryList, + KeyModifierMask currentState, + KeyModifierMask desiredState) const; + + // gets the \c KeyItem used to synthesize the modifier who's bit is + // given by \p modifierBit in group \p group and does not synthesize + // the key \p button. + const KeyItem* keyForModifier(KeyButton button, SInt32 group, + SInt32 modifierBit) const; + + // fills \p keystrokes with the keys to synthesize the key in + // \p keyItem taking the modifiers into account. returns \c true + // iff successful and sets \p currentState to the + // resulting modifier state. + bool keysForKeyItem(const KeyItem& keyItem, + SInt32& group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask desiredState, + KeyModifierMask overrideModifiers, + bool isAutoRepeat, + Keystrokes& keystrokes) const; + + // fills \p keystrokes with the keys to synthesize the modifiers + // in \p desiredModifiers from the active modifiers listed in + // \p activeModifiers not including the key in \p keyItem. + // returns \c true iff successful. + bool keysToRestoreModifiers(const KeyItem& keyItem, + SInt32 group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + const ModifierToKeys& desiredModifiers, + Keystrokes& keystrokes) const; + + // fills \p keystrokes and \p undo with the keys to change the + // current modifier state in \p currentState to match the state in + // \p requiredState for each modifier indicated in \p sensitiveMask. + // returns \c true iff successful and sets \p currentState to the + // resulting modifier state. + bool keysForModifierState(KeyButton button, SInt32 group, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask requiredState, + KeyModifierMask sensitiveMask, + KeyModifierMask notRequiredMask, + Keystrokes& keystrokes) const; + + // Adds keystrokes to synthesize key \p keyItem in mode \p type to + // \p keystrokes and to undo the synthesis to \p undo. + void addKeystrokes(EKeystroke type, + const KeyItem& keyItem, + ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + Keystrokes& keystrokes) const; + + // Returns the number of modifiers indicated in \p state. + static SInt32 getNumModifiers(KeyModifierMask state); + + // Initialize key name/id maps + static void initKeyNameMaps(); + + // not implemented + CKeyMap(const CKeyMap&); + CKeyMap& operator=(const CKeyMap&); + +private: + // Ways to synthesize a KeyID over multiple keyboard groups + typedef std::vector KeyGroupTable; + + // Table of KeyID to ways to synthesize that KeyID + typedef std::map KeyIDMap; + + // List of KeyItems that generate a particular modifier + typedef std::vector ModifierKeyItemList; + + // Map a modifier to the KeyItems that synthesize that modifier + typedef std::vector ModifierToKeyTable; + + // A set of keys + typedef std::set KeySet; + + // A set of buttons + typedef std::set KeyButtonSet; + + // Key maps for parsing/formatting + typedef std::map CNameToKeyMap; + typedef std::map CNameToModifierMap; + typedef std::map CKeyToNameMap; + typedef std::map CModifierToNameMap; + + // KeyID info + KeyIDMap m_keyIDMap; + SInt32 m_numGroups; + ModifierToKeyTable m_modifierKeys; + + // composition info + bool m_composeAcrossGroups; + + // half-duplex info + KeyButtonSet m_halfDuplex; // half-duplex set by synergy + KeySet m_halfDuplexMods; // half-duplex set by user + + // dummy KeyItem for changing modifiers + KeyItem m_modifierKeyItem; + + // parsing/formatting tables + static CNameToKeyMap* s_nameToKeyMap; + static CNameToModifierMap* s_nameToModifierMap; + static CKeyToNameMap* s_keyToNameMap; + static CModifierToNameMap* s_modifierToNameMap; +}; + +#endif diff --git a/src/lib/synergy/CKeyState.cpp b/src/lib/synergy/CKeyState.cpp new file mode 100644 index 00000000..d56f6aaa --- /dev/null +++ b/src/lib/synergy/CKeyState.cpp @@ -0,0 +1,915 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CKeyState.h" +#include "CLog.h" +#include +#include +#include +#include + +static const KeyButton kButtonMask = (KeyButton)(IKeyState::kNumButtons - 1); + +static const KeyID s_decomposeTable[] = { + // spacing version of dead keys + 0x0060, 0x0300, 0x0020, 0, // grave, dead_grave, space + 0x00b4, 0x0301, 0x0020, 0, // acute, dead_acute, space + 0x005e, 0x0302, 0x0020, 0, // asciicircum, dead_circumflex, space + 0x007e, 0x0303, 0x0020, 0, // asciitilde, dead_tilde, space + 0x00a8, 0x0308, 0x0020, 0, // diaeresis, dead_diaeresis, space + 0x00b0, 0x030a, 0x0020, 0, // degree, dead_abovering, space + 0x00b8, 0x0327, 0x0020, 0, // cedilla, dead_cedilla, space + 0x02db, 0x0328, 0x0020, 0, // ogonek, dead_ogonek, space + 0x02c7, 0x030c, 0x0020, 0, // caron, dead_caron, space + 0x02d9, 0x0307, 0x0020, 0, // abovedot, dead_abovedot, space + 0x02dd, 0x030b, 0x0020, 0, // doubleacute, dead_doubleacute, space + 0x02d8, 0x0306, 0x0020, 0, // breve, dead_breve, space + 0x00af, 0x0304, 0x0020, 0, // macron, dead_macron, space + + // Latin-1 (ISO 8859-1) + 0x00c0, 0x0300, 0x0041, 0, // Agrave, dead_grave, A + 0x00c1, 0x0301, 0x0041, 0, // Aacute, dead_acute, A + 0x00c2, 0x0302, 0x0041, 0, // Acircumflex, dead_circumflex, A + 0x00c3, 0x0303, 0x0041, 0, // Atilde, dead_tilde, A + 0x00c4, 0x0308, 0x0041, 0, // Adiaeresis, dead_diaeresis, A + 0x00c5, 0x030a, 0x0041, 0, // Aring, dead_abovering, A + 0x00c7, 0x0327, 0x0043, 0, // Ccedilla, dead_cedilla, C + 0x00c8, 0x0300, 0x0045, 0, // Egrave, dead_grave, E + 0x00c9, 0x0301, 0x0045, 0, // Eacute, dead_acute, E + 0x00ca, 0x0302, 0x0045, 0, // Ecircumflex, dead_circumflex, E + 0x00cb, 0x0308, 0x0045, 0, // Ediaeresis, dead_diaeresis, E + 0x00cc, 0x0300, 0x0049, 0, // Igrave, dead_grave, I + 0x00cd, 0x0301, 0x0049, 0, // Iacute, dead_acute, I + 0x00ce, 0x0302, 0x0049, 0, // Icircumflex, dead_circumflex, I + 0x00cf, 0x0308, 0x0049, 0, // Idiaeresis, dead_diaeresis, I + 0x00d1, 0x0303, 0x004e, 0, // Ntilde, dead_tilde, N + 0x00d2, 0x0300, 0x004f, 0, // Ograve, dead_grave, O + 0x00d3, 0x0301, 0x004f, 0, // Oacute, dead_acute, O + 0x00d4, 0x0302, 0x004f, 0, // Ocircumflex, dead_circumflex, O + 0x00d5, 0x0303, 0x004f, 0, // Otilde, dead_tilde, O + 0x00d6, 0x0308, 0x004f, 0, // Odiaeresis, dead_diaeresis, O + 0x00d9, 0x0300, 0x0055, 0, // Ugrave, dead_grave, U + 0x00da, 0x0301, 0x0055, 0, // Uacute, dead_acute, U + 0x00db, 0x0302, 0x0055, 0, // Ucircumflex, dead_circumflex, U + 0x00dc, 0x0308, 0x0055, 0, // Udiaeresis, dead_diaeresis, U + 0x00dd, 0x0301, 0x0059, 0, // Yacute, dead_acute, Y + 0x00e0, 0x0300, 0x0061, 0, // agrave, dead_grave, a + 0x00e1, 0x0301, 0x0061, 0, // aacute, dead_acute, a + 0x00e2, 0x0302, 0x0061, 0, // acircumflex, dead_circumflex, a + 0x00e3, 0x0303, 0x0061, 0, // atilde, dead_tilde, a + 0x00e4, 0x0308, 0x0061, 0, // adiaeresis, dead_diaeresis, a + 0x00e5, 0x030a, 0x0061, 0, // aring, dead_abovering, a + 0x00e7, 0x0327, 0x0063, 0, // ccedilla, dead_cedilla, c + 0x00e8, 0x0300, 0x0065, 0, // egrave, dead_grave, e + 0x00e9, 0x0301, 0x0065, 0, // eacute, dead_acute, e + 0x00ea, 0x0302, 0x0065, 0, // ecircumflex, dead_circumflex, e + 0x00eb, 0x0308, 0x0065, 0, // ediaeresis, dead_diaeresis, e + 0x00ec, 0x0300, 0x0069, 0, // igrave, dead_grave, i + 0x00ed, 0x0301, 0x0069, 0, // iacute, dead_acute, i + 0x00ee, 0x0302, 0x0069, 0, // icircumflex, dead_circumflex, i + 0x00ef, 0x0308, 0x0069, 0, // idiaeresis, dead_diaeresis, i + 0x00f1, 0x0303, 0x006e, 0, // ntilde, dead_tilde, n + 0x00f2, 0x0300, 0x006f, 0, // ograve, dead_grave, o + 0x00f3, 0x0301, 0x006f, 0, // oacute, dead_acute, o + 0x00f4, 0x0302, 0x006f, 0, // ocircumflex, dead_circumflex, o + 0x00f5, 0x0303, 0x006f, 0, // otilde, dead_tilde, o + 0x00f6, 0x0308, 0x006f, 0, // odiaeresis, dead_diaeresis, o + 0x00f9, 0x0300, 0x0075, 0, // ugrave, dead_grave, u + 0x00fa, 0x0301, 0x0075, 0, // uacute, dead_acute, u + 0x00fb, 0x0302, 0x0075, 0, // ucircumflex, dead_circumflex, u + 0x00fc, 0x0308, 0x0075, 0, // udiaeresis, dead_diaeresis, u + 0x00fd, 0x0301, 0x0079, 0, // yacute, dead_acute, y + 0x00ff, 0x0308, 0x0079, 0, // ydiaeresis, dead_diaeresis, y + + // Latin-2 (ISO 8859-2) + 0x0104, 0x0328, 0x0041, 0, // Aogonek, dead_ogonek, A + 0x013d, 0x030c, 0x004c, 0, // Lcaron, dead_caron, L + 0x015a, 0x0301, 0x0053, 0, // Sacute, dead_acute, S + 0x0160, 0x030c, 0x0053, 0, // Scaron, dead_caron, S + 0x015e, 0x0327, 0x0053, 0, // Scedilla, dead_cedilla, S + 0x0164, 0x030c, 0x0054, 0, // Tcaron, dead_caron, T + 0x0179, 0x0301, 0x005a, 0, // Zacute, dead_acute, Z + 0x017d, 0x030c, 0x005a, 0, // Zcaron, dead_caron, Z + 0x017b, 0x0307, 0x005a, 0, // Zabovedot, dead_abovedot, Z + 0x0105, 0x0328, 0x0061, 0, // aogonek, dead_ogonek, a + 0x013e, 0x030c, 0x006c, 0, // lcaron, dead_caron, l + 0x015b, 0x0301, 0x0073, 0, // sacute, dead_acute, s + 0x0161, 0x030c, 0x0073, 0, // scaron, dead_caron, s + 0x015f, 0x0327, 0x0073, 0, // scedilla, dead_cedilla, s + 0x0165, 0x030c, 0x0074, 0, // tcaron, dead_caron, t + 0x017a, 0x0301, 0x007a, 0, // zacute, dead_acute, z + 0x017e, 0x030c, 0x007a, 0, // zcaron, dead_caron, z + 0x017c, 0x0307, 0x007a, 0, // zabovedot, dead_abovedot, z + 0x0154, 0x0301, 0x0052, 0, // Racute, dead_acute, R + 0x0102, 0x0306, 0x0041, 0, // Abreve, dead_breve, A + 0x0139, 0x0301, 0x004c, 0, // Lacute, dead_acute, L + 0x0106, 0x0301, 0x0043, 0, // Cacute, dead_acute, C + 0x010c, 0x030c, 0x0043, 0, // Ccaron, dead_caron, C + 0x0118, 0x0328, 0x0045, 0, // Eogonek, dead_ogonek, E + 0x011a, 0x030c, 0x0045, 0, // Ecaron, dead_caron, E + 0x010e, 0x030c, 0x0044, 0, // Dcaron, dead_caron, D + 0x0143, 0x0301, 0x004e, 0, // Nacute, dead_acute, N + 0x0147, 0x030c, 0x004e, 0, // Ncaron, dead_caron, N + 0x0150, 0x030b, 0x004f, 0, // Odoubleacute, dead_doubleacute, O + 0x0158, 0x030c, 0x0052, 0, // Rcaron, dead_caron, R + 0x016e, 0x030a, 0x0055, 0, // Uring, dead_abovering, U + 0x0170, 0x030b, 0x0055, 0, // Udoubleacute, dead_doubleacute, U + 0x0162, 0x0327, 0x0054, 0, // Tcedilla, dead_cedilla, T + 0x0155, 0x0301, 0x0072, 0, // racute, dead_acute, r + 0x0103, 0x0306, 0x0061, 0, // abreve, dead_breve, a + 0x013a, 0x0301, 0x006c, 0, // lacute, dead_acute, l + 0x0107, 0x0301, 0x0063, 0, // cacute, dead_acute, c + 0x010d, 0x030c, 0x0063, 0, // ccaron, dead_caron, c + 0x0119, 0x0328, 0x0065, 0, // eogonek, dead_ogonek, e + 0x011b, 0x030c, 0x0065, 0, // ecaron, dead_caron, e + 0x010f, 0x030c, 0x0064, 0, // dcaron, dead_caron, d + 0x0144, 0x0301, 0x006e, 0, // nacute, dead_acute, n + 0x0148, 0x030c, 0x006e, 0, // ncaron, dead_caron, n + 0x0151, 0x030b, 0x006f, 0, // odoubleacute, dead_doubleacute, o + 0x0159, 0x030c, 0x0072, 0, // rcaron, dead_caron, r + 0x016f, 0x030a, 0x0075, 0, // uring, dead_abovering, u + 0x0171, 0x030b, 0x0075, 0, // udoubleacute, dead_doubleacute, u + 0x0163, 0x0327, 0x0074, 0, // tcedilla, dead_cedilla, t + + // Latin-3 (ISO 8859-3) + 0x0124, 0x0302, 0x0048, 0, // Hcircumflex, dead_circumflex, H + 0x0130, 0x0307, 0x0049, 0, // Iabovedot, dead_abovedot, I + 0x011e, 0x0306, 0x0047, 0, // Gbreve, dead_breve, G + 0x0134, 0x0302, 0x004a, 0, // Jcircumflex, dead_circumflex, J + 0x0125, 0x0302, 0x0068, 0, // hcircumflex, dead_circumflex, h + 0x011f, 0x0306, 0x0067, 0, // gbreve, dead_breve, g + 0x0135, 0x0302, 0x006a, 0, // jcircumflex, dead_circumflex, j + 0x010a, 0x0307, 0x0043, 0, // Cabovedot, dead_abovedot, C + 0x0108, 0x0302, 0x0043, 0, // Ccircumflex, dead_circumflex, C + 0x0120, 0x0307, 0x0047, 0, // Gabovedot, dead_abovedot, G + 0x011c, 0x0302, 0x0047, 0, // Gcircumflex, dead_circumflex, G + 0x016c, 0x0306, 0x0055, 0, // Ubreve, dead_breve, U + 0x015c, 0x0302, 0x0053, 0, // Scircumflex, dead_circumflex, S + 0x010b, 0x0307, 0x0063, 0, // cabovedot, dead_abovedot, c + 0x0109, 0x0302, 0x0063, 0, // ccircumflex, dead_circumflex, c + 0x0121, 0x0307, 0x0067, 0, // gabovedot, dead_abovedot, g + 0x011d, 0x0302, 0x0067, 0, // gcircumflex, dead_circumflex, g + 0x016d, 0x0306, 0x0075, 0, // ubreve, dead_breve, u + 0x015d, 0x0302, 0x0073, 0, // scircumflex, dead_circumflex, s + + // Latin-4 (ISO 8859-4) + 0x0156, 0x0327, 0x0052, 0, // Rcedilla, dead_cedilla, R + 0x0128, 0x0303, 0x0049, 0, // Itilde, dead_tilde, I + 0x013b, 0x0327, 0x004c, 0, // Lcedilla, dead_cedilla, L + 0x0112, 0x0304, 0x0045, 0, // Emacron, dead_macron, E + 0x0122, 0x0327, 0x0047, 0, // Gcedilla, dead_cedilla, G + 0x0157, 0x0327, 0x0072, 0, // rcedilla, dead_cedilla, r + 0x0129, 0x0303, 0x0069, 0, // itilde, dead_tilde, i + 0x013c, 0x0327, 0x006c, 0, // lcedilla, dead_cedilla, l + 0x0113, 0x0304, 0x0065, 0, // emacron, dead_macron, e + 0x0123, 0x0327, 0x0067, 0, // gcedilla, dead_cedilla, g + 0x0100, 0x0304, 0x0041, 0, // Amacron, dead_macron, A + 0x012e, 0x0328, 0x0049, 0, // Iogonek, dead_ogonek, I + 0x0116, 0x0307, 0x0045, 0, // Eabovedot, dead_abovedot, E + 0x012a, 0x0304, 0x0049, 0, // Imacron, dead_macron, I + 0x0145, 0x0327, 0x004e, 0, // Ncedilla, dead_cedilla, N + 0x014c, 0x0304, 0x004f, 0, // Omacron, dead_macron, O + 0x0136, 0x0327, 0x004b, 0, // Kcedilla, dead_cedilla, K + 0x0172, 0x0328, 0x0055, 0, // Uogonek, dead_ogonek, U + 0x0168, 0x0303, 0x0055, 0, // Utilde, dead_tilde, U + 0x016a, 0x0304, 0x0055, 0, // Umacron, dead_macron, U + 0x0101, 0x0304, 0x0061, 0, // amacron, dead_macron, a + 0x012f, 0x0328, 0x0069, 0, // iogonek, dead_ogonek, i + 0x0117, 0x0307, 0x0065, 0, // eabovedot, dead_abovedot, e + 0x012b, 0x0304, 0x0069, 0, // imacron, dead_macron, i + 0x0146, 0x0327, 0x006e, 0, // ncedilla, dead_cedilla, n + 0x014d, 0x0304, 0x006f, 0, // omacron, dead_macron, o + 0x0137, 0x0327, 0x006b, 0, // kcedilla, dead_cedilla, k + 0x0173, 0x0328, 0x0075, 0, // uogonek, dead_ogonek, u + 0x0169, 0x0303, 0x0075, 0, // utilde, dead_tilde, u + 0x016b, 0x0304, 0x0075, 0, // umacron, dead_macron, u + + // Latin-8 (ISO 8859-14) + 0x1e02, 0x0307, 0x0042, 0, // Babovedot, dead_abovedot, B + 0x1e03, 0x0307, 0x0062, 0, // babovedot, dead_abovedot, b + 0x1e0a, 0x0307, 0x0044, 0, // Dabovedot, dead_abovedot, D + 0x1e80, 0x0300, 0x0057, 0, // Wgrave, dead_grave, W + 0x1e82, 0x0301, 0x0057, 0, // Wacute, dead_acute, W + 0x1e0b, 0x0307, 0x0064, 0, // dabovedot, dead_abovedot, d + 0x1ef2, 0x0300, 0x0059, 0, // Ygrave, dead_grave, Y + 0x1e1e, 0x0307, 0x0046, 0, // Fabovedot, dead_abovedot, F + 0x1e1f, 0x0307, 0x0066, 0, // fabovedot, dead_abovedot, f + 0x1e40, 0x0307, 0x004d, 0, // Mabovedot, dead_abovedot, M + 0x1e41, 0x0307, 0x006d, 0, // mabovedot, dead_abovedot, m + 0x1e56, 0x0307, 0x0050, 0, // Pabovedot, dead_abovedot, P + 0x1e81, 0x0300, 0x0077, 0, // wgrave, dead_grave, w + 0x1e57, 0x0307, 0x0070, 0, // pabovedot, dead_abovedot, p + 0x1e83, 0x0301, 0x0077, 0, // wacute, dead_acute, w + 0x1e60, 0x0307, 0x0053, 0, // Sabovedot, dead_abovedot, S + 0x1ef3, 0x0300, 0x0079, 0, // ygrave, dead_grave, y + 0x1e84, 0x0308, 0x0057, 0, // Wdiaeresis, dead_diaeresis, W + 0x1e85, 0x0308, 0x0077, 0, // wdiaeresis, dead_diaeresis, w + 0x1e61, 0x0307, 0x0073, 0, // sabovedot, dead_abovedot, s + 0x0174, 0x0302, 0x0057, 0, // Wcircumflex, dead_circumflex, W + 0x1e6a, 0x0307, 0x0054, 0, // Tabovedot, dead_abovedot, T + 0x0176, 0x0302, 0x0059, 0, // Ycircumflex, dead_circumflex, Y + 0x0175, 0x0302, 0x0077, 0, // wcircumflex, dead_circumflex, w + 0x1e6b, 0x0307, 0x0074, 0, // tabovedot, dead_abovedot, t + 0x0177, 0x0302, 0x0079, 0, // ycircumflex, dead_circumflex, y + + // Latin-9 (ISO 8859-15) + 0x0178, 0x0308, 0x0059, 0, // Ydiaeresis, dead_diaeresis, Y + + // Compose key sequences + 0x00c6, kKeyCompose, 0x0041, 0x0045, 0, // AE, A, E + 0x00c1, kKeyCompose, 0x0041, 0x0027, 0, // Aacute, A, apostrophe + 0x00c2, kKeyCompose, 0x0041, 0x0053, 0, // Acircumflex, A, asciicircum + 0x00c3, kKeyCompose, 0x0041, 0x0022, 0, // Adiaeresis, A, quotedbl + 0x00c0, kKeyCompose, 0x0041, 0x0060, 0, // Agrave, A, grave + 0x00c5, kKeyCompose, 0x0041, 0x002a, 0, // Aring, A, asterisk + 0x00c3, kKeyCompose, 0x0041, 0x007e, 0, // Atilde, A, asciitilde + 0x00c7, kKeyCompose, 0x0043, 0x002c, 0, // Ccedilla, C, comma + 0x00d0, kKeyCompose, 0x0044, 0x002d, 0, // ETH, D, minus + 0x00c9, kKeyCompose, 0x0045, 0x0027, 0, // Eacute, E, apostrophe + 0x00ca, kKeyCompose, 0x0045, 0x0053, 0, // Ecircumflex, E, asciicircum + 0x00cb, kKeyCompose, 0x0045, 0x0022, 0, // Ediaeresis, E, quotedbl + 0x00c8, kKeyCompose, 0x0045, 0x0060, 0, // Egrave, E, grave + 0x00cd, kKeyCompose, 0x0049, 0x0027, 0, // Iacute, I, apostrophe + 0x00ce, kKeyCompose, 0x0049, 0x0053, 0, // Icircumflex, I, asciicircum + 0x00cf, kKeyCompose, 0x0049, 0x0022, 0, // Idiaeresis, I, quotedbl + 0x00cc, kKeyCompose, 0x0049, 0x0060, 0, // Igrave, I, grave + 0x00d1, kKeyCompose, 0x004e, 0x007e, 0, // Ntilde, N, asciitilde + 0x00d3, kKeyCompose, 0x004f, 0x0027, 0, // Oacute, O, apostrophe + 0x00d4, kKeyCompose, 0x004f, 0x0053, 0, // Ocircumflex, O, asciicircum + 0x00d6, kKeyCompose, 0x004f, 0x0022, 0, // Odiaeresis, O, quotedbl + 0x00d2, kKeyCompose, 0x004f, 0x0060, 0, // Ograve, O, grave + 0x00d8, kKeyCompose, 0x004f, 0x002f, 0, // Ooblique, O, slash + 0x00d5, kKeyCompose, 0x004f, 0x007e, 0, // Otilde, O, asciitilde + 0x00de, kKeyCompose, 0x0054, 0x0048, 0, // THORN, T, H + 0x00da, kKeyCompose, 0x0055, 0x0027, 0, // Uacute, U, apostrophe + 0x00db, kKeyCompose, 0x0055, 0x0053, 0, // Ucircumflex, U, asciicircum + 0x00dc, kKeyCompose, 0x0055, 0x0022, 0, // Udiaeresis, U, quotedbl + 0x00d9, kKeyCompose, 0x0055, 0x0060, 0, // Ugrave, U, grave + 0x00dd, kKeyCompose, 0x0059, 0x0027, 0, // Yacute, Y, apostrophe + 0x00e1, kKeyCompose, 0x0061, 0x0027, 0, // aacute, a, apostrophe + 0x00e2, kKeyCompose, 0x0061, 0x0053, 0, // acircumflex, a, asciicircum + 0x00b4, kKeyCompose, 0x0027, 0x0027, 0, // acute, apostrophe, apostrophe + 0x00e4, kKeyCompose, 0x0061, 0x0022, 0, // adiaeresis, a, quotedbl + 0x00e6, kKeyCompose, 0x0061, 0x0065, 0, // ae, a, e + 0x00e0, kKeyCompose, 0x0061, 0x0060, 0, // agrave, a, grave + 0x00e5, kKeyCompose, 0x0061, 0x002a, 0, // aring, a, asterisk + 0x0040, kKeyCompose, 0x0041, 0x0054, 0, // at, A, T + 0x00e3, kKeyCompose, 0x0061, 0x007e, 0, // atilde, a, asciitilde + 0x005c, kKeyCompose, 0x002f, 0x002f, 0, // backslash, slash, slash + 0x007c, kKeyCompose, 0x004c, 0x0056, 0, // bar, L, V + 0x007b, kKeyCompose, 0x0028, 0x002d, 0, // braceleft, parenleft, minus + 0x007d, kKeyCompose, 0x0029, 0x002d, 0, // braceright, parenright, minus + 0x005b, kKeyCompose, 0x0028, 0x0028, 0, // bracketleft, parenleft, parenleft + 0x005d, kKeyCompose, 0x0029, 0x0029, 0, // bracketright, parenright, parenright + 0x00a6, kKeyCompose, 0x0042, 0x0056, 0, // brokenbar, B, V + 0x00e7, kKeyCompose, 0x0063, 0x002c, 0, // ccedilla, c, comma + 0x00b8, kKeyCompose, 0x002c, 0x002c, 0, // cedilla, comma, comma + 0x00a2, kKeyCompose, 0x0063, 0x002f, 0, // cent, c, slash + 0x00a9, kKeyCompose, 0x0028, 0x0063, 0, // copyright, parenleft, c + 0x00a4, kKeyCompose, 0x006f, 0x0078, 0, // currency, o, x + 0x00b0, kKeyCompose, 0x0030, 0x0053, 0, // degree, 0, asciicircum + 0x00a8, kKeyCompose, 0x0022, 0x0022, 0, // diaeresis, quotedbl, quotedbl + 0x00f7, kKeyCompose, 0x003a, 0x002d, 0, // division, colon, minus + 0x00e9, kKeyCompose, 0x0065, 0x0027, 0, // eacute, e, apostrophe + 0x00ea, kKeyCompose, 0x0065, 0x0053, 0, // ecircumflex, e, asciicircum + 0x00eb, kKeyCompose, 0x0065, 0x0022, 0, // ediaeresis, e, quotedbl + 0x00e8, kKeyCompose, 0x0065, 0x0060, 0, // egrave, e, grave + 0x00f0, kKeyCompose, 0x0064, 0x002d, 0, // eth, d, minus + 0x00a1, kKeyCompose, 0x0021, 0x0021, 0, // exclamdown, exclam, exclam + 0x00ab, kKeyCompose, 0x003c, 0x003c, 0, // guillemotleft, less, less + 0x00bb, kKeyCompose, 0x003e, 0x003e, 0, // guillemotright, greater, greater + 0x0023, kKeyCompose, 0x002b, 0x002b, 0, // numbersign, plus, plus + 0x00ad, kKeyCompose, 0x002d, 0x002d, 0, // hyphen, minus, minus + 0x00ed, kKeyCompose, 0x0069, 0x0027, 0, // iacute, i, apostrophe + 0x00ee, kKeyCompose, 0x0069, 0x0053, 0, // icircumflex, i, asciicircum + 0x00ef, kKeyCompose, 0x0069, 0x0022, 0, // idiaeresis, i, quotedbl + 0x00ec, kKeyCompose, 0x0069, 0x0060, 0, // igrave, i, grave + 0x00af, kKeyCompose, 0x002d, 0x0053, 0, // macron, minus, asciicircum + 0x00ba, kKeyCompose, 0x006f, 0x005f, 0, // masculine, o, underscore + 0x00b5, kKeyCompose, 0x0075, 0x002f, 0, // mu, u, slash + 0x00d7, kKeyCompose, 0x0078, 0x0078, 0, // multiply, x, x + 0x00a0, kKeyCompose, 0x0020, 0x0020, 0, // nobreakspace, space, space + 0x00ac, kKeyCompose, 0x002c, 0x002d, 0, // notsign, comma, minus + 0x00f1, kKeyCompose, 0x006e, 0x007e, 0, // ntilde, n, asciitilde + 0x00f3, kKeyCompose, 0x006f, 0x0027, 0, // oacute, o, apostrophe + 0x00f4, kKeyCompose, 0x006f, 0x0053, 0, // ocircumflex, o, asciicircum + 0x00f6, kKeyCompose, 0x006f, 0x0022, 0, // odiaeresis, o, quotedbl + 0x00f2, kKeyCompose, 0x006f, 0x0060, 0, // ograve, o, grave + 0x00bd, kKeyCompose, 0x0031, 0x0032, 0, // onehalf, 1, 2 + 0x00bc, kKeyCompose, 0x0031, 0x0034, 0, // onequarter, 1, 4 + 0x00b9, kKeyCompose, 0x0031, 0x0053, 0, // onesuperior, 1, asciicircum + 0x00aa, kKeyCompose, 0x0061, 0x005f, 0, // ordfeminine, a, underscore + 0x00f8, kKeyCompose, 0x006f, 0x002f, 0, // oslash, o, slash + 0x00f5, kKeyCompose, 0x006f, 0x007e, 0, // otilde, o, asciitilde + 0x00b6, kKeyCompose, 0x0070, 0x0021, 0, // paragraph, p, exclam + 0x00b7, kKeyCompose, 0x002e, 0x002e, 0, // periodcentered, period, period + 0x00b1, kKeyCompose, 0x002b, 0x002d, 0, // plusminus, plus, minus + 0x00bf, kKeyCompose, 0x003f, 0x003f, 0, // questiondown, question, question + 0x00ae, kKeyCompose, 0x0028, 0x0072, 0, // registered, parenleft, r + 0x00a7, kKeyCompose, 0x0073, 0x006f, 0, // section, s, o + 0x00df, kKeyCompose, 0x0073, 0x0073, 0, // ssharp, s, s + 0x00a3, kKeyCompose, 0x004c, 0x002d, 0, // sterling, L, minus + 0x00fe, kKeyCompose, 0x0074, 0x0068, 0, // thorn, t, h + 0x00be, kKeyCompose, 0x0033, 0x0034, 0, // threequarters, 3, 4 + 0x00b3, kKeyCompose, 0x0033, 0x0053, 0, // threesuperior, 3, asciicircum + 0x00b2, kKeyCompose, 0x0032, 0x0053, 0, // twosuperior, 2, asciicircum + 0x00fa, kKeyCompose, 0x0075, 0x0027, 0, // uacute, u, apostrophe + 0x00fb, kKeyCompose, 0x0075, 0x0053, 0, // ucircumflex, u, asciicircum + 0x00fc, kKeyCompose, 0x0075, 0x0022, 0, // udiaeresis, u, quotedbl + 0x00f9, kKeyCompose, 0x0075, 0x0060, 0, // ugrave, u, grave + 0x00fd, kKeyCompose, 0x0079, 0x0027, 0, // yacute, y, apostrophe + 0x00ff, kKeyCompose, 0x0079, 0x0022, 0, // ydiaeresis, y, quotedbl + 0x00a5, kKeyCompose, 0x0079, 0x003d, 0, // yen, y, equal + + // end of table + 0 +}; + +static const KeyID s_numpadTable[] = { + kKeyKP_Space, 0x0020, + kKeyKP_Tab, kKeyTab, + kKeyKP_Enter, kKeyReturn, + kKeyKP_F1, kKeyF1, + kKeyKP_F2, kKeyF2, + kKeyKP_F3, kKeyF3, + kKeyKP_F4, kKeyF4, + kKeyKP_Home, kKeyHome, + kKeyKP_Left, kKeyLeft, + kKeyKP_Up, kKeyUp, + kKeyKP_Right, kKeyRight, + kKeyKP_Down, kKeyDown, + kKeyKP_PageUp, kKeyPageUp, + kKeyKP_PageDown, kKeyPageDown, + kKeyKP_End, kKeyEnd, + kKeyKP_Begin, kKeyBegin, + kKeyKP_Insert, kKeyInsert, + kKeyKP_Delete, kKeyDelete, + kKeyKP_Equal, 0x003d, + kKeyKP_Multiply, 0x002a, + kKeyKP_Add, 0x002b, + kKeyKP_Separator, 0x002c, + kKeyKP_Subtract, 0x002d, + kKeyKP_Decimal, 0x002e, + kKeyKP_Divide, 0x002f, + kKeyKP_0, 0x0030, + kKeyKP_1, 0x0031, + kKeyKP_2, 0x0032, + kKeyKP_3, 0x0033, + kKeyKP_4, 0x0034, + kKeyKP_5, 0x0035, + kKeyKP_6, 0x0036, + kKeyKP_7, 0x0037, + kKeyKP_8, 0x0038, + kKeyKP_9, 0x0039 +}; + +// +// CKeyState +// + +CKeyState::CKeyState() : + IKeyState(), + m_mask(0), + m_keyMapPtr(new CKeyMap()), + m_keyMap(*m_keyMapPtr) +{ + init(); +} + +CKeyState::CKeyState(IEventQueue& eventQueue, CKeyMap& keyMap) : + IKeyState(eventQueue), + m_mask(0), + m_keyMapPtr(0), + m_keyMap(keyMap) +{ + init(); +} + +CKeyState::~CKeyState() +{ + if (m_keyMapPtr) + delete m_keyMapPtr; +} + +void +CKeyState::init() +{ + memset(&m_keys, 0, sizeof(m_keys)); + memset(&m_syntheticKeys, 0, sizeof(m_syntheticKeys)); + memset(&m_keyClientData, 0, sizeof(m_keyClientData)); + memset(&m_serverKeys, 0, sizeof(m_serverKeys)); +} + +void +CKeyState::onKey(KeyButton button, bool down, KeyModifierMask newState) +{ + // update modifier state + m_mask = newState; + LOG((CLOG_DEBUG1 "new mask: 0x%04x", m_mask)); + + // ignore bogus buttons + button &= kButtonMask; + if (button == 0) { + return; + } + + // update key state + if (down) { + m_keys[button] = 1; + m_syntheticKeys[button] = 1; + } + else { + m_keys[button] = 0; + m_syntheticKeys[button] = 0; + } +} + +void +CKeyState::sendKeyEvent( + void* target, bool press, bool isAutoRepeat, + KeyID key, KeyModifierMask mask, + SInt32 count, KeyButton button) +{ + if (m_keyMap.isHalfDuplex(key, button)) { + if (isAutoRepeat) { + // ignore auto-repeat on half-duplex keys + } + else { + getEventQueue().addEvent(CEvent(getKeyDownEvent(), target, + CKeyInfo::alloc(key, mask, button, 1))); + getEventQueue().addEvent(CEvent(getKeyUpEvent(), target, + CKeyInfo::alloc(key, mask, button, 1))); + } + } + else { + if (isAutoRepeat) { + getEventQueue().addEvent(CEvent(getKeyRepeatEvent(), target, + CKeyInfo::alloc(key, mask, button, count))); + } + else if (press) { + getEventQueue().addEvent(CEvent(getKeyDownEvent(), target, + CKeyInfo::alloc(key, mask, button, 1))); + } + else { + getEventQueue().addEvent(CEvent(getKeyUpEvent(), target, + CKeyInfo::alloc(key, mask, button, 1))); + } + } +} + +void +CKeyState::updateKeyMap() +{ + // get the current keyboard map + CKeyMap keyMap; + getKeyMap(keyMap); + m_keyMap.swap(keyMap); + m_keyMap.finish(); + + // add special keys + addCombinationEntries(); + addKeypadEntries(); + addAliasEntries(); +} + +void +CKeyState::updateKeyState() +{ + // reset our state + memset(&m_keys, 0, sizeof(m_keys)); + memset(&m_syntheticKeys, 0, sizeof(m_syntheticKeys)); + memset(&m_keyClientData, 0, sizeof(m_keyClientData)); + memset(&m_serverKeys, 0, sizeof(m_serverKeys)); + m_activeModifiers.clear(); + + // get the current keyboard state + KeyButtonSet keysDown; + pollPressedKeys(keysDown); + for (KeyButtonSet::const_iterator i = keysDown.begin(); + i != keysDown.end(); ++i) { + m_keys[*i] = 1; + } + + // get the current modifier state + m_mask = pollActiveModifiers(); + + // set active modifiers + CAddActiveModifierContext addModifierContext(pollActiveGroup(), m_mask, + m_activeModifiers); + m_keyMap.foreachKey(&CKeyState::addActiveModifierCB, &addModifierContext); + + LOG((CLOG_DEBUG1 "modifiers on update: 0x%04x", m_mask)); +} + +void +CKeyState::addActiveModifierCB(KeyID, SInt32 group, + CKeyMap::KeyItem& keyItem, void* vcontext) +{ + CAddActiveModifierContext* context = + reinterpret_cast(vcontext); + if (group == context->m_activeGroup && + (keyItem.m_generates & context->m_mask) != 0) { + context->m_activeModifiers.insert(std::make_pair( + keyItem.m_generates, keyItem)); + } +} + +void +CKeyState::setHalfDuplexMask(KeyModifierMask mask) +{ + m_keyMap.clearHalfDuplexModifiers(); + if ((mask & KeyModifierCapsLock) != 0) { + m_keyMap.addHalfDuplexModifier(kKeyCapsLock); + } + if ((mask & KeyModifierNumLock) != 0) { + m_keyMap.addHalfDuplexModifier(kKeyNumLock); + } + if ((mask & KeyModifierScrollLock) != 0) { + m_keyMap.addHalfDuplexModifier(kKeyScrollLock); + } +} + +void +CKeyState::fakeKeyDown(KeyID id, KeyModifierMask mask, KeyButton serverID) +{ + // if this server key is already down then this is probably a + // mis-reported autorepeat. + serverID &= kButtonMask; + if (m_serverKeys[serverID] != 0) { + fakeKeyRepeat(id, mask, 1, serverID); + return; + } + + // ignore certain keys + if (isIgnoredKey(id, mask)) { + LOG((CLOG_DEBUG1 "ignored key %04x %04x", id, mask)); + return; + } + + // get keys for key press + Keystrokes keys; + ModifierToKeys oldActiveModifiers = m_activeModifiers; + const CKeyMap::KeyItem* keyItem = + m_keyMap.mapKey(keys, id, pollActiveGroup(), m_activeModifiers, + getActiveModifiersRValue(), mask, false); + if (keyItem == NULL) { + return; + } + KeyButton localID = (KeyButton)(keyItem->m_button & kButtonMask); + updateModifierKeyState(localID, oldActiveModifiers, m_activeModifiers); + if (localID != 0) { + // note keys down + ++m_keys[localID]; + ++m_syntheticKeys[localID]; + m_keyClientData[localID] = keyItem->m_client; + m_serverKeys[serverID] = localID; + } + + // generate key events + fakeKeys(keys, 1); +} + +bool +CKeyState::fakeKeyRepeat( + KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton serverID) +{ + serverID &= kButtonMask; + + // if we haven't seen this button go down then ignore it + KeyButton oldLocalID = m_serverKeys[serverID]; + if (oldLocalID == 0) { + return false; + } + + // get keys for key repeat + Keystrokes keys; + ModifierToKeys oldActiveModifiers = m_activeModifiers; + const CKeyMap::KeyItem* keyItem = + m_keyMap.mapKey(keys, id, pollActiveGroup(), m_activeModifiers, + getActiveModifiersRValue(), mask, true); + if (keyItem == NULL) { + return false; + } + KeyButton localID = (KeyButton)(keyItem->m_button & kButtonMask); + if (localID == 0) { + return false; + } + + // if the KeyButton for the auto-repeat is not the same as for the + // initial press then mark the initial key as released and the new + // key as pressed. this can happen when we auto-repeat after a + // dead key. for example, a dead accent followed by 'a' will + // generate an 'a with accent' followed by a repeating 'a'. the + // KeyButtons for the two KeyIDs might be different. + if (localID != oldLocalID) { + // replace key up with previous KeyButton but leave key down + // alone so it uses the new KeyButton. + for (Keystrokes::iterator index = keys.begin(); + index != keys.end(); ++index) { + if (index->m_type == Keystroke::kButton && + index->m_data.m_button.m_button == localID) { + index->m_data.m_button.m_button = oldLocalID; + break; + } + } + + // note that old key is now up + --m_keys[oldLocalID]; + --m_syntheticKeys[oldLocalID]; + + // note keys down + updateModifierKeyState(localID, oldActiveModifiers, m_activeModifiers); + ++m_keys[localID]; + ++m_syntheticKeys[localID]; + m_keyClientData[localID] = keyItem->m_client; + m_serverKeys[serverID] = localID; + } + + // generate key events + fakeKeys(keys, count); + return true; +} + +bool +CKeyState::fakeKeyUp(KeyButton serverID) +{ + // if we haven't seen this button go down then ignore it + KeyButton localID = m_serverKeys[serverID & kButtonMask]; + if (localID == 0) { + return false; + } + + // get the sequence of keys to simulate key release + Keystrokes keys; + keys.push_back(Keystroke(localID, false, false, m_keyClientData[localID])); + + // note keys down + --m_keys[localID]; + --m_syntheticKeys[localID]; + m_serverKeys[serverID] = 0; + + // check if this is a modifier + ModifierToKeys::iterator i = m_activeModifiers.begin(); + while (i != m_activeModifiers.end()) { + if (i->second.m_button == localID && !i->second.m_lock) { + // modifier is no longer down + KeyModifierMask mask = i->first; + + ModifierToKeys::iterator tmp = i; + ++i; + m_activeModifiers.erase(tmp); + + if (m_activeModifiers.count(mask) == 0) { + // no key for modifier is down so deactivate modifier + m_mask &= ~mask; + LOG((CLOG_DEBUG1 "new state %04x", m_mask)); + } + } + else { + ++i; + } + } + + // generate key events + fakeKeys(keys, 1); + return true; +} + +void +CKeyState::fakeAllKeysUp() +{ + Keystrokes keys; + for (KeyButton i = 0; i < IKeyState::kNumButtons; ++i) { + if (m_syntheticKeys[i] > 0) { + keys.push_back(Keystroke(i, false, false, m_keyClientData[i])); + m_keys[i] = 0; + m_syntheticKeys[i] = 0; + } + } + fakeKeys(keys, 1); + memset(&m_serverKeys, 0, sizeof(m_serverKeys)); + m_activeModifiers.clear(); + m_mask = pollActiveModifiers(); +} + +bool +CKeyState::isKeyDown(KeyButton button) const +{ + return (m_keys[button & kButtonMask] > 0); +} + +KeyModifierMask +CKeyState::getActiveModifiers() const +{ + return m_mask; +} + +KeyModifierMask& +CKeyState::getActiveModifiersRValue() +{ + return m_mask; +} + +SInt32 +CKeyState::getEffectiveGroup(SInt32 group, SInt32 offset) const +{ + return m_keyMap.getEffectiveGroup(group, offset); +} + +bool +CKeyState::isIgnoredKey(KeyID key, KeyModifierMask) const +{ + switch (key) { + case kKeyCapsLock: + case kKeyNumLock: + case kKeyScrollLock: + return true; + + default: + return false; + } +} + +KeyButton +CKeyState::getButton(KeyID id, SInt32 group) const +{ + const CKeyMap::KeyItemList* items = + m_keyMap.findCompatibleKey(id, group, 0, 0); + if (items == NULL) { + return 0; + } + else { + return items->back().m_button; + } +} + +void +CKeyState::addAliasEntries() +{ + for (SInt32 g = 0, n = m_keyMap.getNumGroups(); g < n; ++g) { + // if we can't shift any kKeyTab key in a particular group but we can + // shift kKeyLeftTab then add a shifted kKeyTab entry that matches a + // shifted kKeyLeftTab entry. + m_keyMap.addKeyAliasEntry(kKeyTab, g, + KeyModifierShift, KeyModifierShift, + kKeyLeftTab, + KeyModifierShift, KeyModifierShift); + + // if we have no kKeyLeftTab but we do have a kKeyTab that can be + // shifted then add kKeyLeftTab that matches a kKeyTab. + m_keyMap.addKeyAliasEntry(kKeyLeftTab, g, + KeyModifierShift, KeyModifierShift, + kKeyTab, + 0, KeyModifierShift); + + // map non-breaking space to space + m_keyMap.addKeyAliasEntry(0x20, g, 0, 0, 0xa0, 0, 0); + } +} + +void +CKeyState::addKeypadEntries() +{ + // map every numpad key to its equivalent non-numpad key if it's not + // on the keyboard. + for (SInt32 g = 0, n = m_keyMap.getNumGroups(); g < n; ++g) { + for (size_t i = 0; i < sizeof(s_numpadTable) / + sizeof(s_numpadTable[0]); i += 2) { + m_keyMap.addKeyCombinationEntry(s_numpadTable[i], g, + s_numpadTable + i + 1, 1); + } + } +} + +void +CKeyState::addCombinationEntries() +{ + for (SInt32 g = 0, n = m_keyMap.getNumGroups(); g < n; ++g) { + // add dead and compose key composition sequences + for (const KeyID* i = s_decomposeTable; *i != 0; ++i) { + // count the decomposed keys for this key + UInt32 numKeys = 0; + for (const KeyID* j = i; *++j != 0; ) { + ++numKeys; + } + + // add an entry for this key + m_keyMap.addKeyCombinationEntry(*i, g, i + 1, numKeys); + + // next key + i += numKeys + 1; + } + } +} + +void +CKeyState::fakeKeys(const Keystrokes& keys, UInt32 count) +{ + // do nothing if no keys or no repeats + if (count == 0 || keys.empty()) { + return; + } + + // generate key events + LOG((CLOG_DEBUG1 "keystrokes:")); + for (Keystrokes::const_iterator k = keys.begin(); k != keys.end(); ) { + if (k->m_type == Keystroke::kButton && k->m_data.m_button.m_repeat) { + // repeat from here up to but not including the next key + // with m_repeat == false count times. + Keystrokes::const_iterator start = k; + while (count-- > 0) { + // send repeating events + for (k = start; k != keys.end() && + k->m_type == Keystroke::kButton && + k->m_data.m_button.m_repeat; ++k) { + fakeKey(*k); + } + } + + // note -- k is now on the first non-repeat key after the + // repeat keys, exactly where we'd like to continue from. + } + else { + // send event + fakeKey(*k); + + // next key + ++k; + } + } +} + +void +CKeyState::updateModifierKeyState(KeyButton button, + const ModifierToKeys& oldModifiers, + const ModifierToKeys& newModifiers) +{ + // get the pressed modifier buttons before and after + CKeyMap::ButtonToKeyMap oldKeys, newKeys; + for (ModifierToKeys::const_iterator i = oldModifiers.begin(); + i != oldModifiers.end(); ++i) { + oldKeys.insert(std::make_pair(i->second.m_button, &i->second)); + } + for (ModifierToKeys::const_iterator i = newModifiers.begin(); + i != newModifiers.end(); ++i) { + newKeys.insert(std::make_pair(i->second.m_button, &i->second)); + } + + // get the modifier buttons that were pressed or released + CKeyMap::ButtonToKeyMap pressed, released; + std::set_difference(oldKeys.begin(), oldKeys.end(), + newKeys.begin(), newKeys.end(), + std::inserter(released, released.end()), + ButtonToKeyLess()); + std::set_difference(newKeys.begin(), newKeys.end(), + oldKeys.begin(), oldKeys.end(), + std::inserter(pressed, pressed.end()), + ButtonToKeyLess()); + + // update state + for (CKeyMap::ButtonToKeyMap::const_iterator i = released.begin(); + i != released.end(); ++i) { + if (i->first != button) { + m_keys[i->first] = 0; + m_syntheticKeys[i->first] = 0; + } + } + for (CKeyMap::ButtonToKeyMap::const_iterator i = pressed.begin(); + i != pressed.end(); ++i) { + if (i->first != button) { + m_keys[i->first] = 1; + m_syntheticKeys[i->first] = 1; + m_keyClientData[i->first] = i->second->m_client; + } + } +} + + +// +// CKeyState::CAddActiveModifierContext +// + +CKeyState::CAddActiveModifierContext::CAddActiveModifierContext( + SInt32 group, KeyModifierMask mask, + ModifierToKeys& activeModifiers) : + m_activeGroup(group), + m_mask(mask), + m_activeModifiers(activeModifiers) +{ + // do nothing +} diff --git a/src/lib/synergy/CKeyState.h b/src/lib/synergy/CKeyState.h new file mode 100644 index 00000000..bc6d2e69 --- /dev/null +++ b/src/lib/synergy/CKeyState.h @@ -0,0 +1,230 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CKEYSTATE_H +#define CKEYSTATE_H + +#include "IKeyState.h" +#include "CKeyMap.h" + +//! Core key state +/*! +This class provides key state services. Subclasses must implement a few +platform specific methods. +*/ +class CKeyState : public IKeyState { +public: + CKeyState(); + CKeyState(IEventQueue& eventQueue, CKeyMap& keyMap); + virtual ~CKeyState(); + + //! @name manipulators + //@{ + + //! Handle key event + /*! + Sets the state of \p button to down or up and updates the current + modifier state to \p newState. This method should be called by + primary screens only in response to local events. For auto-repeat + set \p down to \c true. Overrides must forward to the superclass. + */ + virtual void onKey(KeyButton button, bool down, + KeyModifierMask newState); + + //! Post a key event + /*! + Posts a key event. This may adjust the event or post additional + events in some circumstances. If this is overridden it must forward + to the superclass. + */ + virtual void sendKeyEvent(void* target, + bool press, bool isAutoRepeat, + KeyID key, KeyModifierMask mask, + SInt32 count, KeyButton button); + + //@} + //! @name accessors + //@{ + + //@} + + // IKeyState overrides + virtual void updateKeyMap(); + virtual void updateKeyState(); + virtual void setHalfDuplexMask(KeyModifierMask); + virtual void fakeKeyDown(KeyID id, KeyModifierMask mask, + KeyButton button); + virtual bool fakeKeyRepeat(KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton button); + virtual bool fakeKeyUp(KeyButton button); + virtual void fakeAllKeysUp(); + virtual bool fakeCtrlAltDel() = 0; + virtual bool isKeyDown(KeyButton) const; + virtual KeyModifierMask + getActiveModifiers() const; + virtual KeyModifierMask + pollActiveModifiers() const = 0; + virtual SInt32 pollActiveGroup() const = 0; + virtual void pollPressedKeys(KeyButtonSet& pressedKeys) const = 0; + + SInt32 getKeyState(KeyButton keyButton) { return m_keys[keyButton]; } + +protected: + typedef CKeyMap::Keystroke Keystroke; + + //! @name protected manipulators + //@{ + + //! Get the keyboard map + /*! + Fills \p keyMap with the current keyboard map. + */ + virtual void getKeyMap(CKeyMap& keyMap) = 0; + + //! Fake a key event + /*! + Synthesize an event for \p keystroke. + */ + virtual void fakeKey(const Keystroke& keystroke) = 0; + + //! Get the active modifiers + /*! + Returns the modifiers that are currently active according to our + shadowed state. The state may be modified. + */ + virtual KeyModifierMask& + getActiveModifiersRValue(); + + //@} + //! @name protected accessors + //@{ + + //! Compute a group number + /*! + Returns the number of the group \p offset groups after group \p group. + */ + SInt32 getEffectiveGroup(SInt32 group, SInt32 offset) const; + + //! Check if key is ignored + /*! + Returns \c true if and only if the key should always be ignored. + The default returns \c true only for the toggle keys. + */ + virtual bool isIgnoredKey(KeyID key, KeyModifierMask mask) const; + + //! Get button for a KeyID + /*! + Return the button mapped to key \p id in group \p group if any, + otherwise returns 0. + */ + KeyButton getButton(KeyID id, SInt32 group) const; + + //@} + +private: + typedef CKeyMap::Keystrokes Keystrokes; + typedef CKeyMap::ModifierToKeys ModifierToKeys; +public: + struct CAddActiveModifierContext { + public: + CAddActiveModifierContext(SInt32 group, KeyModifierMask mask, + ModifierToKeys& activeModifiers); + + public: + SInt32 m_activeGroup; + KeyModifierMask m_mask; + ModifierToKeys& m_activeModifiers; + + private: + // not implemented + CAddActiveModifierContext(const CAddActiveModifierContext&); + CAddActiveModifierContext& operator=(const CAddActiveModifierContext&); + }; +private: + + class ButtonToKeyLess { + public: + bool operator()(const CKeyMap::ButtonToKeyMap::value_type& a, + const CKeyMap::ButtonToKeyMap::value_type b) const + { + return (a.first < b.first); + } + }; + + // not implemented + CKeyState(const CKeyState&); + CKeyState& operator=(const CKeyState&); + + // called by all ctors. + void init(); + + // adds alias key sequences. these are sequences that are equivalent + // to other sequences. + void addAliasEntries(); + + // adds non-keypad key sequences for keypad KeyIDs + void addKeypadEntries(); + + // adds key sequences for combination KeyIDs (those built using + // dead keys) + void addCombinationEntries(); + + // synthesize key events. synthesize auto-repeat events count times. + void fakeKeys(const Keystrokes&, UInt32 count); + + // update key state to match changes to modifiers + void updateModifierKeyState(KeyButton button, + const ModifierToKeys& oldModifiers, + const ModifierToKeys& newModifiers); + + // active modifiers collection callback + static void addActiveModifierCB(KeyID id, SInt32 group, + CKeyMap::KeyItem& keyItem, void* vcontext); + +private: + // must be declared before m_keyMap. used when this class owns the key map. + CKeyMap* m_keyMapPtr; + + // the keyboard map + CKeyMap& m_keyMap; + + // current modifier state + KeyModifierMask m_mask; + + // the active modifiers and the buttons activating them + ModifierToKeys m_activeModifiers; + + // current keyboard state (> 0 if pressed, 0 otherwise). this is + // initialized to the keyboard state according to the system then + // it tracks synthesized events. + SInt32 m_keys[kNumButtons]; + + // synthetic keyboard state (> 0 if pressed, 0 otherwise). this + // tracks the synthesized keyboard state. if m_keys[n] > 0 but + // m_syntheticKeys[n] == 0 then the key was pressed locally and + // not synthesized yet. + SInt32 m_syntheticKeys[kNumButtons]; + + // client data for each pressed key + UInt32 m_keyClientData[kNumButtons]; + + // server keyboard state. an entry is 0 if not the key isn't pressed + // otherwise it's the local KeyButton synthesized for the server key. + KeyButton m_serverKeys[kNumButtons]; +}; + +#endif diff --git a/src/lib/synergy/CMakeLists.txt b/src/lib/synergy/CMakeLists.txt new file mode 100644 index 00000000..1706c3b4 --- /dev/null +++ b/src/lib/synergy/CMakeLists.txt @@ -0,0 +1,129 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(inc + CClientTaskBarReceiver.h + CServerTaskBarReceiver.h + CApp.h + CClientApp.h + CServerApp.h + CClipboard.h + CKeyMap.h + CKeyState.h + CPacketStreamFilter.h + CPlatformScreen.h + CProtocolUtil.h + CScreen.h + ClipboardTypes.h + IClient.h + IClipboard.h + IKeyState.h + IPlatformScreen.h + IPrimaryScreen.h + IScreen.h + IScreenSaver.h + ISecondaryScreen.h + KeyTypes.h + MouseTypes.h + OptionTypes.h + ProtocolTypes.h + XScreen.h + XSynergy.h + GameDeviceTypes.h + CDaemonApp.h + CAppUtil.h + CArgsBase.h + IAppUtil.h + CEventGameDevice.h + CVncClient.h +) + +set(src + CClientTaskBarReceiver.cpp + CServerTaskBarReceiver.cpp + CApp.cpp + CClientApp.cpp + CServerApp.cpp + CClipboard.cpp + CKeyMap.cpp + CKeyState.cpp + CPacketStreamFilter.cpp + CPlatformScreen.cpp + CProtocolUtil.cpp + CScreen.cpp + IClipboard.cpp + IKeyState.cpp + IPrimaryScreen.cpp + IScreen.cpp + KeyTypes.cpp + ProtocolTypes.cpp + XScreen.cpp + XSynergy.cpp + ISecondaryScreen.cpp + CDaemonApp.cpp + CAppUtil.cpp + CArgsBase.cpp + CEventGameDevice.cpp + CVncClient.cpp +) + +if (WIN32) + list(APPEND inc + CAppUtilWindows.h + CGameDevice.h + ) + + list(APPEND src + ${inc} + CAppUtilWindows.cpp + CGameDevice.cpp + ) +elseif(UNIX) + list(APPEND src + CAppUtilUnix.cpp + ) +endif() + +set(inc + ../arch + ../base + ../client + ../common + ../io + ../mt + ../net + ../platform + ../server + ../synergy + ../.. + ../../vnc/common +) + +if (UNIX) + list(APPEND inc + ../../.. + ) +elseif (WIN32) + list(APPEND inc + ../../vnc/win + ) +endif() + +include_directories(${inc}) +add_library(synergy STATIC ${src}) + +if (UNIX) + target_link_libraries(synergy arch client net base platform mt server) +endif() diff --git a/src/lib/synergy/CPacketStreamFilter.cpp b/src/lib/synergy/CPacketStreamFilter.cpp new file mode 100644 index 00000000..a37d5872 --- /dev/null +++ b/src/lib/synergy/CPacketStreamFilter.cpp @@ -0,0 +1,195 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CPacketStreamFilter.h" +#include "IEventQueue.h" +#include "CLock.h" +#include "TMethodEventJob.h" +#include +#include + +// +// CPacketStreamFilter +// + +CPacketStreamFilter::CPacketStreamFilter(IStream* stream, bool adoptStream) : + CStreamFilter(stream, adoptStream), + m_size(0), + m_inputShutdown(false) +{ + // do nothing +} + +CPacketStreamFilter::~CPacketStreamFilter() +{ + // do nothing +} + +void +CPacketStreamFilter::close() +{ + CLock lock(&m_mutex); + m_size = 0; + m_buffer.pop(m_buffer.getSize()); + CStreamFilter::close(); +} + +UInt32 +CPacketStreamFilter::read(void* buffer, UInt32 n) +{ + if (n == 0) { + return 0; + } + + CLock lock(&m_mutex); + + // if not enough data yet then give up + if (!isReadyNoLock()) { + return 0; + } + + // read no more than what's left in the buffered packet + if (n > m_size) { + n = m_size; + } + + // read it + if (buffer != NULL) { + memcpy(buffer, m_buffer.peek(n), n); + } + m_buffer.pop(n); + m_size -= n; + + // get next packet's size if we've finished with this packet and + // there's enough data to do so. + readPacketSize(); + + if (m_inputShutdown && m_size == 0) { + EVENTQUEUE->addEvent(CEvent(getInputShutdownEvent(), + getEventTarget(), NULL)); + } + + return n; +} + +void +CPacketStreamFilter::write(const void* buffer, UInt32 count) +{ + // write the length of the payload + UInt8 length[4]; + length[0] = (UInt8)((count >> 24) & 0xff); + length[1] = (UInt8)((count >> 16) & 0xff); + length[2] = (UInt8)((count >> 8) & 0xff); + length[3] = (UInt8)( count & 0xff); + getStream()->write(length, sizeof(length)); + + // write the payload + getStream()->write(buffer, count); +} + +void +CPacketStreamFilter::shutdownInput() +{ + CLock lock(&m_mutex); + m_size = 0; + m_buffer.pop(m_buffer.getSize()); + CStreamFilter::shutdownInput(); +} + +bool +CPacketStreamFilter::isReady() const +{ + CLock lock(&m_mutex); + return isReadyNoLock(); +} + +UInt32 +CPacketStreamFilter::getSize() const +{ + CLock lock(&m_mutex); + return isReadyNoLock() ? m_size : 0; +} + +bool +CPacketStreamFilter::isReadyNoLock() const +{ + return (m_size != 0 && m_buffer.getSize() >= m_size); +} + +void +CPacketStreamFilter::readPacketSize() +{ + // note -- m_mutex must be locked on entry + + if (m_size == 0 && m_buffer.getSize() >= 4) { + UInt8 buffer[4]; + memcpy(buffer, m_buffer.peek(sizeof(buffer)), sizeof(buffer)); + m_buffer.pop(sizeof(buffer)); + m_size = ((UInt32)buffer[0] << 24) | + ((UInt32)buffer[1] << 16) | + ((UInt32)buffer[2] << 8) | + (UInt32)buffer[3]; + } +} + +bool +CPacketStreamFilter::readMore() +{ + // note if we have whole packet + bool wasReady = isReadyNoLock(); + + // read more data + char buffer[4096]; + UInt32 n = getStream()->read(buffer, sizeof(buffer)); + while (n > 0) { + m_buffer.write(buffer, n); + n = getStream()->read(buffer, sizeof(buffer)); + } + + // if we don't yet have the next packet size then get it, + // if possible. + readPacketSize(); + + // note if we now have a whole packet + bool isReady = isReadyNoLock(); + + // if we weren't ready before but now we are then send a + // input ready event apparently from the filtered stream. + return (wasReady != isReady); +} + +void +CPacketStreamFilter::filterEvent(const CEvent& event) +{ + if (event.getType() == getInputReadyEvent()) { + CLock lock(&m_mutex); + if (!readMore()) { + return; + } + } + else if (event.getType() == getInputShutdownEvent()) { + // discard this if we have buffered data + CLock lock(&m_mutex); + m_inputShutdown = true; + if (m_size != 0) { + return; + } + } + + // pass event + CStreamFilter::filterEvent(event); +} diff --git a/src/lib/synergy/CPacketStreamFilter.h b/src/lib/synergy/CPacketStreamFilter.h new file mode 100644 index 00000000..16d23b93 --- /dev/null +++ b/src/lib/synergy/CPacketStreamFilter.h @@ -0,0 +1,58 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CPACKETSTREAMFILTER_H +#define CPACKETSTREAMFILTER_H + +#include "CStreamFilter.h" +#include "CStreamBuffer.h" +#include "CMutex.h" + +//! Packetizing stream filter +/*! +Filters a stream to read and write packets. +*/ +class CPacketStreamFilter : public CStreamFilter { +public: + CPacketStreamFilter(IStream* stream, bool adoptStream = true); + ~CPacketStreamFilter(); + + // IStream overrides + virtual void close(); + virtual UInt32 read(void* buffer, UInt32 n); + virtual void write(const void* buffer, UInt32 n); + virtual void shutdownInput(); + virtual bool isReady() const; + virtual UInt32 getSize() const; + +protected: + // CStreamFilter overrides + virtual void filterEvent(const CEvent&); + +private: + bool isReadyNoLock() const; + void readPacketSize(); + bool readMore(); + +private: + CMutex m_mutex; + UInt32 m_size; + CStreamBuffer m_buffer; + bool m_inputShutdown; +}; + +#endif diff --git a/src/lib/synergy/CPlatformScreen.cpp b/src/lib/synergy/CPlatformScreen.cpp new file mode 100644 index 00000000..d507b32f --- /dev/null +++ b/src/lib/synergy/CPlatformScreen.cpp @@ -0,0 +1,114 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CPlatformScreen.h" + +CPlatformScreen::CPlatformScreen() +{ + // do nothing +} + +CPlatformScreen::CPlatformScreen(IEventQueue& eventQueue) : + IPlatformScreen(eventQueue) +{ +} + +CPlatformScreen::~CPlatformScreen() +{ + // do nothing +} + +void +CPlatformScreen::updateKeyMap() +{ + getKeyState()->updateKeyMap(); +} + +void +CPlatformScreen::updateKeyState() +{ + getKeyState()->updateKeyState(); + updateButtons(); +} + +void +CPlatformScreen::setHalfDuplexMask(KeyModifierMask mask) +{ + getKeyState()->setHalfDuplexMask(mask); +} + +void +CPlatformScreen::fakeKeyDown(KeyID id, KeyModifierMask mask, + KeyButton button) +{ + getKeyState()->fakeKeyDown(id, mask, button); +} + +bool +CPlatformScreen::fakeKeyRepeat(KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton button) +{ + return getKeyState()->fakeKeyRepeat(id, mask, count, button); +} + +bool +CPlatformScreen::fakeKeyUp(KeyButton button) +{ + return getKeyState()->fakeKeyUp(button); +} + +void +CPlatformScreen::fakeAllKeysUp() +{ + getKeyState()->fakeAllKeysUp(); +} + +bool +CPlatformScreen::fakeCtrlAltDel() +{ + return getKeyState()->fakeCtrlAltDel(); +} + +bool +CPlatformScreen::isKeyDown(KeyButton button) const +{ + return getKeyState()->isKeyDown(button); +} + +KeyModifierMask +CPlatformScreen::getActiveModifiers() const +{ + return getKeyState()->getActiveModifiers(); +} + +KeyModifierMask +CPlatformScreen::pollActiveModifiers() const +{ + return getKeyState()->pollActiveModifiers(); +} + +SInt32 +CPlatformScreen::pollActiveGroup() const +{ + return getKeyState()->pollActiveGroup(); +} + +void +CPlatformScreen::pollPressedKeys(KeyButtonSet& pressedKeys) const +{ + getKeyState()->pollPressedKeys(pressedKeys); +} diff --git a/src/lib/synergy/CPlatformScreen.h b/src/lib/synergy/CPlatformScreen.h new file mode 100644 index 00000000..e6402998 --- /dev/null +++ b/src/lib/synergy/CPlatformScreen.h @@ -0,0 +1,118 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CPLATFORMSCREEN_H +#define CPLATFORMSCREEN_H + +#include "IPlatformScreen.h" + +//! Base screen implementation +/*! +This screen implementation is the superclass of all other screen +implementations. It implements a handful of methods and requires +subclasses to implement the rest. +*/ +class CPlatformScreen : public IPlatformScreen { +public: + CPlatformScreen(); + CPlatformScreen(IEventQueue& eventQueue); + virtual ~CPlatformScreen(); + + // IScreen overrides + virtual void* getEventTarget() const = 0; + virtual bool getClipboard(ClipboardID id, IClipboard*) const = 0; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const = 0; + virtual void getCursorPos(SInt32& x, SInt32& y) const = 0; + + // IPrimaryScreen overrides + virtual void reconfigure(UInt32 activeSides) = 0; + virtual void warpCursor(SInt32 x, SInt32 y) = 0; + virtual UInt32 registerHotKey(KeyID key, + KeyModifierMask mask) = 0; + virtual void unregisterHotKey(UInt32 id) = 0; + virtual void fakeInputBegin() = 0; + virtual void fakeInputEnd() = 0; + virtual SInt32 getJumpZoneSize() const = 0; + virtual bool isAnyMouseButtonDown() const = 0; + virtual void getCursorCenter(SInt32& x, SInt32& y) const = 0; + virtual void gameDeviceTimingResp(UInt16 freq) = 0; + + // ISecondaryScreen overrides + virtual void fakeMouseButton(ButtonID id, bool press) = 0; + virtual void fakeMouseMove(SInt32 x, SInt32 y) const = 0; + virtual void fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const = 0; + virtual void fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const = 0; + virtual void fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const = 0; + virtual void fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const = 0; + virtual void fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const = 0; + virtual void queueGameDeviceTimingReq() const = 0; + + // IKeyState overrides + virtual void updateKeyMap(); + virtual void updateKeyState(); + virtual void setHalfDuplexMask(KeyModifierMask); + virtual void fakeKeyDown(KeyID id, KeyModifierMask mask, + KeyButton button); + virtual bool fakeKeyRepeat(KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton button); + virtual bool fakeKeyUp(KeyButton button); + virtual void fakeAllKeysUp(); + virtual bool fakeCtrlAltDel(); + virtual bool isKeyDown(KeyButton) const; + virtual KeyModifierMask + getActiveModifiers() const; + virtual KeyModifierMask + pollActiveModifiers() const; + virtual SInt32 pollActiveGroup() const; + virtual void pollPressedKeys(KeyButtonSet& pressedKeys) const; + + // IPlatformScreen overrides + virtual void enable() = 0; + virtual void disable() = 0; + virtual void enter() = 0; + virtual bool leave() = 0; + virtual bool setClipboard(ClipboardID, const IClipboard*) = 0; + virtual void checkClipboards() = 0; + virtual void openScreensaver(bool notify) = 0; + virtual void closeScreensaver() = 0; + virtual void screensaver(bool activate) = 0; + virtual void resetOptions() = 0; + virtual void setOptions(const COptionsList& options) = 0; + virtual void setSequenceNumber(UInt32) = 0; + virtual bool isPrimary() const = 0; + +protected: + //! Update mouse buttons + /*! + Subclasses must implement this method to update their internal mouse + button mapping and, if desired, state tracking. + */ + virtual void updateButtons() = 0; + + //! Get the key state + /*! + Subclasses must implement this method to return the platform specific + key state object that each subclass must have. + */ + virtual IKeyState* getKeyState() const = 0; + + // IPlatformScreen overrides + virtual void handleSystemEvent(const CEvent& event, void*) = 0; +}; + +#endif diff --git a/src/lib/synergy/CProtocolUtil.cpp b/src/lib/synergy/CProtocolUtil.cpp new file mode 100644 index 00000000..ecda7ba8 --- /dev/null +++ b/src/lib/synergy/CProtocolUtil.cpp @@ -0,0 +1,541 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CProtocolUtil.h" +#include "IStream.h" +#include "CLog.h" +#include "stdvector.h" +#include +#include + +// +// CProtocolUtil +// + +void +CProtocolUtil::writef(IStream* stream, const char* fmt, ...) +{ + assert(stream != NULL); + assert(fmt != NULL); + LOG((CLOG_DEBUG2 "writef(%s)", fmt)); + + va_list args; + va_start(args, fmt); + UInt32 size = getLength(fmt, args); + va_end(args); + va_start(args, fmt); + vwritef(stream, fmt, size, args); + va_end(args); +} + +bool +CProtocolUtil::readf(IStream* stream, const char* fmt, ...) +{ + assert(stream != NULL); + assert(fmt != NULL); + LOG((CLOG_DEBUG2 "readf(%s)", fmt)); + + bool result; + va_list args; + va_start(args, fmt); + try { + vreadf(stream, fmt, args); + result = true; + } + catch (XIO&) { + result = false; + } + va_end(args); + return result; +} + +void +CProtocolUtil::vwritef(IStream* stream, + const char* fmt, UInt32 size, va_list args) +{ + assert(stream != NULL); + assert(fmt != NULL); + + // done if nothing to write + if (size == 0) { + return; + } + + // fill buffer + UInt8* buffer = new UInt8[size]; + writef(buffer, fmt, args); + + try { + // write buffer + stream->write(buffer, size); + LOG((CLOG_DEBUG2 "wrote %d bytes", size)); + + delete[] buffer; + } + catch (XBase&) { + delete[] buffer; + throw; + } +} + +void +CProtocolUtil::vreadf(IStream* stream, const char* fmt, va_list args) +{ + assert(stream != NULL); + assert(fmt != NULL); + + // begin scanning + while (*fmt) { + if (*fmt == '%') { + // format specifier. determine argument size. + ++fmt; + UInt32 len = eatLength(&fmt); + switch (*fmt) { + case 'i': { + // check for valid length + assert(len == 1 || len == 2 || len == 4); + + // read the data + UInt8 buffer[4]; + read(stream, buffer, len); + + // convert it + void* v = va_arg(args, void*); + switch (len) { + case 1: + // 1 byte integer + *reinterpret_cast(v) = buffer[0]; + LOG((CLOG_DEBUG2 "readf: read %d byte integer: %d (0x%x)", len, *reinterpret_cast(v), *reinterpret_cast(v))); + break; + + case 2: + // 2 byte integer + *reinterpret_cast(v) = + static_cast( + (static_cast(buffer[0]) << 8) | + static_cast(buffer[1])); + LOG((CLOG_DEBUG2 "readf: read %d byte integer: %d (0x%x)", len, *reinterpret_cast(v), *reinterpret_cast(v))); + break; + + case 4: + // 4 byte integer + *reinterpret_cast(v) = + (static_cast(buffer[0]) << 24) | + (static_cast(buffer[1]) << 16) | + (static_cast(buffer[2]) << 8) | + static_cast(buffer[3]); + LOG((CLOG_DEBUG2 "readf: read %d byte integer: %d (0x%x)", len, *reinterpret_cast(v), *reinterpret_cast(v))); + break; + } + break; + } + + case 'I': { + // check for valid length + assert(len == 1 || len == 2 || len == 4); + + // read the vector length + UInt8 buffer[4]; + read(stream, buffer, 4); + UInt32 n = (static_cast(buffer[0]) << 24) | + (static_cast(buffer[1]) << 16) | + (static_cast(buffer[2]) << 8) | + static_cast(buffer[3]); + + // convert it + void* v = va_arg(args, void*); + switch (len) { + case 1: + // 1 byte integer + for (UInt32 i = 0; i < n; ++i) { + read(stream, buffer, 1); + reinterpret_cast*>(v)->push_back( + buffer[0]); + LOG((CLOG_DEBUG2 "readf: read %d byte integer[%d]: %d (0x%x)", len, i, reinterpret_cast*>(v)->back(), reinterpret_cast*>(v)->back())); + } + break; + + case 2: + // 2 byte integer + for (UInt32 i = 0; i < n; ++i) { + read(stream, buffer, 2); + reinterpret_cast*>(v)->push_back( + static_cast( + (static_cast(buffer[0]) << 8) | + static_cast(buffer[1]))); + LOG((CLOG_DEBUG2 "readf: read %d byte integer[%d]: %d (0x%x)", len, i, reinterpret_cast*>(v)->back(), reinterpret_cast*>(v)->back())); + } + break; + + case 4: + // 4 byte integer + for (UInt32 i = 0; i < n; ++i) { + read(stream, buffer, 4); + reinterpret_cast*>(v)->push_back( + (static_cast(buffer[0]) << 24) | + (static_cast(buffer[1]) << 16) | + (static_cast(buffer[2]) << 8) | + static_cast(buffer[3])); + LOG((CLOG_DEBUG2 "readf: read %d byte integer[%d]: %d (0x%x)", len, i, reinterpret_cast*>(v)->back(), reinterpret_cast*>(v)->back())); + } + break; + } + break; + } + + case 's': { + assert(len == 0); + + // read the string length + UInt8 buffer[128]; + read(stream, buffer, 4); + UInt32 len = (static_cast(buffer[0]) << 24) | + (static_cast(buffer[1]) << 16) | + (static_cast(buffer[2]) << 8) | + static_cast(buffer[3]); + + // use a fixed size buffer if its big enough + const bool useFixed = (len <= sizeof(buffer)); + + // allocate a buffer to read the data + UInt8* sBuffer = buffer; + if (!useFixed) { + sBuffer = new UInt8[len]; + } + + // read the data + try { + read(stream, sBuffer, len); + } + catch (...) { + if (!useFixed) { + delete[] sBuffer; + } + throw; + } + LOG((CLOG_DEBUG2 "readf: read %d byte string: %.*s", len, len, sBuffer)); + + // save the data + CString* dst = va_arg(args, CString*); + dst->assign((const char*)sBuffer, len); + + // release the buffer + if (!useFixed) { + delete[] sBuffer; + } + break; + } + + case '%': + assert(len == 0); + break; + + default: + assert(0 && "invalid format specifier"); + } + + // next format character + ++fmt; + } + else { + // read next character + char buffer[1]; + read(stream, buffer, 1); + + // verify match + if (buffer[0] != *fmt) { + LOG((CLOG_DEBUG2 "readf: format mismatch: %c vs %c", *fmt, buffer[0])); + throw XIOReadMismatch(); + } + + // next format character + ++fmt; + } + } +} + +UInt32 +CProtocolUtil::getLength(const char* fmt, va_list args) +{ + UInt32 n = 0; + while (*fmt) { + if (*fmt == '%') { + // format specifier. determine argument size. + ++fmt; + UInt32 len = eatLength(&fmt); + switch (*fmt) { + case 'i': + assert(len == 1 || len == 2 || len == 4); + (void)va_arg(args, UInt32); + break; + + case 'I': + assert(len == 1 || len == 2 || len == 4); + switch (len) { + case 1: + len = (UInt32)(va_arg(args, std::vector*))->size() + 4; + break; + + case 2: + len = 2 * (UInt32)(va_arg(args, std::vector*))->size() + 4; + break; + + case 4: + len = 4 * (UInt32)(va_arg(args, std::vector*))->size() + 4; + break; + } + break; + + case 's': + assert(len == 0); + len = (UInt32)(va_arg(args, CString*))->size() + 4; + (void)va_arg(args, UInt8*); + break; + + case 'S': + assert(len == 0); + len = va_arg(args, UInt32) + 4; + (void)va_arg(args, UInt8*); + break; + + case '%': + assert(len == 0); + len = 1; + break; + + default: + assert(0 && "invalid format specifier"); + } + + // accumulate size + n += len; + ++fmt; + } + else { + // regular character + ++n; + ++fmt; + } + } + return n; +} + +void +CProtocolUtil::writef(void* buffer, const char* fmt, va_list args) +{ + UInt8* dst = reinterpret_cast(buffer); + + while (*fmt) { + if (*fmt == '%') { + // format specifier. determine argument size. + ++fmt; + UInt32 len = eatLength(&fmt); + switch (*fmt) { + case 'i': { + const UInt32 v = va_arg(args, UInt32); + switch (len) { + case 1: + // 1 byte integer + *dst++ = static_cast(v & 0xff); + break; + + case 2: + // 2 byte integer + *dst++ = static_cast((v >> 8) & 0xff); + *dst++ = static_cast( v & 0xff); + break; + + case 4: + // 4 byte integer + *dst++ = static_cast((v >> 24) & 0xff); + *dst++ = static_cast((v >> 16) & 0xff); + *dst++ = static_cast((v >> 8) & 0xff); + *dst++ = static_cast( v & 0xff); + break; + + default: + assert(0 && "invalid integer format length"); + return; + } + break; + } + + case 'I': { + switch (len) { + case 1: { + // 1 byte integers + const std::vector* list = + va_arg(args, const std::vector*); + const UInt32 n = (UInt32)list->size(); + *dst++ = static_cast((n >> 24) & 0xff); + *dst++ = static_cast((n >> 16) & 0xff); + *dst++ = static_cast((n >> 8) & 0xff); + *dst++ = static_cast( n & 0xff); + for (UInt32 i = 0; i < n; ++i) { + *dst++ = (*list)[i]; + } + break; + } + + case 2: { + // 2 byte integers + const std::vector* list = + va_arg(args, const std::vector*); + const UInt32 n = (UInt32)list->size(); + *dst++ = static_cast((n >> 24) & 0xff); + *dst++ = static_cast((n >> 16) & 0xff); + *dst++ = static_cast((n >> 8) & 0xff); + *dst++ = static_cast( n & 0xff); + for (UInt32 i = 0; i < n; ++i) { + const UInt16 v = (*list)[i]; + *dst++ = static_cast((v >> 8) & 0xff); + *dst++ = static_cast( v & 0xff); + } + break; + } + + case 4: { + // 4 byte integers + const std::vector* list = + va_arg(args, const std::vector*); + const UInt32 n = (UInt32)list->size(); + *dst++ = static_cast((n >> 24) & 0xff); + *dst++ = static_cast((n >> 16) & 0xff); + *dst++ = static_cast((n >> 8) & 0xff); + *dst++ = static_cast( n & 0xff); + for (UInt32 i = 0; i < n; ++i) { + const UInt32 v = (*list)[i]; + *dst++ = static_cast((v >> 24) & 0xff); + *dst++ = static_cast((v >> 16) & 0xff); + *dst++ = static_cast((v >> 8) & 0xff); + *dst++ = static_cast( v & 0xff); + } + break; + } + + default: + assert(0 && "invalid integer vector format length"); + return; + } + break; + } + + case 's': { + assert(len == 0); + const CString* src = va_arg(args, CString*); + const UInt32 len = (src != NULL) ? (UInt32)src->size() : 0; + *dst++ = static_cast((len >> 24) & 0xff); + *dst++ = static_cast((len >> 16) & 0xff); + *dst++ = static_cast((len >> 8) & 0xff); + *dst++ = static_cast( len & 0xff); + if (len != 0) { + memcpy(dst, src->data(), len); + dst += len; + } + break; + } + + case 'S': { + assert(len == 0); + const UInt32 len = va_arg(args, UInt32); + const UInt8* src = va_arg(args, UInt8*); + *dst++ = static_cast((len >> 24) & 0xff); + *dst++ = static_cast((len >> 16) & 0xff); + *dst++ = static_cast((len >> 8) & 0xff); + *dst++ = static_cast( len & 0xff); + memcpy(dst, src, len); + dst += len; + break; + } + + case '%': + assert(len == 0); + *dst++ = '%'; + break; + + default: + assert(0 && "invalid format specifier"); + } + + // next format character + ++fmt; + } + else { + // copy regular character + *dst++ = *fmt++; + } + } +} + +UInt32 +CProtocolUtil::eatLength(const char** pfmt) +{ + const char* fmt = *pfmt; + UInt32 n = 0; + for (;;) { + UInt32 d; + switch (*fmt) { + case '0': d = 0; break; + case '1': d = 1; break; + case '2': d = 2; break; + case '3': d = 3; break; + case '4': d = 4; break; + case '5': d = 5; break; + case '6': d = 6; break; + case '7': d = 7; break; + case '8': d = 8; break; + case '9': d = 9; break; + default: *pfmt = fmt; return n; + } + n = 10 * n + d; + ++fmt; + } +} + +void +CProtocolUtil::read(IStream* stream, void* vbuffer, UInt32 count) +{ + assert(stream != NULL); + assert(vbuffer != NULL); + + UInt8* buffer = reinterpret_cast(vbuffer); + while (count > 0) { + // read more + UInt32 n = stream->read(buffer, count); + + // bail if stream has hungup + if (n == 0) { + LOG((CLOG_DEBUG2 "unexpected disconnect in readf(), %d bytes left", count)); + throw XIOEndOfStream(); + } + + // prepare for next read + buffer += n; + count -= n; + } +} + + +// +// XIOReadMismatch +// + +CString +XIOReadMismatch::getWhat() const throw() +{ + return format("XIOReadMismatch", "CProtocolUtil::readf() mismatch"); +} diff --git a/src/lib/synergy/CProtocolUtil.h b/src/lib/synergy/CProtocolUtil.h new file mode 100644 index 00000000..1d5494fd --- /dev/null +++ b/src/lib/synergy/CProtocolUtil.h @@ -0,0 +1,98 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CPROTOCOLUTIL_H +#define CPROTOCOLUTIL_H + +#include "BasicTypes.h" +#include "XIO.h" +#include + +class IStream; + +//! Synergy protocol utilities +/*! +This class provides various functions for implementing the synergy +protocol. +*/ +class CProtocolUtil { +public: + //! Write formatted data + /*! + Write formatted binary data to a stream. \c fmt consists of + regular characters and format specifiers. Format specifiers + begin with \%. All characters not part of a format specifier + are regular and are transmitted unchanged. + + Format specifiers are: + - \%\% -- literal `\%' + - \%1i -- converts integer argument to 1 byte integer + - \%2i -- converts integer argument to 2 byte integer in NBO + - \%4i -- converts integer argument to 4 byte integer in NBO + - \%1I -- converts std::vector* to 1 byte integers + - \%2I -- converts std::vector* to 2 byte integers in NBO + - \%4I -- converts std::vector* to 4 byte integers in NBO + - \%s -- converts CString* to stream of bytes + - \%S -- converts integer N and const UInt8* to stream of N bytes + */ + static void writef(IStream*, + const char* fmt, ...); + + //! Read formatted data + /*! + Read formatted binary data from a buffer. This performs the + reverse operation of writef(). Returns true if the entire + format was successfully parsed, false otherwise. + + Format specifiers are: + - \%\% -- read (and discard) a literal `\%' + - \%1i -- reads a 1 byte integer; argument is a SInt32* or UInt32* + - \%2i -- reads an NBO 2 byte integer; arg is SInt32* or UInt32* + - \%4i -- reads an NBO 4 byte integer; arg is SInt32* or UInt32* + - \%1I -- reads 1 byte integers; arg is std::vector* + - \%2I -- reads NBO 2 byte integers; arg is std::vector* + - \%4I -- reads NBO 4 byte integers; arg is std::vector* + - \%s -- reads bytes; argument must be a CString*, \b not a char* + */ + static bool readf(IStream*, + const char* fmt, ...); + +private: + static void vwritef(IStream*, + const char* fmt, UInt32 size, va_list); + static void vreadf(IStream*, + const char* fmt, va_list); + + static UInt32 getLength(const char* fmt, va_list); + static void writef(void*, const char* fmt, va_list); + static UInt32 eatLength(const char** fmt); + static void read(IStream*, void*, UInt32); +}; + +//! Mismatched read exception +/*! +Thrown by CProtocolUtil::readf() when the data being read does not +match the format. +*/ +class XIOReadMismatch : public XIO { +public: + // XBase overrides + virtual CString getWhat() const throw(); +}; + +#endif + diff --git a/src/lib/synergy/CScreen.cpp b/src/lib/synergy/CScreen.cpp new file mode 100644 index 00000000..9996148e --- /dev/null +++ b/src/lib/synergy/CScreen.cpp @@ -0,0 +1,533 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CScreen.h" +#include "IPlatformScreen.h" +#include "ProtocolTypes.h" +#include "CLog.h" +#include "IEventQueue.h" +#include "CClientProxy.h" +#include "TMethodEventJob.h" + +// +// CScreen +// + +CScreen::CScreen(IPlatformScreen* platformScreen) : + m_screen(platformScreen), + m_isPrimary(platformScreen->isPrimary()), + m_enabled(false), + m_entered(m_isPrimary), + m_screenSaverSync(true), + m_fakeInput(false) +{ + assert(m_screen != NULL); + + // reset options + resetOptions(); + + LOG((CLOG_DEBUG "opened display")); +} + +CScreen::~CScreen() +{ + if (m_enabled) { + disable(); + } + assert(!m_enabled); + assert(m_entered == m_isPrimary); + delete m_screen; + LOG((CLOG_DEBUG "closed display")); +} + +void +CScreen::enable() +{ + assert(!m_enabled); + + m_screen->updateKeyMap(); + m_screen->updateKeyState(); + m_screen->enable(); + if (m_isPrimary) { + enablePrimary(); + } + else { + enableSecondary(); + } + + // note activation + m_enabled = true; +} + +void +CScreen::disable() +{ + assert(m_enabled); + + if (!m_isPrimary && m_entered) { + leave(); + } + else if (m_isPrimary && !m_entered) { + enter(0); + } + m_screen->disable(); + if (m_isPrimary) { + disablePrimary(); + } + else { + disableSecondary(); + } + + // note deactivation + m_enabled = false; +} + +void +CScreen::enter(KeyModifierMask toggleMask) +{ + assert(m_entered == false); + LOG((CLOG_INFO "entering screen")); + + // now on screen + m_entered = true; + + m_screen->enter(); + if (m_isPrimary) { + enterPrimary(); + } + else { + enterSecondary(toggleMask); + } +} + +bool +CScreen::leave() +{ + assert(m_entered == true); + LOG((CLOG_INFO "leaving screen")); + + if (!m_screen->leave()) { + return false; + } + if (m_isPrimary) { + leavePrimary(); + } + else { + leaveSecondary(); + } + + // make sure our idea of clipboard ownership is correct + m_screen->checkClipboards(); + + // now not on screen + m_entered = false; + + return true; +} + +void +CScreen::reconfigure(UInt32 activeSides) +{ + assert(m_isPrimary); + m_screen->reconfigure(activeSides); +} + +void +CScreen::warpCursor(SInt32 x, SInt32 y) +{ + assert(m_isPrimary); + m_screen->warpCursor(x, y); +} + +void +CScreen::setClipboard(ClipboardID id, const IClipboard* clipboard) +{ + m_screen->setClipboard(id, clipboard); +} + +void +CScreen::grabClipboard(ClipboardID id) +{ + m_screen->setClipboard(id, NULL); +} + +void +CScreen::screensaver(bool activate) +{ + if (!m_isPrimary) { + // activate/deactivation screen saver iff synchronization enabled + if (m_screenSaverSync) { + m_screen->screensaver(activate); + } + } +} + +void +CScreen::keyDown(KeyID id, KeyModifierMask mask, KeyButton button) +{ + assert(!m_isPrimary || m_fakeInput); + + // check for ctrl+alt+del emulation + if (id == kKeyDelete && + (mask & (KeyModifierControl | KeyModifierAlt)) == + (KeyModifierControl | KeyModifierAlt)) { + LOG((CLOG_DEBUG "emulating ctrl+alt+del press")); + if (m_screen->fakeCtrlAltDel()) { + return; + } + } + m_screen->fakeKeyDown(id, mask, button); +} + +void +CScreen::keyRepeat(KeyID id, + KeyModifierMask mask, SInt32 count, KeyButton button) +{ + assert(!m_isPrimary); + m_screen->fakeKeyRepeat(id, mask, count, button); +} + +void +CScreen::keyUp(KeyID, KeyModifierMask, KeyButton button) +{ + assert(!m_isPrimary || m_fakeInput); + m_screen->fakeKeyUp(button); +} + +void +CScreen::mouseDown(ButtonID button) +{ + assert(!m_isPrimary); + m_screen->fakeMouseButton(button, true); +} + +void +CScreen::mouseUp(ButtonID button) +{ + assert(!m_isPrimary); + m_screen->fakeMouseButton(button, false); +} + +void +CScreen::mouseMove(SInt32 x, SInt32 y) +{ + assert(!m_isPrimary); + m_screen->fakeMouseMove(x, y); +} + +void +CScreen::mouseRelativeMove(SInt32 dx, SInt32 dy) +{ + assert(!m_isPrimary); + m_screen->fakeMouseRelativeMove(dx, dy); +} + +void +CScreen::mouseWheel(SInt32 xDelta, SInt32 yDelta) +{ + assert(!m_isPrimary); + m_screen->fakeMouseWheel(xDelta, yDelta); +} + +void +CScreen::gameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) +{ + assert(!m_isPrimary); + m_screen->fakeGameDeviceButtons(id, buttons); +} + +void +CScreen::gameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) +{ + assert(!m_isPrimary); + m_screen->fakeGameDeviceSticks(id, x1, y1, x2, y2); +} + +void +CScreen::gameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) +{ + assert(!m_isPrimary); + m_screen->fakeGameDeviceTriggers(id, t1, t2); +} + +void +CScreen::gameDeviceTimingReq() +{ + assert(!m_isPrimary); + m_screen->queueGameDeviceTimingReq(); +} + +void +CScreen::resetOptions() +{ + // reset options + m_halfDuplex = 0; + + // if screen saver synchronization was off then turn it on since + // that's the default option state. + if (!m_screenSaverSync) { + m_screenSaverSync = true; + if (!m_isPrimary) { + m_screen->openScreensaver(false); + } + } + + // let screen handle its own options + m_screen->resetOptions(); +} + +void +CScreen::setOptions(const COptionsList& options) +{ + // update options + bool oldScreenSaverSync = m_screenSaverSync; + for (UInt32 i = 0, n = (UInt32)options.size(); i < n; i += 2) { + if (options[i] == kOptionScreenSaverSync) { + m_screenSaverSync = (options[i + 1] != 0); + LOG((CLOG_DEBUG1 "screen saver synchronization %s", m_screenSaverSync ? "on" : "off")); + } + else if (options[i] == kOptionHalfDuplexCapsLock) { + if (options[i + 1] != 0) { + m_halfDuplex |= KeyModifierCapsLock; + } + else { + m_halfDuplex &= ~KeyModifierCapsLock; + } + LOG((CLOG_DEBUG1 "half-duplex caps-lock %s", ((m_halfDuplex & KeyModifierCapsLock) != 0) ? "on" : "off")); + } + else if (options[i] == kOptionHalfDuplexNumLock) { + if (options[i + 1] != 0) { + m_halfDuplex |= KeyModifierNumLock; + } + else { + m_halfDuplex &= ~KeyModifierNumLock; + } + LOG((CLOG_DEBUG1 "half-duplex num-lock %s", ((m_halfDuplex & KeyModifierNumLock) != 0) ? "on" : "off")); + } + else if (options[i] == kOptionHalfDuplexScrollLock) { + if (options[i + 1] != 0) { + m_halfDuplex |= KeyModifierScrollLock; + } + else { + m_halfDuplex &= ~KeyModifierScrollLock; + } + LOG((CLOG_DEBUG1 "half-duplex scroll-lock %s", ((m_halfDuplex & KeyModifierScrollLock) != 0) ? "on" : "off")); + } + } + + // update half-duplex options + m_screen->setHalfDuplexMask(m_halfDuplex); + + // update screen saver synchronization + if (!m_isPrimary && oldScreenSaverSync != m_screenSaverSync) { + if (m_screenSaverSync) { + m_screen->openScreensaver(false); + } + else { + m_screen->closeScreensaver(); + } + } + + // let screen handle its own options + m_screen->setOptions(options); +} + +void +CScreen::setSequenceNumber(UInt32 seqNum) +{ + m_screen->setSequenceNumber(seqNum); +} + +UInt32 +CScreen::registerHotKey(KeyID key, KeyModifierMask mask) +{ + return m_screen->registerHotKey(key, mask); +} + +void +CScreen::unregisterHotKey(UInt32 id) +{ + m_screen->unregisterHotKey(id); +} + +void +CScreen::fakeInputBegin() +{ + assert(!m_fakeInput); + + m_fakeInput = true; + m_screen->fakeInputBegin(); +} + +void +CScreen::fakeInputEnd() +{ + assert(m_fakeInput); + + m_fakeInput = false; + m_screen->fakeInputEnd(); +} + +bool +CScreen::isOnScreen() const +{ + return m_entered; +} + +bool +CScreen::isLockedToScreen() const +{ + // check for pressed mouse buttons + if (m_screen->isAnyMouseButtonDown()) { + LOG((CLOG_DEBUG "locked by mouse button")); + return true; + } + + // not locked + return false; +} + +SInt32 +CScreen::getJumpZoneSize() const +{ + if (!m_isPrimary) { + return 0; + } + else { + return m_screen->getJumpZoneSize(); + } +} + +void +CScreen::getCursorCenter(SInt32& x, SInt32& y) const +{ + m_screen->getCursorCenter(x, y); +} + +KeyModifierMask +CScreen::getActiveModifiers() const +{ + return m_screen->getActiveModifiers(); +} + +KeyModifierMask +CScreen::pollActiveModifiers() const +{ + return m_screen->pollActiveModifiers(); +} + +void* +CScreen::getEventTarget() const +{ + return m_screen; +} + +bool +CScreen::getClipboard(ClipboardID id, IClipboard* clipboard) const +{ + return m_screen->getClipboard(id, clipboard); +} + +void +CScreen::getShape(SInt32& x, SInt32& y, SInt32& w, SInt32& h) const +{ + m_screen->getShape(x, y, w, h); +} + +void +CScreen::getCursorPos(SInt32& x, SInt32& y) const +{ + m_screen->getCursorPos(x, y); +} + +void +CScreen::enablePrimary() +{ + // get notified of screen saver activation/deactivation + m_screen->openScreensaver(true); + + // claim screen changed size + EVENTQUEUE->addEvent(CEvent(getShapeChangedEvent(), getEventTarget())); +} + +void +CScreen::enableSecondary() +{ + // assume primary has all clipboards + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + grabClipboard(id); + } + + // disable the screen saver if synchronization is enabled + if (m_screenSaverSync) { + m_screen->openScreensaver(false); + } +} + +void +CScreen::disablePrimary() +{ + // done with screen saver + m_screen->closeScreensaver(); +} + +void +CScreen::disableSecondary() +{ + // done with screen saver + m_screen->closeScreensaver(); +} + +void +CScreen::enterPrimary() +{ + // do nothing +} + +void +CScreen::enterSecondary(KeyModifierMask) +{ + // do nothing +} + +void +CScreen::leavePrimary() +{ + // we don't track keys while on the primary screen so update our + // idea of them now. this is particularly to update the state of + // the toggle modifiers. + m_screen->updateKeyState(); +} + +void +CScreen::leaveSecondary() +{ + // release any keys we think are still down + m_screen->fakeAllKeysUp(); +} + +void +CScreen::gameDeviceTimingResp(UInt16 freq) +{ + m_screen->gameDeviceTimingResp(freq); +} + +void +CScreen::gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2) +{ + m_screen->gameDeviceFeedback(id, m1, m2); +} diff --git a/src/lib/synergy/CScreen.h b/src/lib/synergy/CScreen.h new file mode 100644 index 00000000..dc7defcf --- /dev/null +++ b/src/lib/synergy/CScreen.h @@ -0,0 +1,344 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CSCREEN_H +#define CSCREEN_H + +#include "IScreen.h" +#include "ClipboardTypes.h" +#include "KeyTypes.h" +#include "MouseTypes.h" +#include "OptionTypes.h" +#include "GameDeviceTypes.h" + +class IClipboard; +class IPlatformScreen; + +//! Platform independent screen +/*! +This is a platform independent screen. It can work as either a +primary or secondary screen. +*/ +class CScreen : public IScreen { +public: + CScreen(IPlatformScreen* platformScreen); + virtual ~CScreen(); + + //! @name manipulators + //@{ + + //! Activate screen + /*! + Activate the screen, preparing it to report system and user events. + For a secondary screen it also means disabling the screen saver if + synchronizing it and preparing to synthesize events. + */ + void enable(); + + //! Deactivate screen + /*! + Undoes the operations in activate() and events are no longer + reported. It also releases keys that are logically pressed. + */ + void disable(); + + //! Enter screen + /*! + Called when the user navigates to this screen. \p toggleMask has the + toggle keys that should be turned on on the secondary screen. + */ + void enter(KeyModifierMask toggleMask); + + //! Leave screen + /*! + Called when the user navigates off this screen. + */ + bool leave(); + + //! Update configuration + /*! + This is called when the configuration has changed. \c activeSides + is a bitmask of EDirectionMask indicating which sides of the + primary screen are linked to clients. + */ + void reconfigure(UInt32 activeSides); + + //! Warp cursor + /*! + Warps the cursor to the absolute coordinates \c x,y. Also + discards input events up to and including the warp before + returning. + */ + void warpCursor(SInt32 x, SInt32 y); + + //! Set clipboard + /*! + Sets the system's clipboard contents. This is usually called + soon after an enter(). + */ + void setClipboard(ClipboardID, const IClipboard*); + + //! Grab clipboard + /*! + Grabs (i.e. take ownership of) the system clipboard. + */ + void grabClipboard(ClipboardID); + + //! Activate/deactivate screen saver + /*! + Forcibly activates the screen saver if \c activate is true otherwise + forcibly deactivates it. + */ + void screensaver(bool activate); + + //! Notify of key press + /*! + Synthesize key events to generate a press of key \c id. If possible + match the given modifier mask. The KeyButton identifies the physical + key on the server that generated this key down. The client must + ensure that a key up or key repeat that uses the same KeyButton will + synthesize an up or repeat for the same client key synthesized by + keyDown(). + */ + void keyDown(KeyID id, KeyModifierMask, KeyButton); + + //! Notify of key repeat + /*! + Synthesize key events to generate a press and release of key \c id + \c count times. If possible match the given modifier mask. + */ + void keyRepeat(KeyID id, KeyModifierMask, + SInt32 count, KeyButton); + + //! Notify of key release + /*! + Synthesize key events to generate a release of key \c id. If possible + match the given modifier mask. + */ + void keyUp(KeyID id, KeyModifierMask, KeyButton); + + //! Notify of mouse press + /*! + Synthesize mouse events to generate a press of mouse button \c id. + */ + void mouseDown(ButtonID id); + + //! Notify of mouse release + /*! + Synthesize mouse events to generate a release of mouse button \c id. + */ + void mouseUp(ButtonID id); + + //! Notify of mouse motion + /*! + Synthesize mouse events to generate mouse motion to the absolute + screen position \c xAbs,yAbs. + */ + void mouseMove(SInt32 xAbs, SInt32 yAbs); + + //! Notify of mouse motion + /*! + Synthesize mouse events to generate mouse motion by the relative + amount \c xRel,yRel. + */ + void mouseRelativeMove(SInt32 xRel, SInt32 yRel); + + //! Notify of mouse wheel motion + /*! + Synthesize mouse events to generate mouse wheel motion of \c xDelta + and \c yDelta. Deltas are positive for motion away from the user or + to the right and negative for motion towards the user or to the left. + Each wheel click should generate a delta of +/-120. + */ + void mouseWheel(SInt32 xDelta, SInt32 yDelta); + + //! Notify of game device buttons changed + /*! + Synthesize game device button states. + */ + virtual void gameDeviceButtons(GameDeviceID id, GameDeviceButton buttons); + + //! Notify of game device sticks changed + /*! + Synthesize game device stick states. + */ + virtual void gameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2); + + //! Notify of game device trigger changes + /*! + Synthesize game device trigger states. + */ + virtual void gameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2); + + //! Notify of game device timing request + /*! + Causes a game device timing response when state is next faked. + */ + virtual void gameDeviceTimingReq(); + + //! Notify of game device timing response + /*! + Handles a game device timing response coming back from the client. + */ + virtual void gameDeviceTimingResp(UInt16 freq); + + //! Notify of game device feedback changes + /*! + Sets the game device state with new feedback values. + */ + virtual void gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2); + + //! Notify of options changes + /*! + Resets all options to their default values. + */ + void resetOptions(); + + //! Notify of options changes + /*! + Set options to given values. Ignores unknown options and doesn't + modify options that aren't given in \c options. + */ + void setOptions(const COptionsList& options); + + //! Set clipboard sequence number + /*! + Sets the sequence number to use in subsequent clipboard events. + */ + void setSequenceNumber(UInt32); + + //! Register a system hotkey + /*! + Registers a system-wide hotkey for key \p key with modifiers \p mask. + Returns an id used to unregister the hotkey. + */ + UInt32 registerHotKey(KeyID key, KeyModifierMask mask); + + //! Unregister a system hotkey + /*! + Unregisters a previously registered hot key. + */ + void unregisterHotKey(UInt32 id); + + //! Prepare to synthesize input on primary screen + /*! + Prepares the primary screen to receive synthesized input. We do not + want to receive this synthesized input as user input so this method + ensures that we ignore it. Calls to \c fakeInputBegin() may not be + nested. + */ + void fakeInputBegin(); + + //! Done synthesizing input on primary screen + /*! + Undoes whatever \c fakeInputBegin() did. + */ + void fakeInputEnd(); + + //@} + //! @name accessors + //@{ + + //! Test if cursor on screen + /*! + Returns true iff the cursor is on the screen. + */ + bool isOnScreen() const; + + //! Get screen lock state + /*! + Returns true if there's any reason that the user should not be + allowed to leave the screen (usually because a button or key is + pressed). If this method returns true it logs a message as to + why at the CLOG_DEBUG level. + */ + bool isLockedToScreen() const; + + //! Get jump zone size + /*! + Return the jump zone size, the size of the regions on the edges of + the screen that cause the cursor to jump to another screen. + */ + SInt32 getJumpZoneSize() const; + + //! Get cursor center position + /*! + Return the cursor center position which is where we park the + cursor to compute cursor motion deltas and should be far from + the edges of the screen, typically the center. + */ + void getCursorCenter(SInt32& x, SInt32& y) const; + + //! Get the active modifiers + /*! + Returns the modifiers that are currently active according to our + shadowed state. + */ + KeyModifierMask getActiveModifiers() const; + + //! Get the active modifiers from OS + /*! + Returns the modifiers that are currently active according to the + operating system. + */ + KeyModifierMask pollActiveModifiers() const; + + //@} + + // IScreen overrides + virtual void* getEventTarget() const; + virtual bool getClipboard(ClipboardID id, IClipboard*) const; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const; + virtual void getCursorPos(SInt32& x, SInt32& y) const; + +protected: + void enablePrimary(); + void enableSecondary(); + void disablePrimary(); + void disableSecondary(); + + void enterPrimary(); + void enterSecondary(KeyModifierMask toggleMask); + void leavePrimary(); + void leaveSecondary(); + +private: + // our platform dependent screen + IPlatformScreen* m_screen; + + // true if screen is being used as a primary screen, false otherwise + bool m_isPrimary; + + // true if screen is enabled + bool m_enabled; + + // true if the cursor is on this screen + bool m_entered; + + // true if screen saver should be synchronized to server + bool m_screenSaverSync; + + // note toggle keys that toggles on up/down (false) or on + // transition (true) + KeyModifierMask m_halfDuplex; + + // true if we're faking input on a primary screen + bool m_fakeInput; +}; + +#endif diff --git a/src/lib/synergy/CServerApp.cpp b/src/lib/synergy/CServerApp.cpp new file mode 100644 index 00000000..993d64f2 --- /dev/null +++ b/src/lib/synergy/CServerApp.cpp @@ -0,0 +1,939 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CServerApp.h" +#include "CLog.h" +#include "CArch.h" +#include "XSocket.h" +#include "Version.h" +#include "IEventQueue.h" +#include "CServer.h" +#include "CClientListener.h" +#include "CClientProxy.h" +#include "TMethodEventJob.h" +#include "CServerTaskBarReceiver.h" +#include "CPrimaryClient.h" +#include "CScreen.h" +#include "CSocketMultiplexer.h" +#include "CEventQueue.h" +#include "LogOutputters.h" +#include "CFunctionEventJob.h" +#include "TMethodJob.h" +#include "CVncClient.h" + +#if SYSAPI_WIN32 +#include "CArchMiscWindows.h" +#endif + +#if WINAPI_MSWINDOWS +#include "CMSWindowsScreen.h" +#elif WINAPI_XWINDOWS +#include "CXWindowsScreen.h" +#elif WINAPI_CARBON +#include "COSXScreen.h" +#endif + +#include +#include +#include +#include "XScreen.h" +#include "CTCPSocketFactory.h" + +CEvent::Type CServerApp::s_reloadConfigEvent = CEvent::kUnknown; + +CServerApp::CServerApp(CreateTaskBarReceiverFunc createTaskBarReceiver) : +CApp(createTaskBarReceiver, new CArgs()), +s_server(NULL), +s_forceReconnectEvent(CEvent::kUnknown), +s_resetServerEvent(CEvent::kUnknown), +s_serverState(kUninitialized), +s_serverScreen(NULL), +s_primaryClient(NULL), +s_listener(NULL), +s_timer(NULL), +m_vncClient(NULL) +{ +} + +CServerApp::~CServerApp() +{ +} + +CServerApp::CArgs::CArgs() : +m_synergyAddress(NULL), +m_config(NULL) +{ +} + +CServerApp::CArgs::~CArgs() +{ +} + +bool +CServerApp::parseArg(const int& argc, const char* const* argv, int& i) +{ + if (CApp::parseArg(argc, argv, i)) { + // found common arg + return true; + } + + else if (isArg(i, argc, argv, "-a", "--address", 1)) { + // save listen address + try { + *args().m_synergyAddress = CNetworkAddress(argv[i + 1], + kDefaultPort); + args().m_synergyAddress->resolve(); + } + catch (XSocketAddress& e) { + LOG((CLOG_PRINT "%s: %s" BYE, + args().m_pname, e.what(), args().m_pname)); + m_bye(kExitArgs); + } + ++i; + } + + else if (isArg(i, argc, argv, "-c", "--config", 1)) { + // save configuration file path + args().m_configFile = argv[++i]; + } + + else { + // option not supported here + return false; + } + + // argument was valid + return true; +} + +void +CServerApp::parseArgs(int argc, const char* const* argv) +{ + // asserts values, sets defaults, and parses args + int i; + CApp::parseArgs(argc, argv, i); + + // no non-option arguments are allowed + if (i != argc) { + LOG((CLOG_PRINT "%s: unrecognized option `%s'" BYE, + args().m_pname, argv[i], args().m_pname)); + m_bye(kExitArgs); + } + + // set log filter + if (!CLOG->setFilter(args().m_logFilter)) { + LOG((CLOG_PRINT "%s: unrecognized log level `%s'" BYE, + args().m_pname, args().m_logFilter, args().m_pname)); + m_bye(kExitArgs); + } + + // identify system + LOG((CLOG_INFO "%s Server on %s %s", kAppVersion, ARCH->getOSName().c_str(), ARCH->getPlatformName().c_str())); + + loggingFilterWarning(); +} + +void +CServerApp::help() +{ + // window api args (windows/x-windows/carbon) +#if WINAPI_XWINDOWS +# define WINAPI_ARGS \ + " [--display ] [--no-xinitthreads]" +# define WINAPI_INFO \ + " --display connect to the X server at \n" \ + " --no-xinitthreads do not call XInitThreads()\n" +#else +# define WINAPI_ARGS +# define WINAPI_INFO +#endif + + char buffer[2000]; + sprintf( + buffer, + "Usage: %s" + " [--address
]" + " [--config ]" + WINAPI_ARGS + HELP_SYS_ARGS + HELP_COMMON_ARGS + "\n\n" + "Start the synergy mouse/keyboard sharing server.\n" + "\n" + " -a, --address
listen for clients on the given address.\n" + " -c, --config use the named configuration file instead.\n" + HELP_COMMON_INFO_1 + WINAPI_INFO + HELP_SYS_INFO + HELP_COMMON_INFO_2 + "\n" + "* marks defaults.\n" + "\n" + "The argument for --address is of the form: [][:]. The\n" + "hostname must be the address or hostname of an interface on the system.\n" + "The default is to listen on all interfaces. The port overrides the\n" + "default port, %d.\n" + "\n" + "If no configuration file pathname is provided then the first of the\n" + "following to load successfully sets the configuration:\n" + " %s\n" + " %s\n", + args().m_pname, kDefaultPort, + ARCH->concatPath(ARCH->getUserDirectory(), USR_CONFIG_NAME).c_str(), + ARCH->concatPath(ARCH->getSystemDirectory(), SYS_CONFIG_NAME).c_str() + ); + + std::cout << buffer << std::endl; +} + +void +CServerApp::reloadSignalHandler(CArch::ESignal, void*) +{ + EVENTQUEUE->addEvent(CEvent(getReloadConfigEvent(), + IEventQueue::getSystemTarget())); +} + +void +CServerApp::reloadConfig(const CEvent&, void*) +{ + LOG((CLOG_DEBUG "reload configuration")); + if (loadConfig(args().m_configFile)) { + if (s_server != NULL) { + s_server->setConfig(*args().m_config); + } + LOG((CLOG_NOTE "reloaded configuration")); + } +} + +void +CServerApp::loadConfig() +{ + bool loaded = false; + + // load the config file, if specified + if (!args().m_configFile.empty()) { + loaded = loadConfig(args().m_configFile); + } + + // load the default configuration if no explicit file given + else { + // get the user's home directory + CString path = ARCH->getUserDirectory(); + if (!path.empty()) { + // complete path + path = ARCH->concatPath(path, USR_CONFIG_NAME); + + // now try loading the user's configuration + if (loadConfig(path)) { + loaded = true; + args().m_configFile = path; + } + } + if (!loaded) { + // try the system-wide config file + path = ARCH->getSystemDirectory(); + if (!path.empty()) { + path = ARCH->concatPath(path, SYS_CONFIG_NAME); + if (loadConfig(path)) { + loaded = true; + args().m_configFile = path; + } + } + } + } + + if (!loaded) { + LOG((CLOG_PRINT "%s: no configuration available", args().m_pname)); + m_bye(kExitConfig); + } +} + +bool +CServerApp::loadConfig(const CString& pathname) +{ + try { + // load configuration + LOG((CLOG_DEBUG "opening configuration \"%s\"", pathname.c_str())); + std::ifstream configStream(pathname.c_str()); + if (!configStream.is_open()) { + // report failure to open configuration as a debug message + // since we try several paths and we expect some to be + // missing. + LOG((CLOG_DEBUG "cannot open configuration \"%s\"", + pathname.c_str())); + return false; + } + configStream >> *args().m_config; + LOG((CLOG_DEBUG "configuration read successfully")); + return true; + } + catch (XConfigRead& e) { + // report error in configuration file + LOG((CLOG_ERR "cannot read configuration \"%s\": %s", + pathname.c_str(), e.what())); + } + return false; +} + +CEvent::Type +CServerApp::getReloadConfigEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_reloadConfigEvent, "reloadConfig"); +} + +void +CServerApp::forceReconnect(const CEvent&, void*) +{ + if (s_server != NULL) { + s_server->disconnect(); + } +} + +CEvent::Type +CServerApp::getForceReconnectEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_forceReconnectEvent, "forceReconnect"); +} + +CEvent::Type +CServerApp::getResetServerEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_resetServerEvent, "resetServer"); +} + +void +CServerApp::handleClientConnected(const CEvent&, void* vlistener) +{ + CClientListener* listener = reinterpret_cast(vlistener); + CClientProxy* client = listener->getNextClient(); + if (client != NULL) { + s_server->adoptClient(client); + updateStatus(); + + if (args().m_enableVnc) { + // TODO: figure out client IP address from name. + CVncClient* vncClient = new CVncClient("192.168.0.13", client->getName()); + vncClient->start(); + m_vncClients.insert(std::pair(client->getName(), vncClient)); + } + } +} + +void +CServerApp::handleClientsDisconnected(const CEvent&, void*) +{ + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); +} + +void +CServerApp::closeServer(CServer* server) +{ + if (server == NULL) { + return; + } + + // tell all clients to disconnect + server->disconnect(); + + // wait for clients to disconnect for up to timeout seconds + double timeout = 3.0; + CEventQueueTimer* timer = EVENTQUEUE->newOneShotTimer(timeout, NULL); + EVENTQUEUE->adoptHandler(CEvent::kTimer, timer, + new TMethodEventJob(this, &CServerApp::handleClientsDisconnected)); + EVENTQUEUE->adoptHandler(CServer::getDisconnectedEvent(), server, + new TMethodEventJob(this, &CServerApp::handleClientsDisconnected)); + CEvent event; + EVENTQUEUE->getEvent(event); + while (event.getType() != CEvent::kQuit) { + EVENTQUEUE->dispatchEvent(event); + CEvent::deleteData(event); + EVENTQUEUE->getEvent(event); + } + EVENTQUEUE->removeHandler(CEvent::kTimer, timer); + EVENTQUEUE->deleteTimer(timer); + EVENTQUEUE->removeHandler(CServer::getDisconnectedEvent(), server); + + // done with server + delete server; +} + +void +CServerApp::stopRetryTimer() +{ + if (s_timer != NULL) { + EVENTQUEUE->deleteTimer(s_timer); + EVENTQUEUE->removeHandler(CEvent::kTimer, NULL); + s_timer = NULL; + } +} + +void +CServerApp::updateStatus() +{ + updateStatus(""); +} + +void CServerApp::updateStatus( const CString& msg ) +{ + if (m_taskBarReceiver) + { + m_taskBarReceiver->updateStatus(s_server, msg); + } +} + +void +CServerApp::closeClientListener(CClientListener* listen) +{ + if (listen != NULL) { + EVENTQUEUE->removeHandler(CClientListener::getConnectedEvent(), listen); + delete listen; + } +} + +void +CServerApp::stopServer() +{ + if (s_serverState == kStarted) { + closeClientListener(s_listener); + closeServer(s_server); + s_server = NULL; + s_listener = NULL; + s_serverState = kInitialized; + } + else if (s_serverState == kStarting) { + stopRetryTimer(); + s_serverState = kInitialized; + } + assert(s_server == NULL); + assert(s_listener == NULL); +} + +void +CServerApp::closePrimaryClient(CPrimaryClient* primaryClient) +{ + delete primaryClient; +} + +void +CServerApp::closeServerScreen(CScreen* screen) +{ + if (screen != NULL) { + EVENTQUEUE->removeHandler(IScreen::getErrorEvent(), + screen->getEventTarget()); + EVENTQUEUE->removeHandler(IScreen::getSuspendEvent(), + screen->getEventTarget()); + EVENTQUEUE->removeHandler(IScreen::getResumeEvent(), + screen->getEventTarget()); + delete screen; + } +} + +void CServerApp::cleanupServer() +{ + stopServer(); + if (s_serverState == kInitialized) { + closePrimaryClient(s_primaryClient); + closeServerScreen(s_serverScreen); + s_primaryClient = NULL; + s_serverScreen = NULL; + s_serverState = kUninitialized; + } + else if (s_serverState == kInitializing || + s_serverState == kInitializingToStart) { + stopRetryTimer(); + s_serverState = kUninitialized; + } + assert(s_primaryClient == NULL); + assert(s_serverScreen == NULL); + assert(s_serverState == kUninitialized); +} + +void +CServerApp::retryHandler(const CEvent&, void*) +{ + // discard old timer + assert(s_timer != NULL); + stopRetryTimer(); + + // try initializing/starting the server again + switch (s_serverState) { + case kUninitialized: + case kInitialized: + case kStarted: + assert(0 && "bad internal server state"); + break; + + case kInitializing: + LOG((CLOG_DEBUG1 "retry server initialization")); + s_serverState = kUninitialized; + if (!initServer()) { + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); + } + break; + + case kInitializingToStart: + LOG((CLOG_DEBUG1 "retry server initialization")); + s_serverState = kUninitialized; + if (!initServer()) { + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); + } + else if (s_serverState == kInitialized) { + LOG((CLOG_DEBUG1 "starting server")); + if (!startServer()) { + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); + } + } + break; + + case kStarting: + LOG((CLOG_DEBUG1 "retry starting server")); + s_serverState = kInitialized; + if (!startServer()) { + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); + } + break; + } +} + +bool CServerApp::initServer() +{ + // skip if already initialized or initializing + if (s_serverState != kUninitialized) { + return true; + } + + double retryTime; + CScreen* serverScreen = NULL; + CPrimaryClient* primaryClient = NULL; + try { + CString name = args().m_config->getCanonicalName(args().m_name); + serverScreen = openServerScreen(); + primaryClient = openPrimaryClient(name, serverScreen); + s_serverScreen = serverScreen; + s_primaryClient = primaryClient; + s_serverState = kInitialized; + updateStatus(); + return true; + } + catch (XScreenUnavailable& e) { + LOG((CLOG_WARN "primary screen unavailable: %s", e.what())); + closePrimaryClient(primaryClient); + closeServerScreen(serverScreen); + updateStatus(CString("primary screen unavailable: ") + e.what()); + retryTime = e.getRetryTime(); + } + catch (XScreenOpenFailure& e) { + LOG((CLOG_CRIT "failed to start server: %s", e.what())); + closePrimaryClient(primaryClient); + closeServerScreen(serverScreen); + return false; + } + catch (XBase& e) { + LOG((CLOG_CRIT "failed to start server: %s", e.what())); + closePrimaryClient(primaryClient); + closeServerScreen(serverScreen); + return false; + } + + if (args().m_restartable) { + // install a timer and handler to retry later + assert(s_timer == NULL); + LOG((CLOG_DEBUG "retry in %.0f seconds", retryTime)); + s_timer = EVENTQUEUE->newOneShotTimer(retryTime, NULL); + EVENTQUEUE->adoptHandler(CEvent::kTimer, s_timer, + new TMethodEventJob(this, &CServerApp::retryHandler)); + s_serverState = kInitializing; + return true; + } + else { + // don't try again + return false; + } +} + +CScreen* CServerApp::openServerScreen() +{ + CScreen* screen = createScreen(); + EVENTQUEUE->adoptHandler(IScreen::getErrorEvent(), + screen->getEventTarget(), + new TMethodEventJob( + this, &CServerApp::handleScreenError)); + EVENTQUEUE->adoptHandler(IScreen::getSuspendEvent(), + screen->getEventTarget(), + new TMethodEventJob( + this, &CServerApp::handleSuspend)); + EVENTQUEUE->adoptHandler(IScreen::getResumeEvent(), + screen->getEventTarget(), + new TMethodEventJob( + this, &CServerApp::handleResume)); + return screen; +} + +bool +CServerApp::startServer() +{ + // skip if already started or starting + if (s_serverState == kStarting || s_serverState == kStarted) { + return true; + } + + // initialize if necessary + if (s_serverState != kInitialized) { + if (!initServer()) { + // hard initialization failure + return false; + } + if (s_serverState == kInitializing) { + // not ready to start + s_serverState = kInitializingToStart; + return true; + } + assert(s_serverState == kInitialized); + } + + double retryTime; + CClientListener* listener = NULL; + try { + listener = openClientListener(args().m_config->getSynergyAddress()); + s_server = openServer(*args().m_config, s_primaryClient); + listener->setServer(s_server); + s_listener = listener; + updateStatus(); + LOG((CLOG_NOTE "started server, waiting for clients")); + s_serverState = kStarted; + return true; + } + catch (XSocketAddressInUse& e) { + LOG((CLOG_WARN "cannot listen for clients: %s", e.what())); + closeClientListener(listener); + updateStatus(CString("cannot listen for clients: ") + e.what()); + retryTime = 10.0; + } + catch (XBase& e) { + LOG((CLOG_CRIT "failed to start server: %s", e.what())); + closeClientListener(listener); + return false; + } + + if (args().m_restartable) { + // install a timer and handler to retry later + assert(s_timer == NULL); + LOG((CLOG_DEBUG "retry in %.0f seconds", retryTime)); + s_timer = EVENTQUEUE->newOneShotTimer(retryTime, NULL); + EVENTQUEUE->adoptHandler(CEvent::kTimer, s_timer, + new TMethodEventJob(this, &CServerApp::retryHandler)); + s_serverState = kStarting; + return true; + } + else { + // don't try again + return false; + } +} + +CScreen* +CServerApp::createScreen() +{ +#if WINAPI_MSWINDOWS + return new CScreen(new CMSWindowsScreen(true, args().m_noHooks, args().m_gameDevice)); +#elif WINAPI_XWINDOWS + return new CScreen(new CXWindowsScreen( + args().m_display, true, args().m_disableXInitThreads, 0, *EVENTQUEUE)); +#elif WINAPI_CARBON + return new CScreen(new COSXScreen(true)); +#endif +} + +CPrimaryClient* +CServerApp::openPrimaryClient(const CString& name, CScreen* screen) +{ + LOG((CLOG_DEBUG1 "creating primary screen")); + return new CPrimaryClient(name, screen); + +} + +void +CServerApp::handleScreenError(const CEvent&, void*) +{ + LOG((CLOG_CRIT "error on screen")); + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); +} + +void +CServerApp::handleSuspend(const CEvent&, void*) +{ + if (!m_suspended) { + LOG((CLOG_INFO "suspend")); + stopServer(); + m_suspended = true; + } +} + +void +CServerApp::handleResume(const CEvent&, void*) +{ + if (m_suspended) { + LOG((CLOG_INFO "resume")); + startServer(); + m_suspended = false; + } +} + +CClientListener* +CServerApp::openClientListener(const CNetworkAddress& address) +{ + CClientListener* listen = + new CClientListener(address, new CTCPSocketFactory, NULL); + EVENTQUEUE->adoptHandler(CClientListener::getConnectedEvent(), listen, + new TMethodEventJob( + this, &CServerApp::handleClientConnected, listen)); + return listen; +} + +CServer* +CServerApp::openServer(const CConfig& config, CPrimaryClient* primaryClient) +{ + CServer* server = new CServer(config, primaryClient, s_serverScreen); + + try { + EVENTQUEUE->adoptHandler( + CServer::getDisconnectedEvent(), server, + new TMethodEventJob(this, &CServerApp::handleNoClients)); + + EVENTQUEUE->adoptHandler( + CServer::getScreenSwitchedEvent(), server, + new TMethodEventJob(this, &CServerApp::handleScreenSwitched)); + + } catch (std::bad_alloc &ba) { + delete server; + throw ba; + } + + return server; +} + +void +CServerApp::handleNoClients(const CEvent&, void*) +{ + updateStatus(); +} + +void +CServerApp::handleScreenSwitched(const CEvent& e, void*) +{ + if (!args().m_enableVnc) + return; + + if (m_vncClient != NULL) { + LOG((CLOG_DEBUG "hiding vnc viewer for: %s", m_vncClient->m_screen.c_str())); + m_vncClient->hideViewer(); + } + + CServer::CSwitchToScreenInfo* info = reinterpret_cast(e.getData()); + std::map::iterator it = m_vncClients.find(info->m_screen); + if (it == m_vncClients.end()) { + LOG((CLOG_DEBUG "could not find vnc client for: %s", info->m_screen)); + return; + } + + LOG((CLOG_DEBUG "showing vnc viewer for: %s", info->m_screen)); + m_vncClient = it->second; + m_vncClient->showViewer(); +} + +int +CServerApp::mainLoop() +{ + // create socket multiplexer. this must happen after daemonization + // on unix because threads evaporate across a fork(). + CSocketMultiplexer multiplexer; + + // create the event queue + CEventQueue eventQueue; + + // if configuration has no screens then add this system + // as the default + if (args().m_config->begin() == args().m_config->end()) { + args().m_config->addScreen(args().m_name); + } + + // set the contact address, if provided, in the config. + // otherwise, if the config doesn't have an address, use + // the default. + if (args().m_synergyAddress->isValid()) { + args().m_config->setSynergyAddress(*args().m_synergyAddress); + } + else if (!args().m_config->getSynergyAddress().isValid()) { + args().m_config->setSynergyAddress(CNetworkAddress(kDefaultPort)); + } + + // canonicalize the primary screen name + CString primaryName = args().m_config->getCanonicalName(args().m_name); + if (primaryName.empty()) { + LOG((CLOG_CRIT "unknown screen name `%s'", args().m_name.c_str())); + return kExitFailed; + } + + // start server, etc + appUtil().startNode(); + + // load all available plugins. + ARCH->plugin().init(s_serverScreen->getEventTarget()); + + // handle hangup signal by reloading the server's configuration + ARCH->setSignalHandler(CArch::kHANGUP, &reloadSignalHandler, NULL); + EVENTQUEUE->adoptHandler(getReloadConfigEvent(), + IEventQueue::getSystemTarget(), + new TMethodEventJob(this, &CServerApp::reloadConfig)); + + // handle force reconnect event by disconnecting clients. they'll + // reconnect automatically. + EVENTQUEUE->adoptHandler(getForceReconnectEvent(), + IEventQueue::getSystemTarget(), + new TMethodEventJob(this, &CServerApp::forceReconnect)); + + // to work around the sticky meta keys problem, we'll give users + // the option to reset the state of synergys + EVENTQUEUE->adoptHandler(getResetServerEvent(), + IEventQueue::getSystemTarget(), + new TMethodEventJob(this, &CServerApp::resetServer)); + + // run event loop. if startServer() failed we're supposed to retry + // later. the timer installed by startServer() will take care of + // that. + CEvent event; + DAEMON_RUNNING(true); + EVENTQUEUE->getEvent(event); + while (event.getType() != CEvent::kQuit) { + EVENTQUEUE->dispatchEvent(event); + CEvent::deleteData(event); + EVENTQUEUE->getEvent(event); + } + DAEMON_RUNNING(false); + + // close down + LOG((CLOG_DEBUG1 "stopping server")); + EVENTQUEUE->removeHandler(getForceReconnectEvent(), + IEventQueue::getSystemTarget()); + EVENTQUEUE->removeHandler(getReloadConfigEvent(), + IEventQueue::getSystemTarget()); + cleanupServer(); + updateStatus(); + LOG((CLOG_NOTE "stopped server")); + + return kExitSuccess; +} + +void CServerApp::resetServer(const CEvent&, void*) +{ + LOG((CLOG_DEBUG1 "resetting server")); + stopServer(); + cleanupServer(); + startServer(); +} + +int +CServerApp::runInner(int argc, char** argv, ILogOutputter* outputter, StartupFunc startup) +{ + // general initialization + args().m_synergyAddress = new CNetworkAddress; + args().m_config = new CConfig; + args().m_pname = ARCH->getBasename(argv[0]); + + // install caller's output filter + if (outputter != NULL) { + CLOG->insert(outputter); + } + + // run + int result = startup(argc, argv); + + if (m_taskBarReceiver) + { + // done with task bar receiver + delete m_taskBarReceiver; + } + + delete args().m_config; + delete args().m_synergyAddress; + return result; +} + +int daemonMainLoopStatic(int argc, const char** argv) { + return CServerApp::instance().daemonMainLoop(argc, argv); +} + +int +CServerApp::standardStartup(int argc, char** argv) +{ + initApp(argc, argv); + + // daemonize if requested + if (args().m_daemon) { + return ARCH->daemonize(daemonName(), daemonMainLoopStatic); + } + else { + return mainLoop(); + } +} + +int +CServerApp::foregroundStartup(int argc, char** argv) +{ + initApp(argc, argv); + + // never daemonize + return mainLoop(); +} + +static +int +mainLoopStatic() +{ + return CServerApp::instance().mainLoop(); +} + +const char* +CServerApp::daemonName() const +{ +#if SYSAPI_WIN32 + return "Synergy Server"; +#elif SYSAPI_UNIX + return "synergys"; +#endif +} + +const char* +CServerApp::daemonInfo() const +{ +#if SYSAPI_WIN32 + return "Shares this computers mouse and keyboard with other computers."; +#elif SYSAPI_UNIX + return ""; +#endif +} + +void +CServerApp::startNode() +{ + // start the server. if this return false then we've failed and + // we shouldn't retry. + LOG((CLOG_DEBUG1 "starting server")); + if (!startServer()) { + m_bye(kExitFailed); + } +} diff --git a/src/lib/synergy/CServerApp.h b/src/lib/synergy/CServerApp.h new file mode 100644 index 00000000..ab432cef --- /dev/null +++ b/src/lib/synergy/CServerApp.h @@ -0,0 +1,141 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CApp.h" +#include "CString.h" +#include "CConfig.h" +#include "CNetworkAddress.h" +#include "CArch.h" +#include "IArchMultithread.h" +#include "CArgsBase.h" +#include + +enum EServerState { + kUninitialized, + kInitializing, + kInitializingToStart, + kInitialized, + kStarting, + kStarted +}; + +class CServer; +class CScreen; +class CClientListener; +class CEventQueueTimer; +class ILogOutputter; +class CVncClient; + +class CServerApp : public CApp { +public: + class CArgs : public CArgsBase { + public: + CArgs(); + ~CArgs(); + + public: + CString m_configFile; + CNetworkAddress* m_synergyAddress; + CConfig* m_config; + }; + + CServerApp(CreateTaskBarReceiverFunc createTaskBarReceiver); + virtual ~CServerApp(); + + // Parse server specific command line arguments. + void parseArgs(int argc, const char* const* argv); + + // Prints help specific to server. + void help(); + + // Returns arguments that are common and for server. + CArgs& args() const { return (CArgs&)argsBase(); } + + const char* daemonName() const; + const char* daemonInfo() const; + + // TODO: Document these functions. + static void reloadSignalHandler(CArch::ESignal, void*); + static CEvent::Type getReloadConfigEvent(); + + void reloadConfig(const CEvent&, void*); + void loadConfig(); + bool loadConfig(const CString& pathname); + void forceReconnect(const CEvent&, void*); + CEvent::Type getForceReconnectEvent(); + void resetServer(const CEvent&, void*); + CEvent::Type getResetServerEvent(); + void handleClientConnected(const CEvent&, void* vlistener); + void handleClientsDisconnected(const CEvent&, void*); + void closeServer(CServer* server); + void stopRetryTimer(); + void updateStatus(); + void updateStatus(const CString& msg); + void closeClientListener(CClientListener* listen); + void stopServer(); + void closePrimaryClient(CPrimaryClient* primaryClient); + void closeServerScreen(CScreen* screen); + void cleanupServer(); + bool initServer(); + void retryHandler(const CEvent&, void*); + CScreen* openServerScreen(); + CScreen* createScreen(); + CPrimaryClient* openPrimaryClient(const CString& name, CScreen* screen); + void handleScreenError(const CEvent&, void*); + void handleSuspend(const CEvent&, void*); + void handleResume(const CEvent&, void*); + CClientListener* openClientListener(const CNetworkAddress& address); + CServer* openServer(const CConfig& config, CPrimaryClient* primaryClient); + void handleNoClients(const CEvent&, void*); + bool startServer(); + int mainLoop(); + int runInner(int argc, char** argv, ILogOutputter* outputter, StartupFunc startup); + int standardStartup(int argc, char** argv); + int foregroundStartup(int argc, char** argv); + void startNode(); + + static CServerApp& instance() { return (CServerApp&)CApp::instance(); } + + // TODO: change s_ to m_ + CServer* s_server; + static CEvent::Type s_reloadConfigEvent; + CEvent::Type s_forceReconnectEvent; + CEvent::Type s_resetServerEvent; + EServerState s_serverState; + CScreen* s_serverScreen; + CPrimaryClient* s_primaryClient; + CClientListener* s_listener; + CEventQueueTimer* s_timer; + +private: + virtual bool parseArg(const int& argc, const char* const* argv, int& i); + void vncThread(void*); + void handleScreenSwitched(const CEvent&, void* data); + std::map m_vncClients; + CVncClient* m_vncClient; +}; + +// configuration file name +#if SYSAPI_WIN32 +#define USR_CONFIG_NAME "synergy.sgc" +#define SYS_CONFIG_NAME "synergy.sgc" +#elif SYSAPI_UNIX +#define USR_CONFIG_NAME ".synergy.conf" +#define SYS_CONFIG_NAME "synergy.conf" +#endif diff --git a/src/lib/synergy/CServerTaskBarReceiver.cpp b/src/lib/synergy/CServerTaskBarReceiver.cpp new file mode 100644 index 00000000..ca19af73 --- /dev/null +++ b/src/lib/synergy/CServerTaskBarReceiver.cpp @@ -0,0 +1,154 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "CServerTaskBarReceiver.h" +#include "CServer.h" +#include "CLock.h" +#include "CStringUtil.h" +#include "IEventQueue.h" +#include "CArch.h" +#include "Version.h" + +// +// CServerTaskBarReceiver +// + +CServerTaskBarReceiver::CServerTaskBarReceiver() : + m_state(kNotRunning) +{ + // do nothing +} + +CServerTaskBarReceiver::~CServerTaskBarReceiver() +{ + // do nothing +} + +void +CServerTaskBarReceiver::updateStatus(CServer* server, const CString& errorMsg) +{ + { + // update our status + m_errorMessage = errorMsg; + if (server == NULL) { + if (m_errorMessage.empty()) { + m_state = kNotRunning; + } + else { + m_state = kNotWorking; + } + } + else { + m_clients.clear(); + server->getClients(m_clients); + if (m_clients.size() <= 1) { + m_state = kNotConnected; + } + else { + m_state = kConnected; + } + } + + // let subclasses have a go + onStatusChanged(server); + } + + // tell task bar + ARCH->updateReceiver(this); +} + +CServerTaskBarReceiver::EState +CServerTaskBarReceiver::getStatus() const +{ + return m_state; +} + +const CString& +CServerTaskBarReceiver::getErrorMessage() const +{ + return m_errorMessage; +} + +const CServerTaskBarReceiver::CClients& +CServerTaskBarReceiver::getClients() const +{ + return m_clients; +} + +void +CServerTaskBarReceiver::quit() +{ + EVENTQUEUE->addEvent(CEvent(CEvent::kQuit)); +} + +void +CServerTaskBarReceiver::onStatusChanged(CServer*) +{ + // do nothing +} + +void +CServerTaskBarReceiver::lock() const +{ + // do nothing +} + +void +CServerTaskBarReceiver::unlock() const +{ + // do nothing +} + +std::string +CServerTaskBarReceiver::getToolTip() const +{ + switch (m_state) { + case kNotRunning: + return CStringUtil::print("%s: Not running", kAppVersion); + + case kNotWorking: + return CStringUtil::print("%s: %s", + kAppVersion, m_errorMessage.c_str()); + + case kNotConnected: + return CStringUtil::print("%s: Waiting for clients", kAppVersion); + + case kConnected: + return CStringUtil::print("%s: Connected", kAppVersion); + + default: + return ""; + } +} + +CEvent::Type +CServerTaskBarReceiver::getReloadConfigEvent() +{ + return CServerApp::instance().getReloadConfigEvent(); +} + +CEvent::Type +CServerTaskBarReceiver::getForceReconnectEvent() +{ + return CServerApp::instance().getForceReconnectEvent(); +} + +CEvent::Type +CServerTaskBarReceiver::getResetServerEvent() +{ + return CServerApp::instance().getResetServerEvent(); +} diff --git a/src/lib/synergy/CServerTaskBarReceiver.h b/src/lib/synergy/CServerTaskBarReceiver.h new file mode 100644 index 00000000..9bfb5cc7 --- /dev/null +++ b/src/lib/synergy/CServerTaskBarReceiver.h @@ -0,0 +1,101 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CSERVERTASKBARRECEIVER_H +#define CSERVERTASKBARRECEIVER_H + +#include "CString.h" +#include "IArchTaskBarReceiver.h" +#include "stdvector.h" +#include "CEvent.h" +#include "CServerApp.h" +#include "CServer.h" + +//! Implementation of IArchTaskBarReceiver for the synergy server +class CServerTaskBarReceiver : public IArchTaskBarReceiver { +public: + CServerTaskBarReceiver(); + virtual ~CServerTaskBarReceiver(); + + //! @name manipulators + //@{ + + //! Update status + /*! + Determine the status and query required information from the server. + */ + void updateStatus(CServer*, const CString& errorMsg); + + void updateStatus(INode* n, const CString& errorMsg) { updateStatus((CServer*)n, errorMsg); } + + //@} + + // IArchTaskBarReceiver overrides + virtual void showStatus() = 0; + virtual void runMenu(int x, int y) = 0; + virtual void primaryAction() = 0; + virtual void lock() const; + virtual void unlock() const; + virtual const Icon getIcon() const = 0; + virtual std::string getToolTip() const; + +protected: + typedef std::vector CClients; + enum EState { + kNotRunning, + kNotWorking, + kNotConnected, + kConnected, + kMaxState + }; + + //! Get status + EState getStatus() const; + + //! Get error message + const CString& getErrorMessage() const; + + //! Get connected clients + const CClients& getClients() const; + + //! Quit app + /*! + Causes the application to quit gracefully + */ + void quit(); + + //! Status change notification + /*! + Called when status changes. The default implementation does + nothing. + */ + virtual void onStatusChanged(CServer* server); + +protected: + CEvent::Type getReloadConfigEvent(); + CEvent::Type getForceReconnectEvent(); + CEvent::Type getResetServerEvent(); + +private: + EState m_state; + CString m_errorMessage; + CClients m_clients; +}; + +IArchTaskBarReceiver* createTaskBarReceiver(const CBufferedLogOutputter* logBuffer); + +#endif diff --git a/src/lib/synergy/CVncClient.cpp b/src/lib/synergy/CVncClient.cpp new file mode 100644 index 00000000..02ef939c --- /dev/null +++ b/src/lib/synergy/CVncClient.cpp @@ -0,0 +1,71 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "CVncClient.h" +#include "CThread.h" +#include "TMethodJob.h" + +#if VNC_SUPPORT +#include "vnc/win/vncviewer/vncviewer.h" +#include "vnc/win/vncviewer/CConn.h" +#include "vnc/win/vncviewer/CConnThread.h" +#endif + +CVncClient::CVncClient(const char* hostInfo, const std::string& screen) : +m_hostInfo(hostInfo), +m_screen(screen), +m_thread(NULL), +m_connThread(NULL) +{ +} + +CVncClient::~CVncClient() +{ + if (m_thread) + delete m_thread; +} + +void +CVncClient::thread(void*) +{ +#if VNC_SUPPORT + vncClientMain(this); +#endif +} + +void +CVncClient::start() +{ + m_thread = new CThread(new TMethodJob( + this, &CVncClient::thread, NULL)); +} + +void +CVncClient::showViewer() +{ +#if VNC_SUPPORT + m_connThread->connRef->showViewer(); +#endif +} + +void +CVncClient::hideViewer() +{ +#if VNC_SUPPORT + m_connThread->connRef->hideViewer(); +#endif +} diff --git a/src/lib/synergy/CVncClient.h b/src/lib/synergy/CVncClient.h new file mode 100644 index 00000000..da9c607e --- /dev/null +++ b/src/lib/synergy/CVncClient.h @@ -0,0 +1,39 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 + +class CThread; +namespace rfb { namespace win32 { class CConnThread; } } + +class CVncClient { +public: + CVncClient(const char* hostInfo, const std::string& screen); + virtual ~CVncClient(); + void thread(void*); + void start(); + void showViewer(); + void hideViewer(); + std::string m_screen; +public: + const char* m_hostInfo; + rfb::win32::CConnThread* m_connThread; +private: + CThread* m_thread; +}; diff --git a/src/lib/synergy/ClipboardTypes.h b/src/lib/synergy/ClipboardTypes.h new file mode 100644 index 00000000..43ecf408 --- /dev/null +++ b/src/lib/synergy/ClipboardTypes.h @@ -0,0 +1,44 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CLIPBOARDTYPES_H +#define CLIPBOARDTYPES_H + +#include "BasicTypes.h" + +//! Clipboard ID +/*! +Type to hold a clipboard identifier. +*/ +typedef UInt8 ClipboardID; + +//! @name Clipboard identifiers +//@{ +// clipboard identifiers. kClipboardClipboard is what is normally +// considered the clipboard (e.g. the cut/copy/paste menu items +// affect it). kClipboardSelection is the selection on those +// platforms that can treat the selection as a clipboard (e.g. X +// windows). clipboard identifiers must be sequential starting +// at zero. +static const ClipboardID kClipboardClipboard = 0; +static const ClipboardID kClipboardSelection = 1; + +// the number of clipboards (i.e. one greater than the last clipboard id) +static const ClipboardID kClipboardEnd = 2; +//@} + +#endif diff --git a/src/lib/synergy/GameDeviceTypes.h b/src/lib/synergy/GameDeviceTypes.h new file mode 100644 index 00000000..c8bd3f13 --- /dev/null +++ b/src/lib/synergy/GameDeviceTypes.h @@ -0,0 +1,52 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "BasicTypes.h" + +//! Game device ID +/*! +Type to hold a game device ID. +*/ +typedef UInt8 GameDeviceID; + +//! Game device button +/*! +Type to hold a game device button. +*/ +typedef UInt16 GameDeviceButton; + +//! @name Game device buttons +//@{ +static const GameDeviceButton kGameDeviceDpadUp = 0x0001; +static const GameDeviceButton kGameDeviceDpadDown = 0x0002; +static const GameDeviceButton kGameDeviceDpadLeft = 0x0004; +static const GameDeviceButton kGameDeviceDpadRight = 0x0008; +static const GameDeviceButton kGameDeviceButtonStart = 0x0010; +static const GameDeviceButton kGameDeviceButtonBack = 0x0020; +static const GameDeviceButton kGameDeviceThumb1 = 0x0040; +static const GameDeviceButton kGameDeviceThumb2 = 0x0080; +static const GameDeviceButton kGameDeviceShoulder1 = 0x0100; +static const GameDeviceButton kGameDeviceShoulder2 = 0x0200; +static const GameDeviceButton kGameDeviceButton1 = 0x1000; +static const GameDeviceButton kGameDeviceButton2 = 0x2000; +static const GameDeviceButton kGameDeviceButton3 = 0x4000; +static const GameDeviceButton kGameDeviceButton4 = 0x8000; +//@} + +static const UInt8 NumGameDeviceButtons = 14; diff --git a/src/lib/synergy/Global.h b/src/lib/synergy/Global.h new file mode 100644 index 00000000..6da57d61 --- /dev/null +++ b/src/lib/synergy/Global.h @@ -0,0 +1,28 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef GLOBAL_H +#define GLOBAL_H + +// Makes everything public for unit tests +#ifdef TEST_ENV +#define protected public +#define private public +#endif + +#endif + diff --git a/src/lib/synergy/IApp.h b/src/lib/synergy/IApp.h new file mode 100644 index 00000000..747a6f61 --- /dev/null +++ b/src/lib/synergy/IApp.h @@ -0,0 +1,47 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "IInterface.h" + +typedef int (*StartupFunc)(int, char**); + +class ILogOutputter; +class CArgsBase; +class IArchTaskBarReceiver; +class CScreen; + +class IApp : public IInterface +{ +public: + virtual void setByeFunc(void(*bye)(int)) = 0; + virtual bool isArg(int argi, int argc, const char* const* argv, + const char* name1, const char* name2, + int minRequiredParameters = 0) = 0; + virtual CArgsBase& argsBase() const = 0; + virtual int standardStartup(int argc, char** argv) = 0; + virtual int runInner(int argc, char** argv, ILogOutputter* outputter, StartupFunc startup) = 0; + virtual void startNode() = 0; + virtual IArchTaskBarReceiver* taskBarReceiver() const = 0; + virtual void bye(int error) = 0; + virtual int mainLoop() = 0; + virtual void initApp(int argc, const char** argv) = 0; + virtual const char* daemonName() const = 0; + virtual int foregroundStartup(int argc, char** argv) = 0; + virtual CScreen* createScreen() = 0; +}; diff --git a/src/lib/synergy/IAppUtil.h b/src/lib/synergy/IAppUtil.h new file mode 100644 index 00000000..ec9c0046 --- /dev/null +++ b/src/lib/synergy/IAppUtil.h @@ -0,0 +1,31 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IInterface.h" +#include "IApp.h" + +class IAppUtil : public IInterface { +public: + virtual bool parseArg(const int& argc, const char* const* argv, int& i) = 0; + virtual void adoptApp(IApp* app) = 0; + virtual IApp& app() const = 0; + virtual int run(int argc, char** argv) = 0; + virtual void beforeAppExit() = 0; + virtual void startNode() = 0; +}; diff --git a/src/lib/synergy/IClient.h b/src/lib/synergy/IClient.h new file mode 100644 index 00000000..eddfdc1f --- /dev/null +++ b/src/lib/synergy/IClient.h @@ -0,0 +1,203 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ICLIENT_H +#define ICLIENT_H + +#include "IScreen.h" +#include "ClipboardTypes.h" +#include "KeyTypes.h" +#include "MouseTypes.h" +#include "OptionTypes.h" +#include "CString.h" +#include "GameDeviceTypes.h" + +//! Client interface +/*! +This interface defines the methods necessary for the server to +communicate with a client. +*/ +class IClient : public IScreen { +public: + //! @name manipulators + //@{ + + //! Enter screen + /*! + Enter the screen. The cursor should be warped to \p xAbs,yAbs. + \p mask is the expected toggle button state and the client should + update its state to match. \p forScreensaver is true iff the + screen is being entered because the screen saver is starting. + Subsequent clipboard events should report \p seqNum. + */ + virtual void enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, + bool forScreensaver) = 0; + + //! Leave screen + /*! + Leave the screen. Return false iff the user may not leave the + client's screen (because, for example, a button is down). + */ + virtual bool leave() = 0; + + //! Set clipboard + /*! + Update the client's clipboard. This implies that the client's + clipboard is now up to date. If the client's clipboard was + already known to be up to date then this may do nothing. \c data + has marshalled clipboard data. + */ + virtual void setClipboard(ClipboardID, const IClipboard*) = 0; + + //! Grab clipboard + /*! + Grab (i.e. take ownership of) the client's clipboard. Since this + is called when another client takes ownership of the clipboard it + implies that the client's clipboard is out of date. + */ + virtual void grabClipboard(ClipboardID) = 0; + + //! Mark clipboard dirty + /*! + Mark the client's clipboard as dirty (out of date) or clean (up to + date). + */ + virtual void setClipboardDirty(ClipboardID, bool dirty) = 0; + + //! Notify of key press + /*! + Synthesize key events to generate a press of key \c id. If possible + match the given modifier mask. The KeyButton identifies the physical + key on the server that generated this key down. The client must + ensure that a key up or key repeat that uses the same KeyButton will + synthesize an up or repeat for the same client key synthesized by + keyDown(). + */ + virtual void keyDown(KeyID id, KeyModifierMask, KeyButton) = 0; + + //! Notify of key repeat + /*! + Synthesize key events to generate a press and release of key \c id + \c count times. If possible match the given modifier mask. + */ + virtual void keyRepeat(KeyID id, KeyModifierMask, + SInt32 count, KeyButton) = 0; + + //! Notify of key release + /*! + Synthesize key events to generate a release of key \c id. If possible + match the given modifier mask. + */ + virtual void keyUp(KeyID id, KeyModifierMask, KeyButton) = 0; + + //! Notify of mouse press + /*! + Synthesize mouse events to generate a press of mouse button \c id. + */ + virtual void mouseDown(ButtonID id) = 0; + + //! Notify of mouse release + /*! + Synthesize mouse events to generate a release of mouse button \c id. + */ + virtual void mouseUp(ButtonID id) = 0; + + //! Notify of mouse motion + /*! + Synthesize mouse events to generate mouse motion to the absolute + screen position \c xAbs,yAbs. + */ + virtual void mouseMove(SInt32 xAbs, SInt32 yAbs) = 0; + + //! Notify of mouse motion + /*! + Synthesize mouse events to generate mouse motion by the relative + amount \c xRel,yRel. + */ + virtual void mouseRelativeMove(SInt32 xRel, SInt32 yRel) = 0; + + //! Notify of mouse wheel motion + /*! + Synthesize mouse events to generate mouse wheel motion of \c xDelta + and \c yDelta. Deltas are positive for motion away from the user or + to the right and negative for motion towards the user or to the left. + Each wheel click should generate a delta of +/-120. + */ + virtual void mouseWheel(SInt32 xDelta, SInt32 yDelta) = 0; + + //! Notify of game device buttons changed + /*! + Synthesize game device button states. + */ + virtual void gameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) = 0; + + //! Notify of game device sticks changed + /*! + Synthesize game device stick states. + */ + virtual void gameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) = 0; + + //! Notify of game device trigger changes + /*! + Synthesize game device trigger states. + */ + virtual void gameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) = 0; + + //! Notify of game device timing request + /*! + Causes a game device timing response when state is next faked. + */ + virtual void gameDeviceTimingReq() = 0; + + //! Notify of screen saver change + virtual void screensaver(bool activate) = 0; + + //! Notify of options changes + /*! + Reset all options to their default values. + */ + virtual void resetOptions() = 0; + + //! Notify of options changes + /*! + Set options to given values. Ignore unknown options and don't + modify our options that aren't given in \c options. + */ + virtual void setOptions(const COptionsList& options) = 0; + + //@} + //! @name accessors + //@{ + + //! Get client name + /*! + Return the client's name. + */ + virtual CString getName() const = 0; + + //@} + + // IScreen overrides + virtual void* getEventTarget() const = 0; + virtual bool getClipboard(ClipboardID id, IClipboard*) const = 0; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const = 0; + virtual void getCursorPos(SInt32& x, SInt32& y) const = 0; +}; + +#endif diff --git a/src/lib/synergy/IClipboard.cpp b/src/lib/synergy/IClipboard.cpp new file mode 100644 index 00000000..d130ad07 --- /dev/null +++ b/src/lib/synergy/IClipboard.cpp @@ -0,0 +1,158 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IClipboard.h" +#include "stdvector.h" + +// +// IClipboard +// + +void +IClipboard::unmarshall(IClipboard* clipboard, const CString& data, Time time) +{ + assert(clipboard != NULL); + + const char* index = data.data(); + + // clear existing data + clipboard->open(time); + clipboard->empty(); + + // read the number of formats + const UInt32 numFormats = readUInt32(index); + index += 4; + + // read each format + for (UInt32 i = 0; i < numFormats; ++i) { + // get the format id + IClipboard::EFormat format = + static_cast(readUInt32(index)); + index += 4; + + // get the size of the format data + UInt32 size = readUInt32(index); + index += 4; + + // save the data if it's a known format. if either the client + // or server supports more clipboard formats than the other + // then one of them will get a format >= kNumFormats here. + if (format add(format, CString(index, size)); + } + index += size; + } + + // done + clipboard->close(); +} + +CString +IClipboard::marshall(const IClipboard* clipboard) +{ + assert(clipboard != NULL); + + CString data; + + std::vector formatData; + formatData.resize(IClipboard::kNumFormats); + // FIXME -- use current time + clipboard->open(0); + + // compute size of marshalled data + UInt32 size = 4; + UInt32 numFormats = 0; + for (UInt32 format = 0; format != IClipboard::kNumFormats; ++format) { + if (clipboard->has(static_cast(format))) { + ++numFormats; + formatData[format] = + clipboard->get(static_cast(format)); + size += 4 + 4 + (UInt32)formatData[format].size(); + } + } + + // allocate space + data.reserve(size); + + // marshall the data + writeUInt32(&data, numFormats); + for (UInt32 format = 0; format != IClipboard::kNumFormats; ++format) { + if (clipboard->has(static_cast(format))) { + writeUInt32(&data, format); + writeUInt32(&data, (UInt32)formatData[format].size()); + data += formatData[format]; + } + } + clipboard->close(); + + return data; +} + +bool +IClipboard::copy(IClipboard* dst, const IClipboard* src) +{ + assert(dst != NULL); + assert(src != NULL); + + return copy(dst, src, src->getTime()); +} + +bool +IClipboard::copy(IClipboard* dst, const IClipboard* src, Time time) +{ + assert(dst != NULL); + assert(src != NULL); + + bool success = false; + if (src->open(time)) { + if (dst->open(time)) { + if (dst->empty()) { + for (SInt32 format = 0; + format != IClipboard::kNumFormats; ++format) { + IClipboard::EFormat eFormat = (IClipboard::EFormat)format; + if (src->has(eFormat)) { + dst->add(eFormat, src->get(eFormat)); + } + } + success = true; + } + dst->close(); + } + src->close(); + } + + return success; +} + +UInt32 +IClipboard::readUInt32(const char* buf) +{ + const unsigned char* ubuf = reinterpret_cast(buf); + return (static_cast(ubuf[0]) << 24) | + (static_cast(ubuf[1]) << 16) | + (static_cast(ubuf[2]) << 8) | + static_cast(ubuf[3]); +} + +void +IClipboard::writeUInt32(CString* buf, UInt32 v) +{ + *buf += static_cast((v >> 24) & 0xff); + *buf += static_cast((v >> 16) & 0xff); + *buf += static_cast((v >> 8) & 0xff); + *buf += static_cast( v & 0xff); +} diff --git a/src/lib/synergy/IClipboard.h b/src/lib/synergy/IClipboard.h new file mode 100644 index 00000000..037962db --- /dev/null +++ b/src/lib/synergy/IClipboard.h @@ -0,0 +1,171 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ICLIPBOARD_H +#define ICLIPBOARD_H + +#include "IInterface.h" +#include "CString.h" +#include "BasicTypes.h" + +//! Clipboard interface +/*! +This interface defines the methods common to all clipboards. +*/ +class IClipboard : public IInterface { +public: + //! Timestamp type + /*! + Timestamp type. Timestamps are in milliseconds from some + arbitrary starting time. Timestamps will wrap around to 0 + after about 49 3/4 days. + */ + typedef UInt32 Time; + + //! Clipboard formats + /*! + The list of known clipboard formats. kNumFormats must be last and + formats must be sequential starting from zero. Clipboard data set + via add() and retrieved via get() must be in one of these formats. + Platform dependent clipboard subclasses can and should present any + suitable formats derivable from these formats. + + \c kText is a text format encoded in UTF-8. Newlines are LF (not + CR or LF/CR). + + \c kBitmap is an image format. The data is a BMP file without the + 14 byte header (i.e. starting at the INFOHEADER) and with the image + data immediately following the 40 byte INFOHEADER. + + \c kHTML is a text format encoded in UTF-8 and containing a valid + HTML fragment (but not necessarily a complete HTML document). + Newlines are LF. + */ + enum EFormat { + kText, //!< Text format, UTF-8, newline is LF + kBitmap, //!< Bitmap format, BMP 24/32bpp, BI_RGB + kHTML, //!< HTML format, HTML fragment, UTF-8, newline is LF + kNumFormats //!< The number of clipboard formats + }; + + //! @name manipulators + //@{ + + //! Empty clipboard + /*! + Take ownership of the clipboard and clear all data from it. + This must be called between a successful open() and close(). + Return false if the clipboard ownership could not be taken; + the clipboard should not be emptied in this case. + */ + virtual bool empty() = 0; + + //! Add data + /*! + Add data in the given format to the clipboard. May only be + called after a successful empty(). + */ + virtual void add(EFormat, const CString& data) = 0; + + //@} + //! @name accessors + //@{ + + //! Open clipboard + /*! + Open the clipboard. Return true iff the clipboard could be + opened. If open() returns true then the client must call + close() at some later time; if it returns false then close() + must not be called. \c time should be the current time or + a time in the past when the open should effectively have taken + place. + */ + virtual bool open(Time time) const = 0; + + //! Close clipboard + /*! + Close the clipboard. close() must match a preceding successful + open(). This signals that the clipboard has been filled with + all the necessary data or all data has been read. It does not + mean the clipboard ownership should be released (if it was + taken). + */ + virtual void close() const = 0; + + //! Get time + /*! + Return the timestamp passed to the last successful open(). + */ + virtual Time getTime() const = 0; + + //! Check for data + /*! + Return true iff the clipboard contains data in the given + format. Must be called between a successful open() and close(). + */ + virtual bool has(EFormat) const = 0; + + //! Get data + /*! + Return the data in the given format. Returns the empty string + if there is no data in that format. Must be called between + a successful open() and close(). + */ + virtual CString get(EFormat) const = 0; + + //! Marshall clipboard data + /*! + Merge \p clipboard's data into a single buffer that can be later + unmarshalled to restore the clipboard and return the buffer. + */ + static CString marshall(const IClipboard* clipboard); + + //! Unmarshall clipboard data + /*! + Extract marshalled clipboard data and store it in \p clipboard. + Sets the clipboard time to \c time. + */ + static void unmarshall(IClipboard* clipboard, + const CString& data, Time time); + + //! Copy clipboard + /*! + Transfers all the data in one clipboard to another. The + clipboards can be of any concrete clipboard type (and + they don't have to be the same type). This also sets + the destination clipboard's timestamp to source clipboard's + timestamp. Returns true iff the copy succeeded. + */ + static bool copy(IClipboard* dst, const IClipboard* src); + + //! Copy clipboard + /*! + Transfers all the data in one clipboard to another. The + clipboards can be of any concrete clipboard type (and they + don't have to be the same type). This also sets the + timestamp to \c time. Returns true iff the copy succeeded. + */ + static bool copy(IClipboard* dst, const IClipboard* src, Time); + + //@} + +private: + static UInt32 readUInt32(const char*); + static void writeUInt32(CString*, UInt32); +}; + +#endif diff --git a/src/lib/synergy/IKeyState.cpp b/src/lib/synergy/IKeyState.cpp new file mode 100644 index 00000000..3d9efac9 --- /dev/null +++ b/src/lib/synergy/IKeyState.cpp @@ -0,0 +1,190 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IKeyState.h" +#include "CEventQueue.h" +#include +#include + +// +// IKeyState +// + +CEvent::Type IKeyState::s_keyDownEvent = CEvent::kUnknown; +CEvent::Type IKeyState::s_keyUpEvent = CEvent::kUnknown; +CEvent::Type IKeyState::s_keyRepeatEvent = CEvent::kUnknown; + +IKeyState::IKeyState() : + m_eventQueue(*EVENTQUEUE) +{ +} + +IKeyState::IKeyState(IEventQueue& eventQueue) : + m_eventQueue(eventQueue) +{ +} + +CEvent::Type +IKeyState::getKeyDownEvent(IEventQueue& eventQueue) +{ + return eventQueue.registerTypeOnce(s_keyDownEvent, + "IKeyState::keyDown"); +} + +CEvent::Type +IKeyState::getKeyUpEvent(IEventQueue& eventQueue) +{ + return eventQueue.registerTypeOnce(s_keyUpEvent, + "IKeyState::keyUp"); +} + +CEvent::Type +IKeyState::getKeyRepeatEvent(IEventQueue& eventQueue) +{ + return eventQueue.registerTypeOnce(s_keyRepeatEvent, + "IKeyState::keyRepeat"); +} + +// +// IKeyState::CKeyInfo +// + +IKeyState::CKeyInfo* +IKeyState::CKeyInfo::alloc(KeyID id, + KeyModifierMask mask, KeyButton button, SInt32 count) +{ + CKeyInfo* info = (CKeyInfo*)malloc(sizeof(CKeyInfo)); + info->m_key = id; + info->m_mask = mask; + info->m_button = button; + info->m_count = count; + info->m_screens = NULL; + info->m_screensBuffer[0] = '\0'; + return info; +} + +IKeyState::CKeyInfo* +IKeyState::CKeyInfo::alloc(KeyID id, + KeyModifierMask mask, KeyButton button, SInt32 count, + const std::set& destinations) +{ + CString screens = join(destinations); + + // build structure + CKeyInfo* info = (CKeyInfo*)malloc(sizeof(CKeyInfo) + screens.size()); + info->m_key = id; + info->m_mask = mask; + info->m_button = button; + info->m_count = count; + info->m_screens = info->m_screensBuffer; + strcpy(info->m_screensBuffer, screens.c_str()); + return info; +} + +IKeyState::CKeyInfo* +IKeyState::CKeyInfo::alloc(const CKeyInfo& x) +{ + CKeyInfo* info = (CKeyInfo*)malloc(sizeof(CKeyInfo) + + strlen(x.m_screensBuffer)); + info->m_key = x.m_key; + info->m_mask = x.m_mask; + info->m_button = x.m_button; + info->m_count = x.m_count; + info->m_screens = x.m_screens ? info->m_screensBuffer : NULL; + strcpy(info->m_screensBuffer, x.m_screensBuffer); + return info; +} + +bool +IKeyState::CKeyInfo::isDefault(const char* screens) +{ + return (screens == NULL || screens[0] == '\0'); +} + +bool +IKeyState::CKeyInfo::contains(const char* screens, const CString& name) +{ + // special cases + if (isDefault(screens)) { + return false; + } + if (screens[0] == '*') { + return true; + } + + // search + CString match; + match.reserve(name.size() + 2); + match += ":"; + match += name; + match += ":"; + return (strstr(screens, match.c_str()) != NULL); +} + +bool +IKeyState::CKeyInfo::equal(const CKeyInfo* a, const CKeyInfo* b) +{ + return (a->m_key == b->m_key && + a->m_mask == b->m_mask && + a->m_button == b->m_button && + a->m_count == b->m_count && + strcmp(a->m_screensBuffer, b->m_screensBuffer) == 0); +} + +CString +IKeyState::CKeyInfo::join(const std::set& destinations) +{ + // collect destinations into a string. names are surrounded by ':' + // which makes searching easy. the string is empty if there are no + // destinations and "*" means all destinations. + CString screens; + for (std::set::const_iterator i = destinations.begin(); + i != destinations.end(); ++i) { + if (*i == "*") { + screens = "*"; + break; + } + else { + if (screens.empty()) { + screens = ":"; + } + screens += *i; + screens += ":"; + } + } + return screens; +} + +void +IKeyState::CKeyInfo::split(const char* screens, std::set& dst) +{ + dst.clear(); + if (isDefault(screens)) { + return; + } + if (screens[0] == '*') { + dst.insert("*"); + return; + } + + const char* i = screens + 1; + while (*i != '\0') { + const char* j = strchr(i, ':'); + dst.insert(CString(i, j - i)); + i = j + 1; + } +} diff --git a/src/lib/synergy/IKeyState.h b/src/lib/synergy/IKeyState.h new file mode 100644 index 00000000..2966da84 --- /dev/null +++ b/src/lib/synergy/IKeyState.h @@ -0,0 +1,196 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IKEYSTATE_H +#define IKEYSTATE_H + +#include "IInterface.h" +#include "KeyTypes.h" +#include "CEvent.h" +#include "CString.h" +#include "stdset.h" +#include "IEventQueue.h" + +//! Key state interface +/*! +This interface provides access to set and query the keyboard state and +to synthesize key events. +*/ +class IKeyState : public IInterface { +public: + IKeyState(); + IKeyState(IEventQueue& eventQueue); + + enum { + kNumButtons = 0x200 + }; + + //! Key event data + class CKeyInfo { + public: + static CKeyInfo* alloc(KeyID, KeyModifierMask, KeyButton, SInt32 count); + static CKeyInfo* alloc(KeyID, KeyModifierMask, KeyButton, SInt32 count, + const std::set& destinations); + static CKeyInfo* alloc(const CKeyInfo&); + + static bool isDefault(const char* screens); + static bool contains(const char* screens, const CString& name); + static bool equal(const CKeyInfo*, const CKeyInfo*); + static CString join(const std::set& destinations); + static void split(const char* screens, std::set&); + + public: + KeyID m_key; + KeyModifierMask m_mask; + KeyButton m_button; + SInt32 m_count; + char* m_screens; + char m_screensBuffer[1]; + }; + + typedef std::set KeyButtonSet; + + //! @name manipulators + //@{ + + //! Update the keyboard map + /*! + Causes the key state to get updated to reflect the current keyboard + mapping. + */ + virtual void updateKeyMap() = 0; + + //! Update the key state + /*! + Causes the key state to get updated to reflect the physical keyboard + state. + */ + virtual void updateKeyState() = 0; + + //! Set half-duplex mask + /*! + Sets which modifier toggle keys are half-duplex. A half-duplex + toggle key doesn't report a key release when toggled on and + doesn't report a key press when toggled off. + */ + virtual void setHalfDuplexMask(KeyModifierMask) = 0; + + //! Fake a key press + /*! + Synthesizes a key press event and updates the key state. + */ + virtual void fakeKeyDown(KeyID id, KeyModifierMask mask, + KeyButton button) = 0; + + //! Fake a key repeat + /*! + Synthesizes a key repeat event and updates the key state. + */ + virtual bool fakeKeyRepeat(KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton button) = 0; + + //! Fake a key release + /*! + Synthesizes a key release event and updates the key state. + */ + virtual bool fakeKeyUp(KeyButton button) = 0; + + //! Fake key releases for all fake pressed keys + /*! + Synthesizes a key release event for every key that is synthetically + pressed and updates the key state. + */ + virtual void fakeAllKeysUp() = 0; + + //! Fake ctrl+alt+del + /*! + Synthesize a press of ctrl+alt+del. Return true if processing is + complete and false if normal key processing should continue. + */ + virtual bool fakeCtrlAltDel() = 0; + + //@} + //! @name accessors + //@{ + + //! Test if key is pressed + /*! + Returns true iff the given key is down. Half-duplex toggles + always return false. + */ + virtual bool isKeyDown(KeyButton) const = 0; + + //! Get the active modifiers + /*! + Returns the modifiers that are currently active according to our + shadowed state. + */ + virtual KeyModifierMask + getActiveModifiers() const = 0; + + //! Get the active modifiers from OS + /*! + Returns the modifiers that are currently active according to the + operating system. + */ + virtual KeyModifierMask + pollActiveModifiers() const = 0; + + //! Get the active keyboard layout from OS + /*! + Returns the active keyboard layout according to the operating system. + */ + virtual SInt32 pollActiveGroup() const = 0; + + //! Get the keys currently pressed from OS + /*! + Adds any keys that are currently pressed according to the operating + system to \p pressedKeys. + */ + virtual void pollPressedKeys(KeyButtonSet& pressedKeys) const = 0; + + //! Get key down event type. Event data is CKeyInfo*, count == 1. + CEvent::Type getKeyDownEvent() { return getKeyDownEvent(m_eventQueue); } + + //! Get key up event type. Event data is CKeyInfo*, count == 1. + CEvent::Type getKeyUpEvent() { return getKeyUpEvent(m_eventQueue); } + + //! Get key repeat event type. Event data is CKeyInfo*. + CEvent::Type getKeyRepeatEvent() { return getKeyRepeatEvent(m_eventQueue); } + + //! Get key down event type. Event data is CKeyInfo*, count == 1. + static CEvent::Type getKeyDownEvent(IEventQueue& eventQueue); + + //! Get key up event type. Event data is CKeyInfo*, count == 1. + static CEvent::Type getKeyUpEvent(IEventQueue& eventQueue); + + //! Get key repeat event type. Event data is CKeyInfo*. + static CEvent::Type getKeyRepeatEvent(IEventQueue& eventQueue); + + //@} + +protected: + IEventQueue& getEventQueue() const { return m_eventQueue; } + +private: + static CEvent::Type s_keyDownEvent; + static CEvent::Type s_keyUpEvent; + static CEvent::Type s_keyRepeatEvent; + IEventQueue& m_eventQueue; +}; + +#endif diff --git a/src/lib/synergy/INode.h b/src/lib/synergy/INode.h new file mode 100644 index 00000000..50a1fd66 --- /dev/null +++ b/src/lib/synergy/INode.h @@ -0,0 +1,22 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IInterface.h" + +class INode : IInterface { + +}; diff --git a/src/lib/synergy/IPlatformScreen.h b/src/lib/synergy/IPlatformScreen.h new file mode 100644 index 00000000..2287177a --- /dev/null +++ b/src/lib/synergy/IPlatformScreen.h @@ -0,0 +1,222 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IPLATFORMSCREEN_H +#define IPLATFORMSCREEN_H + +#include "IScreen.h" +#include "IPrimaryScreen.h" +#include "ISecondaryScreen.h" +#include "IKeyState.h" +#include "ClipboardTypes.h" +#include "OptionTypes.h" + +class IClipboard; + +//! Screen interface +/*! +This interface defines the methods common to all platform dependent +screen implementations that are used by both primary and secondary +screens. +*/ +class IPlatformScreen : public IScreen, + public IPrimaryScreen, public ISecondaryScreen, + public IKeyState { +public: + //! @name manipulators + //@{ + + IPlatformScreen() { } + IPlatformScreen(IEventQueue& eventQueue) : IKeyState(eventQueue) { } + + //! Enable screen + /*! + Enable the screen, preparing it to report system and user events. + For a secondary screen it also means preparing to synthesize events + and hiding the cursor. + */ + virtual void enable() = 0; + + //! Disable screen + /*! + Undoes the operations in enable() and events should no longer + be reported. + */ + virtual void disable() = 0; + + //! Enter screen + /*! + Called when the user navigates to this screen. + */ + virtual void enter() = 0; + + //! Leave screen + /*! + Called when the user navigates off the screen. Returns true on + success, false on failure. A typical reason for failure is being + unable to install the keyboard and mouse snoopers on a primary + screen. Secondary screens should not fail. + */ + virtual bool leave() = 0; + + //! Set clipboard + /*! + Set the contents of the system clipboard indicated by \c id. + */ + virtual bool setClipboard(ClipboardID id, const IClipboard*) = 0; + + //! Check clipboard owner + /*! + Check ownership of all clipboards and post grab events for any that + have changed. This is used as a backup in case the system doesn't + reliably report clipboard ownership changes. + */ + virtual void checkClipboards() = 0; + + //! Open screen saver + /*! + Open the screen saver. If \c notify is true then this object must + send events when the screen saver activates or deactivates until + \c closeScreensaver() is called. If \c notify is false then the + screen saver is disabled and restored on \c closeScreensaver(). + */ + virtual void openScreensaver(bool notify) = 0; + + //! Close screen saver + /*! + // Close the screen saver. Stop reporting screen saver activation + and deactivation and, if the screen saver was disabled by + openScreensaver(), enable the screen saver. + */ + virtual void closeScreensaver() = 0; + + //! Activate/deactivate screen saver + /*! + Forcibly activate the screen saver if \c activate is true otherwise + forcibly deactivate it. + */ + virtual void screensaver(bool activate) = 0; + + //! Notify of options changes + /*! + Reset all options to their default values. + */ + virtual void resetOptions() = 0; + + //! Notify of options changes + /*! + Set options to given values. Ignore unknown options and don't + modify options that aren't given in \c options. + */ + virtual void setOptions(const COptionsList& options) = 0; + + //! Set clipboard sequence number + /*! + Sets the sequence number to use in subsequent clipboard events. + */ + virtual void setSequenceNumber(UInt32) = 0; + + //@} + //! @name accessors + //@{ + + //! Test if is primary screen + /*! + Return true iff this screen is a primary screen. + */ + virtual bool isPrimary() const = 0; + + //@} + + // IScreen overrides + virtual void* getEventTarget() const = 0; + virtual bool getClipboard(ClipboardID id, IClipboard*) const = 0; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const = 0; + virtual void getCursorPos(SInt32& x, SInt32& y) const = 0; + + // IPrimaryScreen overrides + virtual void reconfigure(UInt32 activeSides) = 0; + virtual void warpCursor(SInt32 x, SInt32 y) = 0; + virtual UInt32 registerHotKey(KeyID key, KeyModifierMask mask) = 0; + virtual void unregisterHotKey(UInt32 id) = 0; + virtual void fakeInputBegin() = 0; + virtual void fakeInputEnd() = 0; + virtual SInt32 getJumpZoneSize() const = 0; + virtual bool isAnyMouseButtonDown() const = 0; + virtual void getCursorCenter(SInt32& x, SInt32& y) const = 0; + virtual void gameDeviceTimingResp(UInt16 freq) = 0; + virtual void gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2) = 0; + + // ISecondaryScreen overrides + virtual void fakeMouseButton(ButtonID id, bool press) = 0; + virtual void fakeMouseMove(SInt32 x, SInt32 y) const = 0; + virtual void fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const = 0; + virtual void fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const = 0; + virtual void fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const = 0; + virtual void fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const = 0; + virtual void fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const = 0; + virtual void queueGameDeviceTimingReq() const = 0; + + // IKeyState overrides + virtual void updateKeyMap() = 0; + virtual void updateKeyState() = 0; + virtual void setHalfDuplexMask(KeyModifierMask) = 0; + virtual void fakeKeyDown(KeyID id, KeyModifierMask mask, + KeyButton button) = 0; + virtual bool fakeKeyRepeat(KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton button) = 0; + virtual bool fakeKeyUp(KeyButton button) = 0; + virtual void fakeAllKeysUp() = 0; + virtual bool fakeCtrlAltDel() = 0; + virtual bool isKeyDown(KeyButton) const = 0; + virtual KeyModifierMask + getActiveModifiers() const = 0; + virtual KeyModifierMask + pollActiveModifiers() const = 0; + virtual SInt32 pollActiveGroup() const = 0; + virtual void pollPressedKeys(KeyButtonSet& pressedKeys) const = 0; + +protected: + //! Handle system event + /*! + A platform screen is expected to install a handler for system + events in its c'tor like so: + \code + EVENTQUEUE->adoptHandler(CEvent::kSystem, + IEventQueue::getSystemTarget(), + new TMethodEventJob(this, + &CXXXPlatformScreen::handleSystemEvent)); + \endcode + It should remove the handler in its d'tor. Override the + \c handleSystemEvent() method to process system events. + It should post the events \c IScreen as appropriate. + + A primary screen has further responsibilities. It should post + the events in \c IPrimaryScreen as appropriate. It should also + call \c onKey() on its \c CKeyState whenever a key is pressed + or released (but not for key repeats). And it should call + \c updateKeyMap() on its \c CKeyState if necessary when the keyboard + mapping changes. + + The target of all events should be the value returned by + \c getEventTarget(). + */ + virtual void handleSystemEvent(const CEvent& event, void*) = 0; +}; + +#endif diff --git a/src/lib/synergy/IPrimaryScreen.cpp b/src/lib/synergy/IPrimaryScreen.cpp new file mode 100644 index 00000000..92e7a9c3 --- /dev/null +++ b/src/lib/synergy/IPrimaryScreen.cpp @@ -0,0 +1,214 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IPrimaryScreen.h" +#include "CEventQueue.h" +#include + +// +// IPrimaryScreen +// + +CEvent::Type IPrimaryScreen::s_buttonDownEvent = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_buttonUpEvent = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_motionPrimaryEvent = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_motionSecondaryEvent = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_wheelEvent = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_ssActivatedEvent = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_ssDeactivatedEvent = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_hotKeyDownEvent = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_hotKeyUpEvent = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_fakeInputBegin = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_fakeInputEnd = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_gameButtonsEvent = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_gameSticksEvent = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_gameTriggersEvent = CEvent::kUnknown; +CEvent::Type IPrimaryScreen::s_gameTimingReqEvent = CEvent::kUnknown; + +CEvent::Type +IPrimaryScreen::getButtonDownEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_buttonDownEvent, + "IPrimaryScreen::buttonDown"); +} + +CEvent::Type +IPrimaryScreen::getButtonUpEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_buttonUpEvent, + "IPrimaryScreen::buttonUp"); +} + +CEvent::Type +IPrimaryScreen::getMotionOnPrimaryEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_motionPrimaryEvent, + "IPrimaryScreen::motionPrimary"); +} + +CEvent::Type +IPrimaryScreen::getMotionOnSecondaryEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_motionSecondaryEvent, + "IPrimaryScreen::motionSecondary"); +} + +CEvent::Type +IPrimaryScreen::getWheelEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_wheelEvent, + "IPrimaryScreen::wheel"); +} + +CEvent::Type +IPrimaryScreen::getScreensaverActivatedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_ssActivatedEvent, + "IPrimaryScreen::screensaverActivated"); +} + +CEvent::Type +IPrimaryScreen::getScreensaverDeactivatedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_ssDeactivatedEvent, + "IPrimaryScreen::screensaverDeactivated"); +} + +CEvent::Type +IPrimaryScreen::getHotKeyDownEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_hotKeyDownEvent, + "IPrimaryScreen::hotKeyDown"); +} + +CEvent::Type +IPrimaryScreen::getHotKeyUpEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_hotKeyUpEvent, + "IPrimaryScreen::hotKeyUp"); +} + +CEvent::Type +IPrimaryScreen::getFakeInputBeginEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_fakeInputBegin, + "IPrimaryScreen::fakeInputBegin"); +} + +CEvent::Type +IPrimaryScreen::getFakeInputEndEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_fakeInputEnd, + "IPrimaryScreen::fakeInputEnd"); +} + +CEvent::Type +IPrimaryScreen::getGameDeviceButtonsEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_gameButtonsEvent, + "IPrimaryScreen::getGameDeviceButtonsEvent"); +} + +CEvent::Type +IPrimaryScreen::getGameDeviceSticksEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_gameSticksEvent, + "IPrimaryScreen::getGameDeviceSticksEvent"); +} + +CEvent::Type +IPrimaryScreen::getGameDeviceTriggersEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_gameTriggersEvent, + "IPrimaryScreen::getGameDeviceTriggersEvent"); +} + +CEvent::Type +IPrimaryScreen::getGameDeviceTimingReqEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_gameTimingReqEvent, + "IPrimaryScreen::getGameDeviceTimingReqEvent"); +} + +// +// IPrimaryScreen::CButtonInfo +// + +IPrimaryScreen::CButtonInfo* +IPrimaryScreen::CButtonInfo::alloc(ButtonID id, KeyModifierMask mask) +{ + CButtonInfo* info = (CButtonInfo*)malloc(sizeof(CButtonInfo)); + info->m_button = id; + info->m_mask = mask; + return info; +} + +IPrimaryScreen::CButtonInfo* +IPrimaryScreen::CButtonInfo::alloc(const CButtonInfo& x) +{ + CButtonInfo* info = (CButtonInfo*)malloc(sizeof(CButtonInfo)); + info->m_button = x.m_button; + info->m_mask = x.m_mask; + return info; +} + +bool +IPrimaryScreen::CButtonInfo::equal(const CButtonInfo* a, const CButtonInfo* b) +{ + return (a->m_button == b->m_button && a->m_mask == b->m_mask); +} + + +// +// IPrimaryScreen::CMotionInfo +// + +IPrimaryScreen::CMotionInfo* +IPrimaryScreen::CMotionInfo::alloc(SInt32 x, SInt32 y) +{ + CMotionInfo* info = (CMotionInfo*)malloc(sizeof(CMotionInfo)); + info->m_x = x; + info->m_y = y; + return info; +} + + +// +// IPrimaryScreen::CWheelInfo +// + +IPrimaryScreen::CWheelInfo* +IPrimaryScreen::CWheelInfo::alloc(SInt32 xDelta, SInt32 yDelta) +{ + CWheelInfo* info = (CWheelInfo*)malloc(sizeof(CWheelInfo)); + info->m_xDelta = xDelta; + info->m_yDelta = yDelta; + return info; +} + + +// +// IPrimaryScreen::CHotKeyInfo +// + +IPrimaryScreen::CHotKeyInfo* +IPrimaryScreen::CHotKeyInfo::alloc(UInt32 id) +{ + CHotKeyInfo* info = (CHotKeyInfo*)malloc(sizeof(CHotKeyInfo)); + info->m_id = id; + return info; +} diff --git a/src/lib/synergy/IPrimaryScreen.h b/src/lib/synergy/IPrimaryScreen.h new file mode 100644 index 00000000..a746d02b --- /dev/null +++ b/src/lib/synergy/IPrimaryScreen.h @@ -0,0 +1,279 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef IPRIMARYSCREEN_H +#define IPRIMARYSCREEN_H + +#include "IInterface.h" +#include "KeyTypes.h" +#include "MouseTypes.h" +#include "CEvent.h" +#include "GameDeviceTypes.h" + +//! Primary screen interface +/*! +This interface defines the methods common to all platform dependent +primary screen implementations. +*/ +class IPrimaryScreen : public IInterface { +public: + //! Button event data + class CButtonInfo { + public: + static CButtonInfo* alloc(ButtonID, KeyModifierMask); + static CButtonInfo* alloc(const CButtonInfo&); + + static bool equal(const CButtonInfo*, const CButtonInfo*); + + public: + ButtonID m_button; + KeyModifierMask m_mask; + }; + //! Motion event data + class CMotionInfo { + public: + static CMotionInfo* alloc(SInt32 x, SInt32 y); + + public: + SInt32 m_x; + SInt32 m_y; + }; + //! Wheel motion event data + class CWheelInfo { + public: + static CWheelInfo* alloc(SInt32 xDelta, SInt32 yDelta); + + public: + SInt32 m_xDelta; + SInt32 m_yDelta; + }; + //! Hot key event data + class CHotKeyInfo { + public: + static CHotKeyInfo* alloc(UInt32 id); + + public: + UInt32 m_id; + }; + //! Game device button event data + class CGameDeviceButtonInfo { + public: + CGameDeviceButtonInfo(GameDeviceID id, GameDeviceButton buttons) : + m_id(id), m_buttons(buttons) { } + public: + GameDeviceID m_id; + GameDeviceButton m_buttons; + }; + //! Game device sticks event data + class CGameDeviceStickInfo { + public: + CGameDeviceStickInfo(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) : + m_id(id), m_x1(x1), m_x2(x2), m_y1(y1), m_y2(y2) { } + public: + GameDeviceID m_id; + SInt16 m_x1; + SInt16 m_x2; + SInt16 m_y1; + SInt16 m_y2; + }; + //! Game device triggers event data + class CGameDeviceTriggerInfo { + public: + CGameDeviceTriggerInfo(GameDeviceID id, UInt8 t1, UInt8 t2) : + m_id(id), m_t1(t1), m_t2(t2) { } + public: + GameDeviceID m_id; + UInt8 m_t1; + UInt8 m_t2; + }; + //! Game device timing response event data + class CGameDeviceTimingRespInfo { + public: + CGameDeviceTimingRespInfo(UInt16 freq) : + m_freq(freq) { } + public: + UInt16 m_freq; + }; + //! Game device feedback event data + class CGameDeviceFeedbackInfo { + public: + CGameDeviceFeedbackInfo(GameDeviceID id, UInt16 m1, UInt16 m2) : + m_id(id), m_m1(m1), m_m2(m2) { } + public: + GameDeviceID m_id; + UInt16 m_m1; + UInt16 m_m2; + }; + + //! @name manipulators + //@{ + + //! Update configuration + /*! + This is called when the configuration has changed. \c activeSides + is a bitmask of EDirectionMask indicating which sides of the + primary screen are linked to clients. Override to handle the + possible change in jump zones. + */ + virtual void reconfigure(UInt32 activeSides) = 0; + + //! Warp cursor + /*! + Warp the cursor to the absolute coordinates \c x,y. Also + discard input events up to and including the warp before + returning. + */ + virtual void warpCursor(SInt32 x, SInt32 y) = 0; + + //! Register a system hotkey + /*! + Registers a system-wide hotkey. The screen should arrange for an event + to be delivered to itself when the hot key is pressed or released. When + that happens the screen should post a \c getHotKeyDownEvent() or + \c getHotKeyUpEvent(), respectively. The hot key is key \p key with + exactly the modifiers \p mask. Returns 0 on failure otherwise an id + that can be used to unregister the hotkey. + + A hot key is a set of modifiers and a key, which may itself be a modifier. + The hot key is pressed when the hot key's modifiers and only those + modifiers are logically down (active) and the key is pressed. The hot + key is released when the key is released, regardless of the modifiers. + + The hot key event should be generated no matter what window or application + has the focus. No other window or application should receive the key + press or release events (they can and should see the modifier key events). + When the key is a modifier, it's acceptable to allow the user to press + the modifiers in any order or to require the user to press the given key + last. + */ + virtual UInt32 registerHotKey(KeyID key, KeyModifierMask mask) = 0; + + //! Unregister a system hotkey + /*! + Unregisters a previously registered hot key. + */ + virtual void unregisterHotKey(UInt32 id) = 0; + + //! Prepare to synthesize input on primary screen + /*! + Prepares the primary screen to receive synthesized input. We do not + want to receive this synthesized input as user input so this method + ensures that we ignore it. Calls to \c fakeInputBegin() may not be + nested. + */ + virtual void fakeInputBegin() = 0; + + //! Done synthesizing input on primary screen + /*! + Undoes whatever \c fakeInputBegin() did. + */ + virtual void fakeInputEnd() = 0; + + //@} + //! @name accessors + //@{ + + //! Get jump zone size + /*! + Return the jump zone size, the size of the regions on the edges of + the screen that cause the cursor to jump to another screen. + */ + virtual SInt32 getJumpZoneSize() const = 0; + + //! Test if mouse is pressed + /*! + Return true if any mouse button is currently pressed. Ideally, + "current" means up to the last processed event but it can mean + the current physical mouse button state. + */ + virtual bool isAnyMouseButtonDown() const = 0; + + //! Get cursor center position + /*! + Return the cursor center position which is where we park the + cursor to compute cursor motion deltas and should be far from + the edges of the screen, typically the center. + */ + virtual void getCursorCenter(SInt32& x, SInt32& y) const = 0; + + //! Handle incoming game device timing responses. + virtual void gameDeviceTimingResp(UInt16 freq) = 0; + + //! Handle incoming game device feedback changes. + virtual void gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2) = 0; + + //! Get button down event type. Event data is CButtonInfo*. + static CEvent::Type getButtonDownEvent(); + //! Get button up event type. Event data is CButtonInfo*. + static CEvent::Type getButtonUpEvent(); + //! Get mouse motion on the primary screen event type + /*! + Event data is CMotionInfo* and the values are an absolute position. + */ + static CEvent::Type getMotionOnPrimaryEvent(); + //! Get mouse motion on a secondary screen event type + /*! + Event data is CMotionInfo* and the values are motion deltas not + absolute coordinates. + */ + static CEvent::Type getMotionOnSecondaryEvent(); + //! Get mouse wheel event type. Event data is CWheelInfo*. + static CEvent::Type getWheelEvent(); + //! Get screensaver activated event type + static CEvent::Type getScreensaverActivatedEvent(); + //! Get screensaver deactivated event type + static CEvent::Type getScreensaverDeactivatedEvent(); + //! Get hot key down event type. Event data is CHotKeyInfo*. + static CEvent::Type getHotKeyDownEvent(); + //! Get hot key up event type. Event data is CHotKeyInfo*. + static CEvent::Type getHotKeyUpEvent(); + //! Get start of fake input event type + static CEvent::Type getFakeInputBeginEvent(); + //! Get end of fake input event type + static CEvent::Type getFakeInputEndEvent(); +public: // HACK + //! Get game device buttons event type. + static CEvent::Type getGameDeviceButtonsEvent(); + //! Get game device sticks event type. + static CEvent::Type getGameDeviceSticksEvent(); + //! Get game device triggers event type. + static CEvent::Type getGameDeviceTriggersEvent(); + //! Get game device timing request event type. + static CEvent::Type getGameDeviceTimingReqEvent(); +private: // HACK + + //@} + +private: + static CEvent::Type s_buttonDownEvent; + static CEvent::Type s_buttonUpEvent; + static CEvent::Type s_motionPrimaryEvent; + static CEvent::Type s_motionSecondaryEvent; + static CEvent::Type s_wheelEvent; + static CEvent::Type s_ssActivatedEvent; + static CEvent::Type s_ssDeactivatedEvent; + static CEvent::Type s_hotKeyDownEvent; + static CEvent::Type s_hotKeyUpEvent; + static CEvent::Type s_fakeInputBegin; + static CEvent::Type s_fakeInputEnd; + static CEvent::Type s_gameButtonsEvent; + static CEvent::Type s_gameSticksEvent; + static CEvent::Type s_gameTriggersEvent; + static CEvent::Type s_gameTimingReqEvent; +}; + +#endif diff --git a/src/lib/synergy/IScreen.cpp b/src/lib/synergy/IScreen.cpp new file mode 100644 index 00000000..83b6328f --- /dev/null +++ b/src/lib/synergy/IScreen.cpp @@ -0,0 +1,64 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2004 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "IScreen.h" +#include "CEventQueue.h" + +// +// IScreen +// + +CEvent::Type IScreen::s_errorEvent = CEvent::kUnknown; +CEvent::Type IScreen::s_shapeChangedEvent = CEvent::kUnknown; +CEvent::Type IScreen::s_clipboardGrabbedEvent = CEvent::kUnknown; +CEvent::Type IScreen::s_suspendEvent = CEvent::kUnknown; +CEvent::Type IScreen::s_resumeEvent = CEvent::kUnknown; + +CEvent::Type +IScreen::getErrorEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_errorEvent, + "IScreen::error"); +} + +CEvent::Type +IScreen::getShapeChangedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_shapeChangedEvent, + "IScreen::shapeChanged"); +} + +CEvent::Type +IScreen::getClipboardGrabbedEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_clipboardGrabbedEvent, + "IScreen::clipboardGrabbed"); +} + +CEvent::Type +IScreen::getSuspendEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_suspendEvent, + "IScreen::suspend"); +} + +CEvent::Type +IScreen::getResumeEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_resumeEvent, + "IScreen::resume"); +} diff --git a/src/lib/synergy/IScreen.h b/src/lib/synergy/IScreen.h new file mode 100644 index 00000000..b6f5e107 --- /dev/null +++ b/src/lib/synergy/IScreen.h @@ -0,0 +1,115 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ISCREEN_H +#define ISCREEN_H + +#include "IInterface.h" +#include "ClipboardTypes.h" +#include "CEvent.h" + +class IClipboard; + +//! Screen interface +/*! +This interface defines the methods common to all screens. +*/ +class IScreen : public IInterface { +public: + struct CClipboardInfo { + public: + ClipboardID m_id; + UInt32 m_sequenceNumber; + }; + + //! @name accessors + //@{ + + //! Get event target + /*! + Returns the target used for events created by this object. + */ + virtual void* getEventTarget() const = 0; + + //! Get clipboard + /*! + Save the contents of the clipboard indicated by \c id and return + true iff successful. + */ + virtual bool getClipboard(ClipboardID id, IClipboard*) const = 0; + + //! Get screen shape + /*! + Return the position of the upper-left corner of the screen in \c x and + \c y and the size of the screen in \c width and \c height. + */ + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const = 0; + + //! Get cursor position + /*! + Return the current position of the cursor in \c x and \c y. + */ + virtual void getCursorPos(SInt32& x, SInt32& y) const = 0; + + //! Get error event type + /*! + Returns the error event type. This is sent whenever the screen has + failed for some reason (e.g. the X Windows server died). + */ + static CEvent::Type getErrorEvent(); + + //! Get shape changed event type + /*! + Returns the shape changed event type. This is sent whenever the + screen's shape changes. + */ + static CEvent::Type getShapeChangedEvent(); + + //! Get clipboard grabbed event type + /*! + Returns the clipboard grabbed event type. This is sent whenever the + clipboard is grabbed by some other application so we don't own it + anymore. The data is a pointer to a CClipboardInfo. + */ + static CEvent::Type getClipboardGrabbedEvent(); + + //! Get suspend event type + /*! + Returns the suspend event type. This is sent whenever the system goes + to sleep or a user session is deactivated (fast user switching). + */ + static CEvent::Type getSuspendEvent(); + + //! Get resume event type + /*! + Returns the suspend event type. This is sent whenever the system wakes + up or a user session is activated (fast user switching). + */ + static CEvent::Type getResumeEvent(); + + //@} + +private: + static CEvent::Type s_errorEvent; + static CEvent::Type s_shapeChangedEvent; + static CEvent::Type s_clipboardGrabbedEvent; + static CEvent::Type s_suspendEvent; + static CEvent::Type s_resumeEvent; +}; + +#endif diff --git a/src/lib/synergy/IScreenSaver.h b/src/lib/synergy/IScreenSaver.h new file mode 100644 index 00000000..460518c3 --- /dev/null +++ b/src/lib/synergy/IScreenSaver.h @@ -0,0 +1,77 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ISCREENSAVER_H +#define ISCREENSAVER_H + +#include "IInterface.h" +#include "CEvent.h" + +//! Screen saver interface +/*! +This interface defines the methods common to all screen savers. +*/ +class IScreenSaver : public IInterface { +public: + // note -- the c'tor/d'tor must *not* enable/disable the screen saver + + //! @name manipulators + //@{ + + //! Enable screen saver + /*! + Enable the screen saver, restoring the screen saver settings to + what they were when disable() was previously called. If disable() + wasn't previously called then it should keep the current settings + or use reasonable defaults. + */ + virtual void enable() = 0; + + //! Disable screen saver + /*! + Disable the screen saver, saving the old settings for the next + call to enable(). + */ + virtual void disable() = 0; + + //! Activate screen saver + /*! + Activate (i.e. show) the screen saver. + */ + virtual void activate() = 0; + + //! Deactivate screen saver + /*! + Deactivate (i.e. hide) the screen saver, reseting the screen saver + timer. + */ + virtual void deactivate() = 0; + + //@} + //! @name accessors + //@{ + + //! Test if screen saver on + /*! + Returns true iff the screen saver is currently active (showing). + */ + virtual bool isActive() const = 0; + + //@} +}; + +#endif diff --git a/src/lib/synergy/ISecondaryScreen.cpp b/src/lib/synergy/ISecondaryScreen.cpp new file mode 100644 index 00000000..d2dc6539 --- /dev/null +++ b/src/lib/synergy/ISecondaryScreen.cpp @@ -0,0 +1,36 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "ISecondaryScreen.h" +#include "IEventQueue.h" + +CEvent::Type ISecondaryScreen::s_gameTimingRespEvent = CEvent::kUnknown; +CEvent::Type ISecondaryScreen::s_gameFeedbackEvent = CEvent::kUnknown; + +CEvent::Type +ISecondaryScreen::getGameDeviceTimingRespEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_gameTimingRespEvent, + "ISecondaryScreen::getGameDeviceTimingRespEvent"); +} + +CEvent::Type +ISecondaryScreen::getGameDeviceFeedbackEvent() +{ + return EVENTQUEUE->registerTypeOnce(s_gameFeedbackEvent, + "ISecondaryScreen::getGameDeviceFeedbackEvent"); +} diff --git a/src/lib/synergy/ISecondaryScreen.h b/src/lib/synergy/ISecondaryScreen.h new file mode 100644 index 00000000..2f624ba6 --- /dev/null +++ b/src/lib/synergy/ISecondaryScreen.h @@ -0,0 +1,91 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2003 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef ISECONDARYSCREEN_H +#define ISECONDARYSCREEN_H + +#include "IInterface.h" +#include "MouseTypes.h" +#include "GameDeviceTypes.h" +#include "CEvent.h" + +//! Secondary screen interface +/*! +This interface defines the methods common to all platform dependent +secondary screen implementations. +*/ +class ISecondaryScreen : public IInterface { +public: + //! @name accessors + //@{ + + //! Fake mouse press/release + /*! + Synthesize a press or release of mouse button \c id. + */ + virtual void fakeMouseButton(ButtonID id, bool press) = 0; + + //! Fake mouse move + /*! + Synthesize a mouse move to the absolute coordinates \c x,y. + */ + virtual void fakeMouseMove(SInt32 x, SInt32 y) const = 0; + + //! Fake mouse move + /*! + Synthesize a mouse move to the relative coordinates \c dx,dy. + */ + virtual void fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const = 0; + + //! Fake mouse wheel + /*! + Synthesize a mouse wheel event of amount \c xDelta and \c yDelta. + */ + virtual void fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const = 0; + + //! Fake game device buttons + /*! + Synthesize game device buttons state. + */ + virtual void fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const = 0; + + //! Fake game device sticks + /*! + Synthesize game device sticks state. + */ + virtual void fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const = 0; + + //! Fake game device triggers + /*! + Synthesize game device triggers state. + */ + virtual void fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const = 0; + + //! Get game device timing response event type. + static CEvent::Type getGameDeviceTimingRespEvent(); + + //! Get game device feedback event type. + static CEvent::Type getGameDeviceFeedbackEvent(); + + //@} + +private: + static CEvent::Type s_gameTimingRespEvent; + static CEvent::Type s_gameFeedbackEvent; +}; + +#endif diff --git a/src/lib/synergy/KeyTypes.cpp b/src/lib/synergy/KeyTypes.cpp new file mode 100644 index 00000000..a51e6ffb --- /dev/null +++ b/src/lib/synergy/KeyTypes.cpp @@ -0,0 +1,207 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "KeyTypes.h" + +const KeyNameMapEntry kKeyNameMap[] = { + { "AltGr", kKeyAltGr }, + { "Alt_L", kKeyAlt_L }, + { "Alt_R", kKeyAlt_R }, + { "AppMail", kKeyAppMail }, + { "AppMedia", kKeyAppMedia }, + { "AppUser1", kKeyAppUser1 }, + { "AppUser2", kKeyAppUser2 }, + { "AudioDown", kKeyAudioDown }, + { "AudioMute", kKeyAudioMute }, + { "AudioNext", kKeyAudioNext }, + { "AudioPlay", kKeyAudioPlay }, + { "AudioPrev", kKeyAudioPrev }, + { "AudioStop", kKeyAudioStop }, + { "AudioUp", kKeyAudioUp }, + { "BackSpace", kKeyBackSpace }, + { "Begin", kKeyBegin }, + { "Break", kKeyBreak }, + { "Cancel", kKeyCancel }, + { "CapsLock", kKeyCapsLock }, + { "Clear", kKeyClear }, + { "Control_L", kKeyControl_L }, + { "Control_R", kKeyControl_R }, + { "Delete", kKeyDelete }, + { "Down", kKeyDown }, + { "Eject", kKeyEject }, + { "End", kKeyEnd }, + { "Escape", kKeyEscape }, + { "Execute", kKeyExecute }, + { "F1", kKeyF1 }, + { "F2", kKeyF2 }, + { "F3", kKeyF3 }, + { "F4", kKeyF4 }, + { "F5", kKeyF5 }, + { "F6", kKeyF6 }, + { "F7", kKeyF7 }, + { "F8", kKeyF8 }, + { "F9", kKeyF9 }, + { "F10", kKeyF10 }, + { "F11", kKeyF11 }, + { "F12", kKeyF12 }, + { "F13", kKeyF13 }, + { "F14", kKeyF14 }, + { "F15", kKeyF15 }, + { "F16", kKeyF16 }, + { "F17", kKeyF17 }, + { "F18", kKeyF18 }, + { "F19", kKeyF19 }, + { "F20", kKeyF20 }, + { "F21", kKeyF21 }, + { "F22", kKeyF22 }, + { "F23", kKeyF23 }, + { "F24", kKeyF24 }, + { "F25", kKeyF25 }, + { "F26", kKeyF26 }, + { "F27", kKeyF27 }, + { "F28", kKeyF28 }, + { "F29", kKeyF29 }, + { "F30", kKeyF30 }, + { "F31", kKeyF31 }, + { "F32", kKeyF32 }, + { "F33", kKeyF33 }, + { "F34", kKeyF34 }, + { "F35", kKeyF35 }, + { "Find", kKeyFind }, + { "Help", kKeyHelp }, + { "Henkan", kKeyHenkan }, + { "Home", kKeyHome }, + { "Hyper_L", kKeyHyper_L }, + { "Hyper_R", kKeyHyper_R }, + { "Insert", kKeyInsert }, + { "KP_0", kKeyKP_0 }, + { "KP_1", kKeyKP_1 }, + { "KP_2", kKeyKP_2 }, + { "KP_3", kKeyKP_3 }, + { "KP_4", kKeyKP_4 }, + { "KP_5", kKeyKP_5 }, + { "KP_6", kKeyKP_6 }, + { "KP_7", kKeyKP_7 }, + { "KP_8", kKeyKP_8 }, + { "KP_9", kKeyKP_9 }, + { "KP_Add", kKeyKP_Add }, + { "KP_Begin", kKeyKP_Begin }, + { "KP_Decimal", kKeyKP_Decimal }, + { "KP_Delete", kKeyKP_Delete }, + { "KP_Divide", kKeyKP_Divide }, + { "KP_Down", kKeyKP_Down }, + { "KP_End", kKeyKP_End }, + { "KP_Enter", kKeyKP_Enter }, + { "KP_Equal", kKeyKP_Equal }, + { "KP_F1", kKeyKP_F1 }, + { "KP_F2", kKeyKP_F2 }, + { "KP_F3", kKeyKP_F3 }, + { "KP_F4", kKeyKP_F4 }, + { "KP_Home", kKeyKP_Home }, + { "KP_Insert", kKeyKP_Insert }, + { "KP_Left", kKeyKP_Left }, + { "KP_Multiply", kKeyKP_Multiply }, + { "KP_PageDown", kKeyKP_PageDown }, + { "KP_PageUp", kKeyKP_PageUp }, + { "KP_Right", kKeyKP_Right }, + { "KP_Separator", kKeyKP_Separator }, + { "KP_Space", kKeyKP_Space }, + { "KP_Subtract", kKeyKP_Subtract }, + { "KP_Tab", kKeyKP_Tab }, + { "KP_Up", kKeyKP_Up }, + { "Left", kKeyLeft }, + { "LeftTab", kKeyLeftTab }, + { "Linefeed", kKeyLinefeed }, + { "Menu", kKeyMenu }, + { "Meta_L", kKeyMeta_L }, + { "Meta_R", kKeyMeta_R }, + { "NumLock", kKeyNumLock }, + { "PageDown", kKeyPageDown }, + { "PageUp", kKeyPageUp }, + { "Pause", kKeyPause }, + { "Print", kKeyPrint }, + { "Redo", kKeyRedo }, + { "Return", kKeyReturn }, + { "Right", kKeyRight }, + { "ScrollLock", kKeyScrollLock }, + { "Select", kKeySelect }, + { "ShiftLock", kKeyShiftLock }, + { "Shift_L", kKeyShift_L }, + { "Shift_R", kKeyShift_R }, + { "Sleep", kKeySleep }, + { "Super_L", kKeySuper_L }, + { "Super_R", kKeySuper_R }, + { "SysReq", kKeySysReq }, + { "Tab", kKeyTab }, + { "Undo", kKeyUndo }, + { "Up", kKeyUp }, + { "WWWBack", kKeyWWWBack }, + { "WWWFavorites", kKeyWWWFavorites }, + { "WWWForward", kKeyWWWForward }, + { "WWWHome", kKeyWWWHome }, + { "WWWRefresh", kKeyWWWRefresh }, + { "WWWSearch", kKeyWWWSearch }, + { "WWWStop", kKeyWWWStop }, + { "Zenkaku", kKeyZenkaku }, + { "Space", 0x0020 }, + { "Exclaim", 0x0021 }, + { "DoubleQuote", 0x0022 }, + { "Number", 0x0023 }, + { "Dollar", 0x0024 }, + { "Percent", 0x0025 }, + { "Ampersand", 0x0026 }, + { "Apostrophe", 0x0027 }, + { "ParenthesisL", 0x0028 }, + { "ParenthesisR", 0x0029 }, + { "Asterisk", 0x002a }, + { "Plus", 0x002b }, + { "Comma", 0x002c }, + { "Minus", 0x002d }, + { "Period", 0x002e }, + { "Slash", 0x002f }, + { "Colon", 0x003a }, + { "Semicolon", 0x003b }, + { "Less", 0x003c }, + { "Equal", 0x003d }, + { "Greater", 0x003e }, + { "Question", 0x003f }, + { "At", 0x0040 }, + { "BracketL", 0x005b }, + { "Backslash", 0x005c }, + { "BracketR", 0x005d }, + { "Circumflex", 0x005e }, + { "Underscore", 0x005f }, + { "Grave", 0x0060 }, + { "BraceL", 0x007b }, + { "Bar", 0x007c }, + { "BraceR", 0x007d }, + { "Tilde", 0x007e }, + { NULL, 0 }, +}; + +const KeyModifierNameMapEntry kModifierNameMap[] = { + { "Alt", KeyModifierAlt }, + { "AltGr", KeyModifierAltGr }, +// { "CapsLock", KeyModifierCapsLock }, + { "Control", KeyModifierControl }, + { "Meta", KeyModifierMeta }, +// { "NumLock", KeyModifierNumLock }, +// { "ScrollLock", KeyModifierScrollLock }, + { "Shift", KeyModifierShift }, + { "Super", KeyModifierSuper }, + { NULL, 0 }, +}; diff --git a/src/lib/synergy/KeyTypes.h b/src/lib/synergy/KeyTypes.h new file mode 100644 index 00000000..116d326b --- /dev/null +++ b/src/lib/synergy/KeyTypes.h @@ -0,0 +1,312 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef KEYTYPES_H +#define KEYTYPES_H + +#include "BasicTypes.h" + +//! Key ID +/*! +Type to hold a key symbol identifier. The encoding is UTF-32, using +U+E000 through U+EFFF for the various control keys (e.g. arrow +keys, function keys, modifier keys, etc). +*/ +typedef UInt32 KeyID; + +//! Key Code +/*! +Type to hold a physical key identifier. That is, it identifies a +physical key on the keyboard. KeyButton 0 is reserved to be an +invalid key; platforms that use 0 as a physical key identifier +will have to remap that value to some arbitrary unused id. +*/ +typedef UInt16 KeyButton; + +//! Modifier key mask +/*! +Type to hold a bitmask of key modifiers (e.g. shift keys). +*/ +typedef UInt32 KeyModifierMask; + +//! Modifier key ID +/*! +Type to hold the id of a key modifier (e.g. a shift key). +*/ +typedef UInt32 KeyModifierID; + +//! @name Modifier key masks +//@{ +static const KeyModifierMask KeyModifierShift = 0x0001; +static const KeyModifierMask KeyModifierControl = 0x0002; +static const KeyModifierMask KeyModifierAlt = 0x0004; +static const KeyModifierMask KeyModifierMeta = 0x0008; +static const KeyModifierMask KeyModifierSuper = 0x0010; +static const KeyModifierMask KeyModifierAltGr = 0x0020; +static const KeyModifierMask KeyModifierLevel5Lock = 0x0040; +static const KeyModifierMask KeyModifierCapsLock = 0x1000; +static const KeyModifierMask KeyModifierNumLock = 0x2000; +static const KeyModifierMask KeyModifierScrollLock = 0x4000; +//@} + +//! @name Modifier key bits +//@{ +static const UInt32 kKeyModifierBitNone = 16; +static const UInt32 kKeyModifierBitShift = 0; +static const UInt32 kKeyModifierBitControl = 1; +static const UInt32 kKeyModifierBitAlt = 2; +static const UInt32 kKeyModifierBitMeta = 3; +static const UInt32 kKeyModifierBitSuper = 4; +static const UInt32 kKeyModifierBitAltGr = 5; +static const UInt32 kKeyModifierBitLevel5Lock = 6; +static const UInt32 kKeyModifierBitCapsLock = 12; +static const UInt32 kKeyModifierBitNumLock = 13; +static const UInt32 kKeyModifierBitScrollLock = 14; +static const SInt32 kKeyModifierNumBits = 16; +//@} + +//! @name Modifier key identifiers +//@{ +static const KeyModifierID kKeyModifierIDNull = 0; +static const KeyModifierID kKeyModifierIDShift = 1; +static const KeyModifierID kKeyModifierIDControl = 2; +static const KeyModifierID kKeyModifierIDAlt = 3; +static const KeyModifierID kKeyModifierIDMeta = 4; +static const KeyModifierID kKeyModifierIDSuper = 5; +static const KeyModifierID kKeyModifierIDAltGr = 6; +static const KeyModifierID kKeyModifierIDLast = 7; +//@} + +//! @name Key identifiers +//@{ +// all identifiers except kKeyNone and those in 0xE000 to 0xE0FF +// inclusive are equal to the corresponding X11 keysym - 0x1000. + +// no key +static const KeyID kKeyNone = 0x0000; + +// TTY functions +static const KeyID kKeyBackSpace = 0xEF08; /* back space, back char */ +static const KeyID kKeyTab = 0xEF09; +static const KeyID kKeyLinefeed = 0xEF0A; /* Linefeed, LF */ +static const KeyID kKeyClear = 0xEF0B; +static const KeyID kKeyReturn = 0xEF0D; /* Return, enter */ +static const KeyID kKeyPause = 0xEF13; /* Pause, hold */ +static const KeyID kKeyScrollLock = 0xEF14; +static const KeyID kKeySysReq = 0xEF15; +static const KeyID kKeyEscape = 0xEF1B; +static const KeyID kKeyHenkan = 0xEF23; /* Start/Stop Conversion */ +static const KeyID kKeyHangulKana = 0xEF26; /* Hangul, Kana */ +static const KeyID kKeyHiraganaKatakana = 0xEF27; /* Hiragana/Katakana toggle */ +static const KeyID kKeyZenkaku = 0xEF2A; /* Zenkaku/Hankaku */ +static const KeyID kKeyHanjaKanzi = 0xEF2A; /* Hanja, Kanzi */ +static const KeyID kKeyDelete = 0xEFFF; /* Delete, rubout */ + +// cursor control +static const KeyID kKeyHome = 0xEF50; +static const KeyID kKeyLeft = 0xEF51; /* Move left, left arrow */ +static const KeyID kKeyUp = 0xEF52; /* Move up, up arrow */ +static const KeyID kKeyRight = 0xEF53; /* Move right, right arrow */ +static const KeyID kKeyDown = 0xEF54; /* Move down, down arrow */ +static const KeyID kKeyPageUp = 0xEF55; +static const KeyID kKeyPageDown = 0xEF56; +static const KeyID kKeyEnd = 0xEF57; /* EOL */ +static const KeyID kKeyBegin = 0xEF58; /* BOL */ + +// misc functions +static const KeyID kKeySelect = 0xEF60; /* Select, mark */ +static const KeyID kKeyPrint = 0xEF61; +static const KeyID kKeyExecute = 0xEF62; /* Execute, run, do */ +static const KeyID kKeyInsert = 0xEF63; /* Insert, insert here */ +static const KeyID kKeyUndo = 0xEF65; /* Undo, oops */ +static const KeyID kKeyRedo = 0xEF66; /* redo, again */ +static const KeyID kKeyMenu = 0xEF67; +static const KeyID kKeyFind = 0xEF68; /* Find, search */ +static const KeyID kKeyCancel = 0xEF69; /* Cancel, stop, abort, exit */ +static const KeyID kKeyHelp = 0xEF6A; /* Help */ +static const KeyID kKeyBreak = 0xEF6B; +static const KeyID kKeyAltGr = 0xEF7E; /* Character set switch */ +static const KeyID kKeyNumLock = 0xEF7F; + +// keypad +static const KeyID kKeyKP_Space = 0xEF80; /* space */ +static const KeyID kKeyKP_Tab = 0xEF89; +static const KeyID kKeyKP_Enter = 0xEF8D; /* enter */ +static const KeyID kKeyKP_F1 = 0xEF91; /* PF1, KP_A, ... */ +static const KeyID kKeyKP_F2 = 0xEF92; +static const KeyID kKeyKP_F3 = 0xEF93; +static const KeyID kKeyKP_F4 = 0xEF94; +static const KeyID kKeyKP_Home = 0xEF95; +static const KeyID kKeyKP_Left = 0xEF96; +static const KeyID kKeyKP_Up = 0xEF97; +static const KeyID kKeyKP_Right = 0xEF98; +static const KeyID kKeyKP_Down = 0xEF99; +static const KeyID kKeyKP_PageUp = 0xEF9A; +static const KeyID kKeyKP_PageDown = 0xEF9B; +static const KeyID kKeyKP_End = 0xEF9C; +static const KeyID kKeyKP_Begin = 0xEF9D; +static const KeyID kKeyKP_Insert = 0xEF9E; +static const KeyID kKeyKP_Delete = 0xEF9F; +static const KeyID kKeyKP_Equal = 0xEFBD; /* equals */ +static const KeyID kKeyKP_Multiply = 0xEFAA; +static const KeyID kKeyKP_Add = 0xEFAB; +static const KeyID kKeyKP_Separator= 0xEFAC; /* separator, often comma */ +static const KeyID kKeyKP_Subtract = 0xEFAD; +static const KeyID kKeyKP_Decimal = 0xEFAE; +static const KeyID kKeyKP_Divide = 0xEFAF; +static const KeyID kKeyKP_0 = 0xEFB0; +static const KeyID kKeyKP_1 = 0xEFB1; +static const KeyID kKeyKP_2 = 0xEFB2; +static const KeyID kKeyKP_3 = 0xEFB3; +static const KeyID kKeyKP_4 = 0xEFB4; +static const KeyID kKeyKP_5 = 0xEFB5; +static const KeyID kKeyKP_6 = 0xEFB6; +static const KeyID kKeyKP_7 = 0xEFB7; +static const KeyID kKeyKP_8 = 0xEFB8; +static const KeyID kKeyKP_9 = 0xEFB9; + +// function keys +static const KeyID kKeyF1 = 0xEFBE; +static const KeyID kKeyF2 = 0xEFBF; +static const KeyID kKeyF3 = 0xEFC0; +static const KeyID kKeyF4 = 0xEFC1; +static const KeyID kKeyF5 = 0xEFC2; +static const KeyID kKeyF6 = 0xEFC3; +static const KeyID kKeyF7 = 0xEFC4; +static const KeyID kKeyF8 = 0xEFC5; +static const KeyID kKeyF9 = 0xEFC6; +static const KeyID kKeyF10 = 0xEFC7; +static const KeyID kKeyF11 = 0xEFC8; +static const KeyID kKeyF12 = 0xEFC9; +static const KeyID kKeyF13 = 0xEFCA; +static const KeyID kKeyF14 = 0xEFCB; +static const KeyID kKeyF15 = 0xEFCC; +static const KeyID kKeyF16 = 0xEFCD; +static const KeyID kKeyF17 = 0xEFCE; +static const KeyID kKeyF18 = 0xEFCF; +static const KeyID kKeyF19 = 0xEFD0; +static const KeyID kKeyF20 = 0xEFD1; +static const KeyID kKeyF21 = 0xEFD2; +static const KeyID kKeyF22 = 0xEFD3; +static const KeyID kKeyF23 = 0xEFD4; +static const KeyID kKeyF24 = 0xEFD5; +static const KeyID kKeyF25 = 0xEFD6; +static const KeyID kKeyF26 = 0xEFD7; +static const KeyID kKeyF27 = 0xEFD8; +static const KeyID kKeyF28 = 0xEFD9; +static const KeyID kKeyF29 = 0xEFDA; +static const KeyID kKeyF30 = 0xEFDB; +static const KeyID kKeyF31 = 0xEFDC; +static const KeyID kKeyF32 = 0xEFDD; +static const KeyID kKeyF33 = 0xEFDE; +static const KeyID kKeyF34 = 0xEFDF; +static const KeyID kKeyF35 = 0xEFE0; + +// modifiers +static const KeyID kKeyShift_L = 0xEFE1; /* Left shift */ +static const KeyID kKeyShift_R = 0xEFE2; /* Right shift */ +static const KeyID kKeyControl_L = 0xEFE3; /* Left control */ +static const KeyID kKeyControl_R = 0xEFE4; /* Right control */ +static const KeyID kKeyCapsLock = 0xEFE5; /* Caps lock */ +static const KeyID kKeyShiftLock = 0xEFE6; /* Shift lock */ +static const KeyID kKeyMeta_L = 0xEFE7; /* Left meta */ +static const KeyID kKeyMeta_R = 0xEFE8; /* Right meta */ +static const KeyID kKeyAlt_L = 0xEFE9; /* Left alt */ +static const KeyID kKeyAlt_R = 0xEFEA; /* Right alt */ +static const KeyID kKeySuper_L = 0xEFEB; /* Left super */ +static const KeyID kKeySuper_R = 0xEFEC; /* Right super */ +static const KeyID kKeyHyper_L = 0xEFED; /* Left hyper */ +static const KeyID kKeyHyper_R = 0xEFEE; /* Right hyper */ + +// multi-key character composition +static const KeyID kKeyCompose = 0xEF20; +static const KeyID kKeyDeadGrave = 0x0300; +static const KeyID kKeyDeadAcute = 0x0301; +static const KeyID kKeyDeadCircumflex = 0x0302; +static const KeyID kKeyDeadTilde = 0x0303; +static const KeyID kKeyDeadMacron = 0x0304; +static const KeyID kKeyDeadBreve = 0x0306; +static const KeyID kKeyDeadAbovedot = 0x0307; +static const KeyID kKeyDeadDiaeresis = 0x0308; +static const KeyID kKeyDeadAbovering = 0x030a; +static const KeyID kKeyDeadDoubleacute = 0x030b; +static const KeyID kKeyDeadCaron = 0x030c; +static const KeyID kKeyDeadCedilla = 0x0327; +static const KeyID kKeyDeadOgonek = 0x0328; + +// more function and modifier keys +static const KeyID kKeyLeftTab = 0xEE20; + +// update modifiers +static const KeyID kKeySetModifiers = 0xEE06; +static const KeyID kKeyClearModifiers = 0xEE07; + +// group change +static const KeyID kKeyNextGroup = 0xEE08; +static const KeyID kKeyPrevGroup = 0xEE0A; + +// extended keys +static const KeyID kKeyEject = 0xE001; +static const KeyID kKeySleep = 0xE05F; +static const KeyID kKeyWWWBack = 0xE0A6; +static const KeyID kKeyWWWForward = 0xE0A7; +static const KeyID kKeyWWWRefresh = 0xE0A8; +static const KeyID kKeyWWWStop = 0xE0A9; +static const KeyID kKeyWWWSearch = 0xE0AA; +static const KeyID kKeyWWWFavorites = 0xE0AB; +static const KeyID kKeyWWWHome = 0xE0AC; +static const KeyID kKeyAudioMute = 0xE0AD; +static const KeyID kKeyAudioDown = 0xE0AE; +static const KeyID kKeyAudioUp = 0xE0AF; +static const KeyID kKeyAudioNext = 0xE0B0; +static const KeyID kKeyAudioPrev = 0xE0B1; +static const KeyID kKeyAudioStop = 0xE0B2; +static const KeyID kKeyAudioPlay = 0xE0B3; +static const KeyID kKeyAppMail = 0xE0B4; +static const KeyID kKeyAppMedia = 0xE0B5; +static const KeyID kKeyAppUser1 = 0xE0B6; +static const KeyID kKeyAppUser2 = 0xE0B7; + +//@} + +struct KeyNameMapEntry { +public: + const char* m_name; + KeyID m_id; +}; +struct KeyModifierNameMapEntry { +public: + const char* m_name; + KeyModifierMask m_mask; +}; + +//! Key name to KeyID table +/*! +A table of key names to the corresponding KeyID. Only the keys listed +above plus non-alphanumeric ASCII characters are in the table. The end +of the table is the first pair with a NULL m_name. +*/ +extern const KeyNameMapEntry kKeyNameMap[]; + +//! Modifier key name to KeyModifierMask table +/*! +A table of modifier key names to the corresponding KeyModifierMask. +The end of the table is the first pair with a NULL m_name. +*/ +extern const KeyModifierNameMapEntry kModifierNameMap[]; + +#endif diff --git a/src/lib/synergy/MouseTypes.h b/src/lib/synergy/MouseTypes.h new file mode 100644 index 00000000..b72c7056 --- /dev/null +++ b/src/lib/synergy/MouseTypes.h @@ -0,0 +1,40 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef MOUSETYPES_H +#define MOUSETYPES_H + +#include "BasicTypes.h" + +//! Mouse button ID +/*! +Type to hold a mouse button identifier. +*/ +typedef UInt8 ButtonID; + +//! @name Mouse button identifiers +//@{ +static const ButtonID kButtonNone = 0; +static const ButtonID kButtonLeft = 1; +static const ButtonID kButtonMiddle = 2; +static const ButtonID kButtonRight = 3; +static const ButtonID kButtonExtra0 = 4; +//@} + +static const UInt8 NumButtonIDs = 5; + +#endif diff --git a/src/lib/synergy/OptionTypes.h b/src/lib/synergy/OptionTypes.h new file mode 100644 index 00000000..6515d2f5 --- /dev/null +++ b/src/lib/synergy/OptionTypes.h @@ -0,0 +1,100 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef OPTIONTYPES_H +#define OPTIONTYPES_H + +#include "BasicTypes.h" +#include "stdvector.h" + +//! Option ID +/*! +Type to hold an option identifier. +*/ +typedef UInt32 OptionID; + +//! Option Value +/*! +Type to hold an option value. +*/ +typedef SInt32 OptionValue; + +// for now, options are just pairs of integers +typedef std::vector COptionsList; + +// macro for packing 4 character strings into 4 byte integers +#define OPTION_CODE(_s) \ + (static_cast(static_cast(_s[0]) << 24) | \ + static_cast(static_cast(_s[1]) << 16) | \ + static_cast(static_cast(_s[2]) << 8) | \ + static_cast(static_cast(_s[3]) )) + +//! @name Option identifiers +//@{ +static const OptionID kOptionHalfDuplexCapsLock = OPTION_CODE("HDCL"); +static const OptionID kOptionHalfDuplexNumLock = OPTION_CODE("HDNL"); +static const OptionID kOptionHalfDuplexScrollLock = OPTION_CODE("HDSL"); +static const OptionID kOptionModifierMapForShift = OPTION_CODE("MMFS"); +static const OptionID kOptionModifierMapForControl = OPTION_CODE("MMFC"); +static const OptionID kOptionModifierMapForAlt = OPTION_CODE("MMFA"); +static const OptionID kOptionModifierMapForAltGr = OPTION_CODE("MMFG"); +static const OptionID kOptionModifierMapForMeta = OPTION_CODE("MMFM"); +static const OptionID kOptionModifierMapForSuper = OPTION_CODE("MMFR"); +static const OptionID kOptionHeartbeat = OPTION_CODE("HART"); +static const OptionID kOptionScreenSwitchCorners = OPTION_CODE("SSCM"); +static const OptionID kOptionScreenSwitchCornerSize = OPTION_CODE("SSCS"); +static const OptionID kOptionScreenSwitchDelay = OPTION_CODE("SSWT"); +static const OptionID kOptionScreenSwitchTwoTap = OPTION_CODE("SSTT"); +static const OptionID kOptionScreenSwitchNeedsShift = OPTION_CODE("SSNS"); +static const OptionID kOptionScreenSwitchNeedsControl = OPTION_CODE("SSNC"); +static const OptionID kOptionScreenSwitchNeedsAlt = OPTION_CODE("SSNA"); +static const OptionID kOptionScreenSaverSync = OPTION_CODE("SSVR"); +static const OptionID kOptionXTestXineramaUnaware = OPTION_CODE("XTXU"); +static const OptionID kOptionScreenPreserveFocus = OPTION_CODE("SFOC"); +static const OptionID kOptionRelativeMouseMoves = OPTION_CODE("MDLT"); +static const OptionID kOptionWin32KeepForeground = OPTION_CODE("_KFW"); +//@} + +//! @name Screen switch corner enumeration +//@{ +enum EScreenSwitchCorners { + kNoCorner, + kTopLeft, + kTopRight, + kBottomLeft, + kBottomRight, + kFirstCorner = kTopLeft, + kLastCorner = kBottomRight +}; +//@} + +//! @name Screen switch corner masks +//@{ +enum EScreenSwitchCornerMasks { + kNoCornerMask = 0, + kTopLeftMask = 1 << (kTopLeft - kFirstCorner), + kTopRightMask = 1 << (kTopRight - kFirstCorner), + kBottomLeftMask = 1 << (kBottomLeft - kFirstCorner), + kBottomRightMask = 1 << (kBottomRight - kFirstCorner), + kAllCornersMask = kTopLeftMask | kTopRightMask | + kBottomLeftMask | kBottomRightMask +}; +//@} + +#undef OPTION_CODE + +#endif diff --git a/src/lib/synergy/ProtocolTypes.cpp b/src/lib/synergy/ProtocolTypes.cpp new file mode 100644 index 00000000..7c50038b --- /dev/null +++ b/src/lib/synergy/ProtocolTypes.cpp @@ -0,0 +1,56 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "ProtocolTypes.h" + +const char* kMsgHello = "Synergy%2i%2i"; +const char* kMsgHelloBack = "Synergy%2i%2i%s"; +const char* kMsgCNoop = "CNOP"; +const char* kMsgCClose = "CBYE"; +const char* kMsgCEnter = "CINN%2i%2i%4i%2i"; +const char* kMsgCLeave = "COUT"; +const char* kMsgCClipboard = "CCLP%1i%4i"; +const char* kMsgCScreenSaver = "CSEC%1i"; +const char* kMsgCResetOptions = "CROP"; +const char* kMsgCInfoAck = "CIAK"; +const char* kMsgCKeepAlive = "CALV"; +const char* kMsgCGameTimingReq = "CGRQ"; +const char* kMsgCGameTimingResp = "CGRS%2i"; +const char* kMsgDKeyDown = "DKDN%2i%2i%2i"; +const char* kMsgDKeyDown1_0 = "DKDN%2i%2i"; +const char* kMsgDKeyRepeat = "DKRP%2i%2i%2i%2i"; +const char* kMsgDKeyRepeat1_0 = "DKRP%2i%2i%2i"; +const char* kMsgDKeyUp = "DKUP%2i%2i%2i"; +const char* kMsgDKeyUp1_0 = "DKUP%2i%2i"; +const char* kMsgDMouseDown = "DMDN%1i"; +const char* kMsgDMouseUp = "DMUP%1i"; +const char* kMsgDMouseMove = "DMMV%2i%2i"; +const char* kMsgDMouseRelMove = "DMRM%2i%2i"; +const char* kMsgDMouseWheel = "DMWM%2i%2i"; +const char* kMsgDMouseWheel1_0 = "DMWM%2i"; +const char* kMsgDClipboard = "DCLP%1i%4i%s"; +const char* kMsgDInfo = "DINF%2i%2i%2i%2i%2i%2i%2i"; +const char* kMsgDSetOptions = "DSOP%4I"; +const char* kMsgDGameButtons = "DGBT%1i%2i"; +const char* kMsgDGameSticks = "DGST%1i%2i%2i%2i%2i"; +const char* kMsgDGameTriggers = "DGTR%1i%1i%1i"; +const char* kMsgDGameFeedback = "DGFB%1i%2i%2i"; +const char* kMsgQInfo = "QINF"; +const char* kMsgEIncompatible = "EICV%2i%2i"; +const char* kMsgEBusy = "EBSY"; +const char* kMsgEUnknown = "EUNK"; +const char* kMsgEBad = "EBAD"; diff --git a/src/lib/synergy/ProtocolTypes.h b/src/lib/synergy/ProtocolTypes.h new file mode 100644 index 00000000..028729d4 --- /dev/null +++ b/src/lib/synergy/ProtocolTypes.h @@ -0,0 +1,349 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef PROTOCOLTYPES_H +#define PROTOCOLTYPES_H + +#include "BasicTypes.h" + +// protocol version number +// 1.0: initial protocol +// 1.1: adds KeyCode to key press, release, and repeat +// 1.2: adds mouse relative motion +// 1.3: adds keep alive and deprecates heartbeats, +// adds horizontal mouse scrolling +// 1.4: adds game device support +static const SInt16 kProtocolMajorVersion = 1; +static const SInt16 kProtocolMinorVersion = 4; + +// default contact port number +static const UInt16 kDefaultPort = 24800; + +// maximum total length for greeting returned by client +static const UInt32 kMaxHelloLength = 1024; + +// time between kMsgCKeepAlive (in seconds). a non-positive value disables +// keep alives. this is the default rate that can be overridden using an +// option. +static const double kKeepAliveRate = 3.0; + +// number of skipped kMsgCKeepAlive messages that indicates a problem +static const double kKeepAlivesUntilDeath = 3.0; + +// obsolete heartbeat stuff +static const double kHeartRate = -1.0; +static const double kHeartBeatsUntilDeath = 3.0; + +// direction constants +enum EDirection { + kNoDirection, + kLeft, + kRight, + kTop, + kBottom, + kFirstDirection = kLeft, + kLastDirection = kBottom, + kNumDirections = kLastDirection - kFirstDirection + 1 +}; +enum EDirectionMask { + kNoDirMask = 0, + kLeftMask = 1 << kLeft, + kRightMask = 1 << kRight, + kTopMask = 1 << kTop, + kBottomMask = 1 << kBottom +}; + + +// +// message codes (trailing NUL is not part of code). in comments, $n +// refers to the n'th argument (counting from one). message codes are +// always 4 bytes optionally followed by message specific parameters +// except those for the greeting handshake. +// + +// +// positions and sizes are signed 16 bit integers. +// + +// +// greeting handshake messages +// + +// say hello to client; primary -> secondary +// $1 = protocol major version number supported by server. $2 = +// protocol minor version number supported by server. +extern const char* kMsgHello; + +// respond to hello from server; secondary -> primary +// $1 = protocol major version number supported by client. $2 = +// protocol minor version number supported by client. $3 = client +// name. +extern const char* kMsgHelloBack; + + +// +// command codes +// + +// no operation; secondary -> primary +extern const char* kMsgCNoop; + +// close connection; primary -> secondary +extern const char* kMsgCClose; + +// enter screen: primary -> secondary +// entering screen at screen position $1 = x, $2 = y. x,y are +// absolute screen coordinates. $3 = sequence number, which is +// used to order messages between screens. the secondary screen +// must return this number with some messages. $4 = modifier key +// mask. this will have bits set for each toggle modifier key +// that is activated on entry to the screen. the secondary screen +// should adjust its toggle modifiers to reflect that state. +extern const char* kMsgCEnter; + +// leave screen: primary -> secondary +// leaving screen. the secondary screen should send clipboard +// data in response to this message for those clipboards that +// it has grabbed (i.e. has sent a kMsgCClipboard for and has +// not received a kMsgCClipboard for with a greater sequence +// number) and that were grabbed or have changed since the +// last leave. +extern const char* kMsgCLeave; + +// grab clipboard: primary <-> secondary +// sent by screen when some other app on that screen grabs a +// clipboard. $1 = the clipboard identifier, $2 = sequence number. +// secondary screens must use the sequence number passed in the +// most recent kMsgCEnter. the primary always sends 0. +extern const char* kMsgCClipboard; + +// screensaver change: primary -> secondary +// screensaver on primary has started ($1 == 1) or closed ($1 == 0) +extern const char* kMsgCScreenSaver; + +// reset options: primary -> secondary +// client should reset all of its options to their defaults. +extern const char* kMsgCResetOptions; + +// resolution change acknowledgment: primary -> secondary +// sent by primary in response to a secondary screen's kMsgDInfo. +// this is sent for every kMsgDInfo, whether or not the primary +// had sent a kMsgQInfo. +extern const char* kMsgCInfoAck; + +// keep connection alive: primary <-> secondary +// sent by the server periodically to verify that connections are still +// up and running. clients must reply in kind on receipt. if the server +// gets an error sending the message or does not receive a reply within +// a reasonable time then the server disconnects the client. if the +// client doesn't receive these (or any message) periodically then it +// should disconnect from the server. the appropriate interval is +// defined by an option. +extern const char* kMsgCKeepAlive; + +// game device timing: primary -> secondary +// periodically, sent from primary to secondary when game device device is polled. +// this causes a game timing response to be queued, which is dequeued when +// the device is next faked. +extern const char* kMsgCGameTimingReq; + +// game device timing: primary <- secondary +// in response, sent from secondary to primary when game device device is faked. +// the difference between when the message was sent and received is a +// measurement of time it took for the game device device state to reach the +// game device device user. a timing request is not retransmitted until after +// the pending timing response is received. +extern const char* kMsgCGameTimingResp; + +// +// data codes +// + +// key pressed: primary -> secondary +// $1 = KeyID, $2 = KeyModifierMask, $3 = KeyButton +// the KeyButton identifies the physical key on the primary used to +// generate this key. the secondary should note the KeyButton along +// with the physical key it uses to generate the key press. on +// release, the secondary can then use the primary's KeyButton to +// find its corresponding physical key and release it. this is +// necessary because the KeyID on release may not be the KeyID of +// the press. this can happen with combining (dead) keys or if +// the keyboard layouts are not identical and the user releases +// a modifier key before releasing the modified key. +extern const char* kMsgDKeyDown; + +// key pressed 1.0: same as above but without KeyButton +extern const char* kMsgDKeyDown1_0; + +// key auto-repeat: primary -> secondary +// $1 = KeyID, $2 = KeyModifierMask, $3 = number of repeats, $4 = KeyButton +extern const char* kMsgDKeyRepeat; + +// key auto-repeat 1.0: same as above but without KeyButton +extern const char* kMsgDKeyRepeat1_0; + +// key released: primary -> secondary +// $1 = KeyID, $2 = KeyModifierMask, $3 = KeyButton +extern const char* kMsgDKeyUp; + +// key released 1.0: same as above but without KeyButton +extern const char* kMsgDKeyUp1_0; + +// mouse button pressed: primary -> secondary +// $1 = ButtonID +extern const char* kMsgDMouseDown; + +// mouse button released: primary -> secondary +// $1 = ButtonID +extern const char* kMsgDMouseUp; + +// mouse moved: primary -> secondary +// $1 = x, $2 = y. x,y are absolute screen coordinates. +extern const char* kMsgDMouseMove; + +// relative mouse move: primary -> secondary +// $1 = dx, $2 = dy. dx,dy are motion deltas. +extern const char* kMsgDMouseRelMove; + +// mouse scroll: primary -> secondary +// $1 = xDelta, $2 = yDelta. the delta should be +120 for one tick forward +// (away from the user) or right and -120 for one tick backward (toward +// the user) or left. +extern const char* kMsgDMouseWheel; + +// mouse vertical scroll: primary -> secondary +// like as kMsgDMouseWheel except only sends $1 = yDelta. +extern const char* kMsgDMouseWheel1_0; + +// game device buttons: primary -> secondary +// $1 = device id +// $2 = buttons bit mask +extern const char* kMsgDGameButtons; + +// game device sticks: primary -> secondary +// $1 = device id +// $2 = x1 +// $3 = y1 +// $4 = x2 +// $5 = y2 +extern const char* kMsgDGameSticks; + +// game device triggers: primary -> secondary +// $1 = device id +// $2 = t1 +// $3 = t2 +extern const char* kMsgDGameTriggers; + +// game device feedback: secondary -> primary +// $1 = device id +// $2 = motor 1 +// $3 = motor 2 +extern const char* kMsgDGameFeedback; + +// clipboard data: primary <-> secondary +// $2 = sequence number, $3 = clipboard data. the sequence number +// is 0 when sent by the primary. secondary screens should use the +// sequence number from the most recent kMsgCEnter. $1 = clipboard +// identifier. +extern const char* kMsgDClipboard; + +// client data: secondary -> primary +// $1 = coordinate of leftmost pixel on secondary screen, +// $2 = coordinate of topmost pixel on secondary screen, +// $3 = width of secondary screen in pixels, +// $4 = height of secondary screen in pixels, +// $5 = size of warp zone, (obsolete) +// $6, $7 = the x,y position of the mouse on the secondary screen. +// +// the secondary screen must send this message in response to the +// kMsgQInfo message. it must also send this message when the +// screen's resolution changes. in this case, the secondary screen +// should ignore any kMsgDMouseMove messages until it receives a +// kMsgCInfoAck in order to prevent attempts to move the mouse off +// the new screen area. +extern const char* kMsgDInfo; + +// set options: primary -> secondary +// client should set the given option/value pairs. $1 = option/value +// pairs. +extern const char* kMsgDSetOptions; + +// +// query codes +// + +// query screen info: primary -> secondary +// client should reply with a kMsgDInfo. +extern const char* kMsgQInfo; + + +// +// error codes +// + +// incompatible versions: primary -> secondary +// $1 = major version of primary, $2 = minor version of primary. +extern const char* kMsgEIncompatible; + +// name provided when connecting is already in use: primary -> secondary +extern const char* kMsgEBusy; + +// unknown client: primary -> secondary +// name provided when connecting is not in primary's screen +// configuration map. +extern const char* kMsgEUnknown; + +// protocol violation: primary -> secondary +// primary should disconnect after sending this message. +extern const char* kMsgEBad; + + +// +// structures +// + +//! Screen information +/*! +This class contains information about a screen. +*/ +class CClientInfo { +public: + //! Screen position + /*! + The position of the upper-left corner of the screen. This is + typically 0,0. + */ + SInt32 m_x, m_y; + + //! Screen size + /*! + The size of the screen in pixels. + */ + SInt32 m_w, m_h; + + //! Obsolete (jump zone size) + SInt32 obsolete1; + + //! Mouse position + /*! + The current location of the mouse cursor. + */ + SInt32 m_mx, m_my; +}; + +#endif + diff --git a/src/lib/synergy/XScreen.cpp b/src/lib/synergy/XScreen.cpp new file mode 100644 index 00000000..bc1a4bd1 --- /dev/null +++ b/src/lib/synergy/XScreen.cpp @@ -0,0 +1,67 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "XScreen.h" + +// +// XScreenOpenFailure +// + +CString +XScreenOpenFailure::getWhat() const throw() +{ + return format("XScreenOpenFailure", "unable to open screen"); +} + + +// +// XScreenXInputFailure +// + +CString +XScreenXInputFailure::getWhat() const throw() +{ + return ""; +} + + +// +// XScreenUnavailable +// + +XScreenUnavailable::XScreenUnavailable(double timeUntilRetry) : + m_timeUntilRetry(timeUntilRetry) +{ + // do nothing +} + +XScreenUnavailable::~XScreenUnavailable() +{ + // do nothing +} + +double +XScreenUnavailable::getRetryTime() const +{ + return m_timeUntilRetry; +} + +CString +XScreenUnavailable::getWhat() const throw() +{ + return format("XScreenUnavailable", "unable to open screen"); +} diff --git a/src/lib/synergy/XScreen.h b/src/lib/synergy/XScreen.h new file mode 100644 index 00000000..00fc12b0 --- /dev/null +++ b/src/lib/synergy/XScreen.h @@ -0,0 +1,70 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef XSCREEN_H +#define XSCREEN_H + +#include "XBase.h" + +//! Generic screen exception +XBASE_SUBCLASS(XScreen, XBase); + +//! Cannot open screen exception +/*! +Thrown when a screen cannot be opened or initialized. +*/ +XBASE_SUBCLASS_WHAT(XScreenOpenFailure, XScreen); + +//! XInput exception +/*! +Thrown when an XInput error occurs +*/ +XBASE_SUBCLASS_WHAT(XScreenXInputFailure, XScreen); + +//! Screen unavailable exception +/*! +Thrown when a screen cannot be opened or initialized but retrying later +may be successful. +*/ +class XScreenUnavailable : public XScreenOpenFailure { +public: + /*! + \c timeUntilRetry is the suggested time the caller should wait until + trying to open the screen again. + */ + XScreenUnavailable(double timeUntilRetry); + virtual ~XScreenUnavailable(); + + //! @name manipulators + //@{ + + //! Get retry time + /*! + Returns the suggested time to wait until retrying to open the screen. + */ + double getRetryTime() const; + + //@} + +protected: + virtual CString getWhat() const throw(); + +private: + double m_timeUntilRetry; +}; + +#endif diff --git a/src/lib/synergy/XSynergy.cpp b/src/lib/synergy/XSynergy.cpp new file mode 100644 index 00000000..e3191c2e --- /dev/null +++ b/src/lib/synergy/XSynergy.cpp @@ -0,0 +1,132 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 "XSynergy.h" +#include "CStringUtil.h" + +// +// XBadClient +// + +CString +XBadClient::getWhat() const throw() +{ + return "XBadClient"; +} + + +// +// XIncompatibleClient +// + +XIncompatibleClient::XIncompatibleClient(int major, int minor) : + m_major(major), + m_minor(minor) +{ + // do nothing +} + +int +XIncompatibleClient::getMajor() const throw() +{ + return m_major; +} + +int +XIncompatibleClient::getMinor() const throw() +{ + return m_minor; +} + +CString +XIncompatibleClient::getWhat() const throw() +{ + return format("XIncompatibleClient", "incompatible client %{1}.%{2}", + CStringUtil::print("%d", m_major).c_str(), + CStringUtil::print("%d", m_minor).c_str()); +} + + +// +// XDuplicateClient +// + +XDuplicateClient::XDuplicateClient(const CString& name) : + m_name(name) +{ + // do nothing +} + +const CString& +XDuplicateClient::getName() const throw() +{ + return m_name; +} + +CString +XDuplicateClient::getWhat() const throw() +{ + return format("XDuplicateClient", "duplicate client %{1}", m_name.c_str()); +} + + +// +// XUnknownClient +// + +XUnknownClient::XUnknownClient(const CString& name) : + m_name(name) +{ + // do nothing +} + +const CString& +XUnknownClient::getName() const throw() +{ + return m_name; +} + +CString +XUnknownClient::getWhat() const throw() +{ + return format("XUnknownClient", "unknown client %{1}", m_name.c_str()); +} + + +// +// XExitApp +// + +XExitApp::XExitApp(int code) : + m_code(code) +{ + // do nothing +} + +int +XExitApp::getCode() const throw() +{ + return m_code; +} + +CString +XExitApp::getWhat() const throw() +{ + return format( + "XExitApp", "exiting with code %{1}", + CStringUtil::print("%d", m_code).c_str()); +} diff --git a/src/lib/synergy/XSynergy.h b/src/lib/synergy/XSynergy.h new file mode 100644 index 00000000..59b63964 --- /dev/null +++ b/src/lib/synergy/XSynergy.h @@ -0,0 +1,128 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2002 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef XSYNERGY_H +#define XSYNERGY_H + +#include "XBase.h" + +//! Generic synergy exception +XBASE_SUBCLASS(XSynergy, XBase); + +//! Client error exception +/*! +Thrown when the client fails to follow the protocol. +*/ +XBASE_SUBCLASS_WHAT(XBadClient, XSynergy); + +//! Incompatible client exception +/*! +Thrown when a client attempting to connect has an incompatible version. +*/ +class XIncompatibleClient : public XSynergy { +public: + XIncompatibleClient(int major, int minor); + + //! @name accessors + //@{ + + //! Get client's major version number + int getMajor() const throw(); + //! Get client's minor version number + int getMinor() const throw(); + + //@} + +protected: + virtual CString getWhat() const throw(); + +private: + int m_major; + int m_minor; +}; + +//! Client already connected exception +/*! +Thrown when a client attempting to connect is using the same name as +a client that is already connected. +*/ +class XDuplicateClient : public XSynergy { +public: + XDuplicateClient(const CString& name); + + //! @name accessors + //@{ + + //! Get client's name + virtual const CString& + getName() const throw(); + + //@} + +protected: + virtual CString getWhat() const throw(); + +private: + CString m_name; +}; + +//! Client not in map exception +/*! +Thrown when a client attempting to connect is using a name that is +unknown to the server. +*/ +class XUnknownClient : public XSynergy { +public: + XUnknownClient(const CString& name); + + //! @name accessors + //@{ + + //! Get the client's name + virtual const CString& + getName() const throw(); + + //@} + +protected: + virtual CString getWhat() const throw(); + +private: + CString m_name; +}; + +//! Generic exit eception +/*! +Thrown when we want to abort, with the opportunity to clean up. This is a +little bit of a hack, but it's a better way of exiting, than just calling +exit(int). +*/ +class XExitApp : public XSynergy { +public: + XExitApp(int code); + + //! Get the exit code + int getCode() const throw(); + +protected: + virtual CString getWhat() const throw(); + +private: + int m_code; +}; + +#endif diff --git a/src/plugin/CMakeLists.txt b/src/plugin/CMakeLists.txt new file mode 100644 index 00000000..194031b8 --- /dev/null +++ b/src/plugin/CMakeLists.txt @@ -0,0 +1,18 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2012 Nick Bolton +# +# 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 COPYING 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 . + +if (WIN32) + add_subdirectory(winmmjoy) +endif() diff --git a/src/plugin/winmmjoy/CMakeLists.txt b/src/plugin/winmmjoy/CMakeLists.txt new file mode 100644 index 00000000..30d0c576 --- /dev/null +++ b/src/plugin/winmmjoy/CMakeLists.txt @@ -0,0 +1,33 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2012 Nick Bolton +# +# 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 COPYING 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 . + +set(inc + winmmjoy.h +) + +set(src + winmmjoy.cpp + ${inc} +) + +add_library(winmmjoy SHARED ${inc} ${src}) + +add_custom_command( + TARGET winmmjoy + POST_BUILD + COMMAND xcopy /Y /Q + ..\\..\\..\\..\\lib\\${CMAKE_CFG_INTDIR}\\winmmjoy.* + ..\\..\\..\\..\\bin\\${CMAKE_CFG_INTDIR}\\plugins\\ +) diff --git a/src/plugin/winmmjoy/winmmjoy.cpp b/src/plugin/winmmjoy/winmmjoy.cpp new file mode 100644 index 00000000..a945e34e --- /dev/null +++ b/src/plugin/winmmjoy/winmmjoy.cpp @@ -0,0 +1,103 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "winmmjoy.h" + +#include +#include +#include + +#pragma comment(lib, "winmm.lib") + +std::stringstream _logStream; +#define LOG(s) \ + _logStream.str(""); \ + _logStream << "winmmjoy: " << s << std::endl; \ + s_log( _logStream.str().c_str()) + +static bool s_running = true; +static void (*s_sendEvent)(const char*, void*) = NULL; +static void (*s_log)(const char*) = NULL; + +extern "C" { + +int +init(void (*sendEvent)(const char*, void*), void (*log)(const char*)) +{ + s_sendEvent = sendEvent; + s_log = log; + LOG("init"); + CreateThread(NULL, 0, mainLoop, NULL, 0, NULL); + return 0; +} + +int +cleanup() +{ + LOG("cleanup"); + s_running = false; + return 0; +} + +} + +DWORD WINAPI +mainLoop(void* data) +{ + const char* buttonsEvent = "IPrimaryScreen::getGameDeviceButtonsEvent"; + const char* sticksEvent = "IPrimaryScreen::getGameDeviceSticksEvent"; + const char* triggersEvent = "IPrimaryScreen::getGameDeviceTriggersEvent"; + + JOYINFOEX joyInfo; + ZeroMemory(&joyInfo, sizeof(joyInfo)); + joyInfo.dwSize = sizeof(joyInfo); + joyInfo.dwFlags = JOY_RETURNALL; + + // note: synergy data is often 16-bit, where winmm is 32-bit. + UINT index = JOYSTICKID1; + DWORD buttons, buttonsLast = 0; + DWORD xPos, xPosLast = 0; + DWORD yPos, yPosLast = 0; + + while (s_running) { + + if (joyGetPosEx(index, &joyInfo) != JOYERR_NOERROR) { + Sleep(1000); + continue; + } + + buttons = joyInfo.dwButtons; + xPos = joyInfo.dwXpos; + yPos = joyInfo.dwYpos; + + if (buttons != buttonsLast) { + s_sendEvent(buttonsEvent, + new CGameDeviceButtonInfo(index, (GameDeviceButton)joyInfo.dwButtons)); + } + + if (xPos != xPosLast || yPos != yPosLast) { + s_sendEvent(sticksEvent, + new CGameDeviceStickInfo(index, (short)xPos, (short)yPos, 0, 0)); + } + + buttonsLast = buttons; + xPosLast = xPos; + yPosLast = yPos; + Sleep(1); + } + return 0; +} diff --git a/src/plugin/winmmjoy/winmmjoy.h b/src/plugin/winmmjoy/winmmjoy.h new file mode 100644 index 00000000..fe9d8c79 --- /dev/null +++ b/src/plugin/winmmjoy/winmmjoy.h @@ -0,0 +1,70 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 + +#define WIN32_LEAN_AND_MEAN +#include + +#if defined(winmmjoy_EXPORTS) +#define WINMMJOY_API __declspec(dllexport) +#else +#define WINMMJOY_API __declspec(dllimport) +#endif + +extern "C" { + +WINMMJOY_API int init(void (*sendEvent)(const char*, void*), void (*log)(const char*)); +WINMMJOY_API int cleanup(); + +} + +DWORD WINAPI mainLoop(void* data); + +typedef unsigned char GameDeviceID; +typedef unsigned short GameDeviceButton; + +class CGameDeviceButtonInfo { +public: + CGameDeviceButtonInfo(GameDeviceID id, GameDeviceButton buttons) : + m_id(id), m_buttons(buttons) { } +public: + GameDeviceID m_id; + GameDeviceButton m_buttons; +}; + +class CGameDeviceStickInfo { +public: + CGameDeviceStickInfo(GameDeviceID id, short x1, short y1, short x2, short y2) : + m_id(id), m_x1(x1), m_x2(x2), m_y1(y1), m_y2(y2) { } +public: + GameDeviceID m_id; + short m_x1; + short m_x2; + short m_y1; + short m_y2; +}; + +class CGameDeviceTriggerInfo { +public: + CGameDeviceTriggerInfo(GameDeviceID id, unsigned char t1, unsigned char t2) : + m_id(id), m_t1(t1), m_t2(t2) { } +public: + GameDeviceID m_id; + unsigned char m_t1; + unsigned char m_t2; +}; diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt new file mode 100644 index 00000000..b0725df8 --- /dev/null +++ b/src/test/CMakeLists.txt @@ -0,0 +1,26 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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_directories( + ../../tools/gtest-1.6.0 + ../../tools/gtest-1.6.0/include + ../../tools/gmock-1.6.0 + ../../tools/gmock-1.6.0/include) + +add_library(gtest STATIC ../../tools/gtest-1.6.0/src/gtest-all.cc) +add_library(gmock STATIC ../../tools/gmock-1.6.0/src/gmock-all.cc) + +add_subdirectory(integtests) +add_subdirectory(unittests) diff --git a/src/test/guitests/guitests.pro b/src/test/guitests/guitests.pro new file mode 100644 index 00000000..dc9986a4 --- /dev/null +++ b/src/test/guitests/guitests.pro @@ -0,0 +1,17 @@ +QT += network +QT -= gui +TARGET = guitests +CONFIG += qtestlib +CONFIG += console +CONFIG -= app_bundle +TEMPLATE = app +INCLUDEPATH += ../../gui/src +SOURCES += src/main.cpp \ + src/VersionCheckerTests.cpp +HEADERS += src/VersionCheckerTests.h \ + src/VersionChecker.h +win32 { + Debug:DESTDIR = ../../../bin/Debug + Release:DESTDIR = ../../../bin/Release +} +else:DESTDIR = ../../../bin diff --git a/src/test/guitests/src/VersionCheckerTests.cpp b/src/test/guitests/src/VersionCheckerTests.cpp new file mode 100644 index 00000000..2cd7659e --- /dev/null +++ b/src/test/guitests/src/VersionCheckerTests.cpp @@ -0,0 +1,46 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "VersionCheckerTests.h" +#include "VersionChecker.cpp" +#include "../../gui/tmp/debug/moc_VersionChecker.cpp" + +#include + +void VersionCheckerTests::compareVersions() +{ + VersionChecker versionChecker; + + // compare majors + QCOMPARE(versionChecker.compareVersions("1.0.0", "2.0.0"), 1); + QCOMPARE(versionChecker.compareVersions("2.0.0", "1.0.0"), -1); + QCOMPARE(versionChecker.compareVersions("1.0.0", "1.0.0"), 0); + QCOMPARE(versionChecker.compareVersions("1.4.8", "2.4.7"), 1); + QCOMPARE(versionChecker.compareVersions("2.4.7", "1.4.8"), -1); + + // compare minors + QCOMPARE(versionChecker.compareVersions("1.3.0", "1.4.0"), 1); + QCOMPARE(versionChecker.compareVersions("1.4.0", "1.3.0"), -1); + QCOMPARE(versionChecker.compareVersions("1.4.0", "1.4.0"), 0); + QCOMPARE(versionChecker.compareVersions("1.3.8", "1.4.7"), 1); + QCOMPARE(versionChecker.compareVersions("1.4.7", "1.3.8"), -1); + + // compare revs + QCOMPARE(versionChecker.compareVersions("1.4.7", "1.4.8"), 1); + QCOMPARE(versionChecker.compareVersions("1.4.8", "1.4.7"), -1); + QCOMPARE(versionChecker.compareVersions("1.4.7", "1.4.7"), 0); +} diff --git a/src/test/guitests/src/VersionCheckerTests.h b/src/test/guitests/src/VersionCheckerTests.h new file mode 100644 index 00000000..865469a9 --- /dev/null +++ b/src/test/guitests/src/VersionCheckerTests.h @@ -0,0 +1,25 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 "QObject.h" + +class VersionCheckerTests : public QObject +{ + Q_OBJECT +private slots: + void compareVersions(); +}; diff --git a/src/test/guitests/src/main.cpp b/src/test/guitests/src/main.cpp new file mode 100644 index 00000000..aec3b538 --- /dev/null +++ b/src/test/guitests/src/main.cpp @@ -0,0 +1,25 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Nick Bolton + * + * 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 COPYING 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 +#include "VersionCheckerTests.h" + +int main(int argc, char *argv[]) +{ + VersionCheckerTests versionCheckerTests; + QTest::qExec(&versionCheckerTests, argc, argv); +} diff --git a/src/test/integtests/CMakeLists.txt b/src/test/integtests/CMakeLists.txt new file mode 100644 index 00000000..c29d72b0 --- /dev/null +++ b/src/test/integtests/CMakeLists.txt @@ -0,0 +1,79 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(src + Main.cpp +) + +if (WIN32) + + # windows + list(APPEND src + platform/CMSWindowsClipboardTests.cpp + platform/CMSWindowsKeyStateTests.cpp + ) + +elseif (APPLE) + + # mac + list(APPEND src + platform/COSXClipboardTests.cpp + platform/COSXKeyStateTests.cpp + ) + +elseif (UNIX) + + # unix/linux + list(APPEND src + platform/CXWindowsClipboardTests.cpp + platform/CXWindowsKeyStateTests.cpp + platform/CXWindowsScreenTests.cpp + platform/CXWindowsScreenSaverTests.cpp + ) + +endif() + +set(inc + ../../lib/arch + ../../lib/base + ../../lib/client + ../../lib/common + ../../lib/io + ../../lib/mt + ../../lib/net + ../../lib/platform + ../../lib/synergy + ../../../tools/gtest-1.6.0/include + ../../../tools/gmock-1.6.0/include + ../unittests + ../unittests/synergy +) + +if (UNIX) + list(APPEND inc + ../../.. + ) +endif() + +if (WIN32) + if (GAME_DEVICE_SUPPORT) + link_directories("$ENV{DXSDK_DIR}/Lib/x86") + endif() +endif() + +include_directories(${inc}) +add_executable(integtests ${src}) +target_link_libraries(integtests + arch base client common io mt net platform server synergy gtest gmock ${libs}) diff --git a/src/test/integtests/Main.cpp b/src/test/integtests/Main.cpp new file mode 100644 index 00000000..680ebd27 --- /dev/null +++ b/src/test/integtests/Main.cpp @@ -0,0 +1,96 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include +#include +#include "CArch.h" +#include "CLog.h" + +#if SYSAPI_WIN32 +#include "CArchMiscWindows.h" +#endif + +#define LOCK_TIMEOUT 30 + +using namespace std; + +void lock(string lockFile); +void unlock(string lockFile); + +int +main(int argc, char **argv) +{ +#if SYSAPI_WIN32 + // record window instance for tray icon, etc + CArchMiscWindows::setInstanceWin32(GetModuleHandle(NULL)); +#endif + + string lockFile; + for (int i = 0; i < argc; i++) { + if (string(argv[i]).compare("--lock-file") == 0) { + lockFile = argv[i + 1]; + } + } + + if (!lockFile.empty()) { + lock(lockFile); + } + + CLOG->setFilter(kDEBUG2); + + testing::InitGoogleTest(&argc, argv); + + int result = RUN_ALL_TESTS(); + + if (!lockFile.empty()) { + unlock(lockFile); + } + + return result; +} + +void +lock(string lockFile) +{ + double start = ARCH->time(); + + // keep checking until timeout is reached. + while ((ARCH->time() - start) < LOCK_TIMEOUT) { + + ifstream is(lockFile.c_str()); + bool noLock = !is; + is.close(); + + if (noLock) { + break; + } + + // check every second if file has gone. + ARCH->sleep(1); + } + + // write empty lock file. + ofstream os(lockFile.c_str()); + os.close(); +} + +void +unlock(string lockFile) +{ + remove(lockFile.c_str()); +} diff --git a/src/test/integtests/platform/CMSWindowsClipboardTests.cpp b/src/test/integtests/platform/CMSWindowsClipboardTests.cpp new file mode 100644 index 00000000..e3779666 --- /dev/null +++ b/src/test/integtests/platform/CMSWindowsClipboardTests.cpp @@ -0,0 +1,227 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include +#include "CMSWindowsClipboard.h" +#include "IMSWindowsClipboardFacade.h" + +class CMSWindowsClipboardTests : public ::testing::Test +{ +protected: + virtual void SetUp() + { + emptyClipboard(); + } + + virtual void TearDown() + { + emptyClipboard(); + } + +private: + void emptyClipboard() + { + CMSWindowsClipboard clipboard(NULL); + clipboard.open(0); + clipboard.empty(); + } +}; + +class MockFacade : public IMSWindowsClipboardFacade +{ +public: + MOCK_METHOD2(write, void(HANDLE, UINT)); +}; + +TEST_F(CMSWindowsClipboardTests, emptyUnowned_openCalled_returnsTrue) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(0); + + bool actual = clipboard.emptyUnowned(); + + EXPECT_EQ(true, actual); +} + +TEST_F(CMSWindowsClipboardTests, empty_openCalled_returnsTrue) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(0); + + bool actual = clipboard.empty(); + + EXPECT_EQ(true, actual); +} + +TEST_F(CMSWindowsClipboardTests, empty_singleFormat_hasReturnsFalse) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(0); + clipboard.add(CMSWindowsClipboard::kText, "synergy rocks!"); + + clipboard.empty(); + + bool actual = clipboard.has(CMSWindowsClipboard::kText); + EXPECT_EQ(false, actual); +} + +TEST_F(CMSWindowsClipboardTests, add_newValue_valueWasStored) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(0); + + clipboard.add(IClipboard::kText, "synergy rocks!"); + + CString actual = clipboard.get(IClipboard::kText); + EXPECT_EQ("synergy rocks!", actual); +} + +TEST_F(CMSWindowsClipboardTests, add_newValue_writeWasCalled) +{ + MockFacade facade; + EXPECT_CALL(facade, write(testing::_, testing::_)); + + CMSWindowsClipboard clipboard(NULL); + clipboard.setFacade(facade); + clipboard.open(0); + + clipboard.add(IClipboard::kText, "synergy rocks!"); +} + +TEST_F(CMSWindowsClipboardTests, add_replaceValue_valueWasReplaced) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(0); + + clipboard.add(IClipboard::kText, "synergy rocks!"); + clipboard.add(IClipboard::kText, "maxivista sucks"); // haha, just kidding. + + CString actual = clipboard.get(IClipboard::kText); + EXPECT_EQ("maxivista sucks", actual); +} + +TEST_F(CMSWindowsClipboardTests, open_timeIsZero_returnsTrue) +{ + CMSWindowsClipboard clipboard(NULL); + + bool actual = clipboard.open(0); + + EXPECT_EQ(true, actual); +} + +TEST_F(CMSWindowsClipboardTests, open_timeIsOne_returnsTrue) +{ + CMSWindowsClipboard clipboard(NULL); + + bool actual = clipboard.open(1); + + EXPECT_EQ(true, actual); +} + +TEST_F(CMSWindowsClipboardTests, close_isOpen_noErrors) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(0); + + clipboard.close(); + + // can't assert anything +} + +// looks like this test may fail intermittently: +// * http://buildbot.synergy-foss.org:8000/builders/trunk-win32/builds/246/steps/shell_3/logs/stdio +/*TEST_F(CMSWindowsClipboardTests, getTime_openWithNoEmpty_returnsOne) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(1); + + CMSWindowsClipboard::Time actual = clipboard.getTime(); + + // this behavior is different to that of CClipboard which only + // returns the value passed into open(t) after empty() is called. + EXPECT_EQ(1, actual); +}*/ + +// this also fails intermittently: +// http://buildbot.synergy-foss.org:8000/builders/trunk-win32/builds/266/steps/shell_3/logs/stdio +/*TEST_F(CMSWindowsClipboardTests, getTime_openAndEmpty_returnsOne) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(1); + clipboard.empty(); + + CMSWindowsClipboard::Time actual = clipboard.getTime(); + + EXPECT_EQ(1, actual); +}*/ + +TEST_F(CMSWindowsClipboardTests, has_withFormatAdded_returnsTrue) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(0); + clipboard.empty(); + clipboard.add(IClipboard::kText, "synergy rocks!"); + + bool actual = clipboard.has(IClipboard::kText); + + EXPECT_EQ(true, actual); +} + +TEST_F(CMSWindowsClipboardTests, has_withNoFormats_returnsFalse) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(0); + clipboard.empty(); + + bool actual = clipboard.has(IClipboard::kText); + + EXPECT_EQ(false, actual); +} + +TEST_F(CMSWindowsClipboardTests, get_withNoFormats_returnsEmpty) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(0); + clipboard.empty(); + + CString actual = clipboard.get(IClipboard::kText); + + EXPECT_EQ("", actual); +} + +TEST_F(CMSWindowsClipboardTests, get_withFormatAdded_returnsExpected) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(0); + clipboard.empty(); + clipboard.add(IClipboard::kText, "synergy rocks!"); + + CString actual = clipboard.get(IClipboard::kText); + + EXPECT_EQ("synergy rocks!", actual); +} + +TEST_F(CMSWindowsClipboardTests, isOwnedBySynergy_defaultState_noError) +{ + CMSWindowsClipboard clipboard(NULL); + clipboard.open(0); + + bool actual = clipboard.isOwnedBySynergy(); + + EXPECT_EQ(true, actual); +} diff --git a/src/test/integtests/platform/CMSWindowsKeyStateTests.cpp b/src/test/integtests/platform/CMSWindowsKeyStateTests.cpp new file mode 100644 index 00000000..02957be3 --- /dev/null +++ b/src/test/integtests/platform/CMSWindowsKeyStateTests.cpp @@ -0,0 +1,134 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include +#include "CMSWindowsKeyState.h" +#include "CMSWindowsDesks.h" +#include "CMSWindowsScreen.h" +#include "CMSWindowsScreenSaver.h" +#include "TMethodJob.h" +#include "CMockEventQueue.h" +#include "CMockKeyMap.h" + +// wParam = flags, HIBYTE(lParam) = virtual key, LOBYTE(lParam) = scan code +#define SYNERGY_MSG_FAKE_KEY SYNERGY_HOOK_LAST_MSG + 4 + +using ::testing::_; +using ::testing::NiceMock; + +class CMSWindowsKeyStateTests : public ::testing::Test +{ +protected: + virtual void SetUp() + { + // load synrgyhk.dll + m_hookLibrary = m_hookLibraryLoader.openHookLibrary("synrgyhk"); + m_screensaver = new CMSWindowsScreenSaver(); + m_desks = new CMSWindowsDesks( + true, false, m_hookLibrary, m_screensaver, + new TMethodJob( + this, &CMSWindowsKeyStateTests::updateKeysCB)); + } + + virtual void TearDown() + { + delete m_screensaver; + delete m_desks; + } + + CMSWindowsDesks* getDesks() const + { + return m_desks; + } + + void* getEventTarget() const + { + return const_cast(this); + } + +private: + void updateKeysCB(void*) { } + HINSTANCE m_hookLibrary; + IScreenSaver* m_screensaver; + CMSWindowsDesks* m_desks; + CMSWindowsHookLibraryLoader m_hookLibraryLoader; +}; + +TEST_F(CMSWindowsKeyStateTests, disable_nonWin95OS_eventQueueNotUsed) +{ + NiceMock eventQueue; + CMockKeyMap keyMap; + CMSWindowsKeyState keyState(getDesks(), getEventTarget(), eventQueue, keyMap); + + // in anything above win95-family, event handler should not be called. + EXPECT_CALL(eventQueue, removeHandler(_, _)).Times(0); + + keyState.disable(); +} + +TEST_F(CMSWindowsKeyStateTests, testAutoRepeat_noRepeatAndButtonIsZero_resultIsTrue) +{ + NiceMock eventQueue; + CMockKeyMap keyMap; + CMSWindowsKeyState keyState(getDesks(), getEventTarget(), eventQueue, keyMap); + keyState.setLastDown(1); + + bool actual = keyState.testAutoRepeat(true, false, 1); + + ASSERT_TRUE(actual); +} + +TEST_F(CMSWindowsKeyStateTests, testAutoRepeat_pressFalse_lastDownIsZero) +{ + NiceMock eventQueue; + CMockKeyMap keyMap; + CMSWindowsKeyState keyState(getDesks(), getEventTarget(), eventQueue, keyMap); + keyState.setLastDown(1); + + keyState.testAutoRepeat(false, false, 1); + + ASSERT_EQ(0, keyState.getLastDown()); +} + +TEST_F(CMSWindowsKeyStateTests, saveModifiers_noModifiers_savedModifiers0) +{ + NiceMock eventQueue; + CMockKeyMap keyMap; + CMSWindowsKeyState keyState(getDesks(), getEventTarget(), eventQueue, keyMap); + + keyState.saveModifiers(); + + ASSERT_EQ(0, keyState.getSavedModifiers()); +} + +/* +// TODO: fix Assertion failed: s_instance != NULL, +// file ..\..\..\..\src\lib\base\IEventQueue.cpp, line 37 +TEST_F(CMSWindowsKeyStateTests, saveModifiers_shiftKeyDown_savedModifiers4) +{ + NiceMock eventQueue; + CMockKeyMap keyMap; + CMSWindowsKeyState keyState(getDesks(), getEventTarget(), eventQueue, keyMap); + getDesks()->enable(); + getDesks()->fakeKeyEvent(1, 1, true, false); + + keyState.saveModifiers(); + + ASSERT_EQ(1, keyState.getSavedModifiers()); +} +*/ diff --git a/src/test/integtests/platform/COSXClipboardTests.cpp b/src/test/integtests/platform/COSXClipboardTests.cpp new file mode 100644 index 00000000..f9e6a19a --- /dev/null +++ b/src/test/integtests/platform/COSXClipboardTests.cpp @@ -0,0 +1,162 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include +#include "COSXClipboard.h" + +TEST(COSXClipboardTests, empty_openCalled_returnsTrue) +{ + COSXClipboard clipboard; + clipboard.open(0); + + bool actual = clipboard.empty(); + + EXPECT_EQ(true, actual); +} + +TEST(COSXClipboardTests, empty_singleFormat_hasReturnsFalse) +{ + COSXClipboard clipboard; + clipboard.open(0); + clipboard.add(COSXClipboard::kText, "synergy rocks!"); + + clipboard.empty(); + + bool actual = clipboard.has(COSXClipboard::kText); + EXPECT_EQ(false, actual); +} + +TEST(COSXClipboardTests, add_newValue_valueWasStored) +{ + COSXClipboard clipboard; + clipboard.open(0); + + clipboard.add(IClipboard::kText, "synergy rocks!"); + + CString actual = clipboard.get(IClipboard::kText); + EXPECT_EQ("synergy rocks!", actual); +} + +TEST(COSXClipboardTests, add_replaceValue_valueWasReplaced) +{ + COSXClipboard clipboard; + clipboard.open(0); + + clipboard.add(IClipboard::kText, "synergy rocks!"); + clipboard.add(IClipboard::kText, "maxivista sucks"); // haha, just kidding. + + CString actual = clipboard.get(IClipboard::kText); + EXPECT_EQ("maxivista sucks", actual); +} + +TEST(COSXClipboardTests, open_timeIsZero_returnsTrue) +{ + COSXClipboard clipboard; + + bool actual = clipboard.open(0); + + EXPECT_EQ(true, actual); +} + +TEST(COSXClipboardTests, open_timeIsOne_returnsTrue) +{ + COSXClipboard clipboard; + + bool actual = clipboard.open(1); + + EXPECT_EQ(true, actual); +} + +TEST(COSXClipboardTests, close_isOpen_noErrors) +{ + COSXClipboard clipboard; + clipboard.open(0); + + clipboard.close(); + + // can't assert anything +} + +TEST(COSXClipboardTests, getTime_openWithNoEmpty_returnsOne) +{ + COSXClipboard clipboard; + clipboard.open(1); + + COSXClipboard::Time actual = clipboard.getTime(); + + // this behavior is different to that of CClipboard which only + // returns the value passed into open(t) after empty() is called. + EXPECT_EQ((UInt32)1, actual); +} + +TEST(COSXClipboardTests, getTime_openAndEmpty_returnsOne) +{ + COSXClipboard clipboard; + clipboard.open(1); + clipboard.empty(); + + COSXClipboard::Time actual = clipboard.getTime(); + + EXPECT_EQ((UInt32)1, actual); +} + +TEST(COSXClipboardTests, has_withFormatAdded_returnsTrue) +{ + COSXClipboard clipboard; + clipboard.open(0); + clipboard.empty(); + clipboard.add(IClipboard::kText, "synergy rocks!"); + + bool actual = clipboard.has(IClipboard::kText); + + EXPECT_EQ(true, actual); +} + +TEST(COSXClipboardTests, has_withNoFormats_returnsFalse) +{ + COSXClipboard clipboard; + clipboard.open(0); + clipboard.empty(); + + bool actual = clipboard.has(IClipboard::kText); + + EXPECT_EQ(false, actual); +} + +TEST(COSXClipboardTests, get_withNoFormats_returnsEmpty) +{ + COSXClipboard clipboard; + clipboard.open(0); + clipboard.empty(); + + CString actual = clipboard.get(IClipboard::kText); + + EXPECT_EQ("", actual); +} + +TEST(COSXClipboardTests, get_withFormatAdded_returnsExpected) +{ + COSXClipboard clipboard; + clipboard.open(0); + clipboard.empty(); + clipboard.add(IClipboard::kText, "synergy rocks!"); + + CString actual = clipboard.get(IClipboard::kText); + + EXPECT_EQ("synergy rocks!", actual); +} diff --git a/src/test/integtests/platform/COSXKeyStateTests.cpp b/src/test/integtests/platform/COSXKeyStateTests.cpp new file mode 100644 index 00000000..e777ad3c --- /dev/null +++ b/src/test/integtests/platform/COSXKeyStateTests.cpp @@ -0,0 +1,98 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include + +#include "COSXKeyState.h" +#include "CMockKeyMap.h" +#include "CMockEventQueue.h" + +CGKeyCode escKeyCode = 53; +CGKeyCode shiftKeyCode = 56; +CGKeyCode controlKeyCode = 59; + +// TODO: make pollActiveModifiers tests work reliably. +/* +TEST(COSXKeyStateTests, pollActiveModifiers_shiftKeyDownThenUp_masksAreCorrect) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + COSXKeyState keyState((IEventQueue&)keyMap, (CKeyMap&)eventQueue); + + // fake shift key down (without using synergy). this is a bit weird; + // looks like you need to create a shift down event *and* set the + // shift modifier. + CGEventRef shiftDown = CGEventCreateKeyboardEvent(NULL, shiftKeyCode, true); + CGEventSetFlags(shiftDown, kCGEventFlagMaskShift); + CGEventPost(kCGHIDEventTap, shiftDown); + CFRelease(shiftDown); + + // function under test (1st call) + KeyModifierMask downMask = keyState.pollActiveModifiers(); + + // fake shift key up (without using synergy). also as weird as the + // shift down; use a non-shift key down and reset the pressed modifiers. + CGEventRef shiftUp = CGEventCreateKeyboardEvent(NULL, escKeyCode, true); + CGEventSetFlags(shiftUp, 0); + CGEventPost(kCGHIDEventTap, shiftUp); + CFRelease(shiftUp); + + // function under test (2nd call) + KeyModifierMask upMask = keyState.pollActiveModifiers(); + + EXPECT_TRUE((downMask & KeyModifierShift) == KeyModifierShift) + << "shift key not in mask (" << downMask << ") - key was not pressed"; + + EXPECT_TRUE((upMask & KeyModifierShift) == 0) + << "shift key still in mask (" << upMask << ") - make sure no keys are being held down"; +} + +TEST(COSXKeyStateTests, pollActiveModifiers_controlKeyDownThenUp_masksAreCorrect) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + COSXKeyState keyState((IEventQueue&)keyMap, (CKeyMap&)eventQueue); + + // fake control key down (without using synergy). this is a bit weird; + // looks like you need to create a shift down event *and* set the + // shift modifier. + CGEventRef controlDown = CGEventCreateKeyboardEvent(NULL, controlKeyCode, true); + CGEventSetFlags(controlDown, kCGEventFlagMaskControl); + CGEventPost(kCGHIDEventTap, controlDown); + CFRelease(controlDown); + + // function under test (1st call) + KeyModifierMask downMask = keyState.pollActiveModifiers(); + + // fake control key up (without using synergy). also as weird as the + // shift down; use a non-shift key down and reset the pressed modifiers. + CGEventRef controlUp = CGEventCreateKeyboardEvent(NULL, escKeyCode, true); + CGEventSetFlags(controlUp, 0); + CGEventPost(kCGHIDEventTap, controlUp); + CFRelease(controlUp); + + // function under test (2nd call) + KeyModifierMask upMask = keyState.pollActiveModifiers(); + + EXPECT_TRUE((downMask & KeyModifierControl) == KeyModifierControl) + << "control key not in mask (" << downMask << ") - key was not pressed"; + + EXPECT_TRUE((upMask & KeyModifierControl) == 0) + << "control key still in mask (" << upMask << ") - make sure no keys are being held down"; +} +*/ diff --git a/src/test/integtests/platform/CXWindowsClipboardTests.cpp b/src/test/integtests/platform/CXWindowsClipboardTests.cpp new file mode 100644 index 00000000..3565f938 --- /dev/null +++ b/src/test/integtests/platform/CXWindowsClipboardTests.cpp @@ -0,0 +1,150 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include +#include "CXWindowsClipboard.h" + +class CXWindowsClipboardTests : public ::testing::Test +{ +protected: + virtual void + SetUp() + { + m_display = XOpenDisplay(NULL); + int screen = DefaultScreen(m_display); + Window root = XRootWindow(m_display, screen); + + XSetWindowAttributes attr; + attr.do_not_propagate_mask = 0; + attr.override_redirect = True; + attr.cursor = Cursor(); + + m_window = XCreateWindow( + m_display, root, 0, 0, 1, 1, 0, 0, + InputOnly, CopyFromParent, 0, &attr); + } + + virtual void + TearDown() + { + XDestroyWindow(m_display, m_window); + XCloseDisplay(m_display); + } + + CXWindowsClipboard& + createClipboard() + { + CXWindowsClipboard* clipboard; + clipboard = new CXWindowsClipboard(m_display, m_window, 0); + clipboard->open(0); // needed to empty the clipboard + clipboard->empty(); // needed to own the clipboard + return *clipboard; + } + + Display* m_display; + Window m_window; +}; + +TEST_F(CXWindowsClipboardTests, empty_openCalled_returnsTrue) +{ + CXWindowsClipboard clipboard = createClipboard(); + + bool actual = clipboard.empty(); + + EXPECT_EQ(true, actual); +} + +TEST_F(CXWindowsClipboardTests, empty_singleFormat_hasReturnsFalse) +{ + CXWindowsClipboard clipboard = createClipboard(); + clipboard.add(CXWindowsClipboard::kText, "synergy rocks!"); + + clipboard.empty(); + + bool actual = clipboard.has(CXWindowsClipboard::kText); + EXPECT_FALSE(actual); +} + +TEST_F(CXWindowsClipboardTests, add_newValue_valueWasStored) +{ + CXWindowsClipboard clipboard = createClipboard(); + + clipboard.add(IClipboard::kText, "synergy rocks!"); + + CString actual = clipboard.get(IClipboard::kText); + EXPECT_EQ("synergy rocks!", actual); +} + +TEST_F(CXWindowsClipboardTests, add_replaceValue_valueWasReplaced) +{ + CXWindowsClipboard clipboard = createClipboard(); + + clipboard.add(IClipboard::kText, "synergy rocks!"); + clipboard.add(IClipboard::kText, "maxivista sucks"); // haha, just kidding. + + CString actual = clipboard.get(IClipboard::kText); + EXPECT_EQ("maxivista sucks", actual); +} + +TEST_F(CXWindowsClipboardTests, close_isOpen_noErrors) +{ + CXWindowsClipboard clipboard = createClipboard(); + + // clipboard opened in createClipboard() + clipboard.close(); + + // can't assert anything +} + +TEST_F(CXWindowsClipboardTests, has_withFormatAdded_returnsTrue) +{ + CXWindowsClipboard clipboard = createClipboard(); + clipboard.add(IClipboard::kText, "synergy rocks!"); + + bool actual = clipboard.has(IClipboard::kText); + + EXPECT_EQ(true, actual); +} + +TEST_F(CXWindowsClipboardTests, has_withNoFormats_returnsFalse) +{ + CXWindowsClipboard clipboard = createClipboard(); + + bool actual = clipboard.has(IClipboard::kText); + + EXPECT_FALSE(actual); +} + +TEST_F(CXWindowsClipboardTests, get_withNoFormats_returnsEmpty) +{ + CXWindowsClipboard clipboard = createClipboard(); + + CString actual = clipboard.get(IClipboard::kText); + + EXPECT_EQ("", actual); +} + +TEST_F(CXWindowsClipboardTests, get_withFormatAdded_returnsExpected) +{ + CXWindowsClipboard clipboard = createClipboard(); + clipboard.add(IClipboard::kText, "synergy rocks!"); + + CString actual = clipboard.get(IClipboard::kText); + + EXPECT_EQ("synergy rocks!", actual); +} diff --git a/src/test/integtests/platform/CXWindowsKeyStateTests.cpp b/src/test/integtests/platform/CXWindowsKeyStateTests.cpp new file mode 100644 index 00000000..ea665889 --- /dev/null +++ b/src/test/integtests/platform/CXWindowsKeyStateTests.cpp @@ -0,0 +1,234 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include + +#define TEST_ENV +#include "Global.h" + +#include "CMockKeyMap.h" +#include "CMockEventQueue.h" +#include "CXWindowsKeyState.h" +#include "CLog.h" +#include + +#define XK_LATIN1 +#define XK_MISCELLANY +#include "X11/keysymdef.h" + +#if HAVE_XKB_EXTENSION +# include +#endif + +class CXWindowsKeyStateTests : public ::testing::Test +{ +protected: + CXWindowsKeyStateTests() : + m_display(NULL) + { + } + + ~CXWindowsKeyStateTests() + { + if (m_display != NULL) { + LOG((CLOG_DEBUG "closing display")); + XCloseDisplay(m_display); + } + } + + virtual void + SetUp() + { + // open the display only once for the entire test suite + if (this->m_display == NULL) { + LOG((CLOG_DEBUG "opening display")); + this->m_display = XOpenDisplay(NULL); + + ASSERT_TRUE(this->m_display != NULL) + << "unable to open display: " << errno; + } + } + + virtual void + TearDown() + { + } + + Display* m_display; +}; + +TEST_F(CXWindowsKeyStateTests, setActiveGroup_pollAndSet_groupIsZero) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CXWindowsKeyState keyState(m_display, true, eventQueue, keyMap); + + keyState.setActiveGroup(CXWindowsKeyState::kGroupPollAndSet); + + ASSERT_EQ(0, keyState.m_group); +} + +TEST_F(CXWindowsKeyStateTests, setActiveGroup_poll_groupIsNotSet) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CXWindowsKeyState keyState(m_display, true, eventQueue, keyMap); + + keyState.setActiveGroup(CXWindowsKeyState::kGroupPoll); + + ASSERT_LE(-1, keyState.m_group); +} + +TEST_F(CXWindowsKeyStateTests, setActiveGroup_customGroup_groupWasSet) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CXWindowsKeyState keyState(m_display, true, eventQueue, keyMap); + + keyState.setActiveGroup(1); + + ASSERT_EQ(1, keyState.m_group); +} + +TEST_F(CXWindowsKeyStateTests, mapModifiersFromX_zeroState_zeroMask) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CXWindowsKeyState keyState(m_display, true, eventQueue, keyMap); + + int mask = keyState.mapModifiersFromX(0); + + ASSERT_EQ(0, mask); +} + +TEST_F(CXWindowsKeyStateTests, mapModifiersToX_zeroMask_resultIsTrue) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CXWindowsKeyState keyState(m_display, true, eventQueue, keyMap); + + unsigned int modifiers = 0; + bool result = keyState.mapModifiersToX(0, modifiers); + + ASSERT_TRUE(result); +} + +TEST_F(CXWindowsKeyStateTests, fakeCtrlAltDel_default_returnsFalse) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CXWindowsKeyState keyState(m_display, true, eventQueue, keyMap); + + bool result = keyState.fakeCtrlAltDel(); + + ASSERT_FALSE(result); +} + +TEST_F(CXWindowsKeyStateTests, pollActiveModifiers_defaultState_returnsZero) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CXWindowsKeyState keyState(m_display, true, eventQueue, keyMap); + + KeyModifierMask actual = keyState.pollActiveModifiers(); + + ASSERT_EQ(0, actual); +} + +TEST_F(CXWindowsKeyStateTests, pollActiveModifiers_shiftKeyDownThenUp_masksAreCorrect) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CXWindowsKeyState keyState(m_display, true, eventQueue, keyMap); + + // set mock modifier mapping + std::fill( + keyState.m_modifierFromX.begin(), keyState.m_modifierFromX.end(), 0); + keyState.m_modifierFromX[ShiftMapIndex] = KeyModifierShift; + + KeyCode key = XKeysymToKeycode(m_display, XK_Shift_L); + + // fake shift key down (without using synergy) + XTestFakeKeyEvent(m_display, key, true, CurrentTime); + + // function under test (1st call) + KeyModifierMask modDown = keyState.pollActiveModifiers(); + + // fake shift key up (without using synergy) + XTestFakeKeyEvent(m_display, key, false, CurrentTime); + + // function under test (2nd call) + KeyModifierMask modUp = keyState.pollActiveModifiers(); + + EXPECT_TRUE((modDown & KeyModifierShift) == KeyModifierShift) + << "shift key not in mask - key was not pressed"; + + EXPECT_TRUE((modUp & KeyModifierShift) == 0) + << "shift key still in mask - make sure no keys are being held down"; +} + +TEST_F(CXWindowsKeyStateTests, pollActiveGroup_defaultState_returnsZero) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CXWindowsKeyState keyState(m_display, true, eventQueue, keyMap); + + SInt32 actual = keyState.pollActiveGroup(); + + ASSERT_EQ(0, actual); +} + +TEST_F(CXWindowsKeyStateTests, pollActiveGroup_positiveGroup_returnsGroup) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CXWindowsKeyState keyState(m_display, true, eventQueue, keyMap); + + keyState.m_group = 3; + + SInt32 actual = keyState.pollActiveGroup(); + + ASSERT_EQ(3, actual); +} + +TEST_F(CXWindowsKeyStateTests, pollActiveGroup_xkb_areEqual) +{ +#if HAVE_XKB_EXTENSION + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CXWindowsKeyState keyState(m_display, true, eventQueue, keyMap); + + // reset the group + keyState.m_group = -1; + + XkbStateRec state; + + // compare pollActiveGroup() with XkbGetState() + if (XkbGetState(m_display, XkbUseCoreKbd, &state) == Success) { + SInt32 actual = keyState.pollActiveGroup(); + + ASSERT_EQ(state.group, actual); + } + else { + FAIL() << "XkbGetState() returned error " << errno; + } +#else + SUCCEED() << "Xkb extension not installed"; +#endif +} + diff --git a/src/test/integtests/platform/CXWindowsScreenSaverTests.cpp b/src/test/integtests/platform/CXWindowsScreenSaverTests.cpp new file mode 100644 index 00000000..f896840c --- /dev/null +++ b/src/test/integtests/platform/CXWindowsScreenSaverTests.cpp @@ -0,0 +1,43 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include "CXWindowsScreenSaver.h" +#include "CMockEventQueue.h" +#include + +using ::testing::_; + +// TODO: not working on build machine for some reason +#if 0 +TEST(CXWindowsScreenSaverTests, activate_defaultScreen_todo) +{ + Display* display = XOpenDisplay(":0.0"); + Window window = DefaultRootWindow(display); + CMockEventQueue eventQueue; + EXPECT_CALL(eventQueue, removeHandler(_, _)).Times(1); + CXWindowsScreenSaver screenSaver(display, window, NULL, eventQueue); + + screenSaver.activate(); + + bool isActive = screenSaver.isActive(); + + screenSaver.deactivate(); + + ASSERT_EQ(true, isActive); +} +#endif \ No newline at end of file diff --git a/src/test/integtests/platform/CXWindowsScreenTests.cpp b/src/test/integtests/platform/CXWindowsScreenTests.cpp new file mode 100644 index 00000000..ea36d620 --- /dev/null +++ b/src/test/integtests/platform/CXWindowsScreenTests.cpp @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include "CXWindowsScreen.h" +#include "CMockEventQueue.h" + +using ::testing::_; + +TEST(CXWindowsScreenTests, fakeMouseMove_nonPrimary_getCursorPosValuesCorrect) +{ + CMockEventQueue eventQueue; + EXPECT_CALL(eventQueue, adoptHandler(_, _, _)).Times(2); + EXPECT_CALL(eventQueue, adoptBuffer(_)).Times(2); + EXPECT_CALL(eventQueue, removeHandler(_, _)).Times(2); + CXWindowsScreen screen(":0.0", false, false, 0, eventQueue); + + screen.fakeMouseMove(10, 20); + + int x, y; + screen.getCursorPos(x, y); + ASSERT_EQ(10, x); + ASSERT_EQ(20, y); +} diff --git a/src/test/unittests/CMakeLists.txt b/src/test/unittests/CMakeLists.txt new file mode 100644 index 00000000..f319f4ba --- /dev/null +++ b/src/test/unittests/CMakeLists.txt @@ -0,0 +1,63 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2009 Chris Schoeneman, Nick Bolton, Sorin Sbarnea +# +# 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 COPYING 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 . + +set(h + synergy/CKeyStateTests.h + synergy/CMockEventQueue.h + synergy/CMockKeyMap.h + client/CMockClient.h + io/CMockStream.h +) + +set(src + ${h} + Main.cpp + synergy/CClipboardTests.cpp + synergy/CKeyStateTests.cpp + client/CServerProxyTests.cpp +) + +set(inc + ../../lib/arch + ../../lib/base + ../../lib/client + ../../lib/common + ../../lib/io + ../../lib/mt + ../../lib/net + ../../lib/platform + ../../lib/synergy + ../../../tools/gtest-1.6.0/include + ../../../tools/gmock-1.6.0/include + io + synergy +) + +if (UNIX) + list(APPEND inc + ../../.. + ) +endif() + +if (WIN32) + if (GAME_DEVICE_SUPPORT) + link_directories("$ENV{DXSDK_DIR}/Lib/x86") + endif() +endif() + +include_directories(${inc}) +add_executable(unittests ${src}) +target_link_libraries(unittests + arch base client common io net platform server synergy mt gtest gmock ${libs}) diff --git a/src/test/unittests/Main.cpp b/src/test/unittests/Main.cpp new file mode 100644 index 00000000..eafc8155 --- /dev/null +++ b/src/test/unittests/Main.cpp @@ -0,0 +1,39 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include "CArch.h" +#include "CLog.h" + +#if SYSAPI_WIN32 +#include "CArchMiscWindows.h" +#endif + +int +main(int argc, char **argv) +{ +#if SYSAPI_WIN32 + // HACK: shouldn't be needed, but logging fails without this. + CArchMiscWindows::setInstanceWin32(GetModuleHandle(NULL)); +#endif + + CLOG->setFilter(kDEBUG2); + + testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +} diff --git a/src/test/unittests/client/CMockClient.h b/src/test/unittests/client/CMockClient.h new file mode 100644 index 00000000..abc37c08 --- /dev/null +++ b/src/test/unittests/client/CMockClient.h @@ -0,0 +1,30 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include "CClient.h" + +class IEventQueue; + +class CMockClient : public CClient +{ +public: + CMockClient(IEventQueue& eventQueue) : CClient(eventQueue) { m_mock = true; } + MOCK_METHOD2(mouseMove, void(SInt32, SInt32)); +}; diff --git a/src/test/unittests/client/CServerProxyTests.cpp b/src/test/unittests/client/CServerProxyTests.cpp new file mode 100644 index 00000000..38858d73 --- /dev/null +++ b/src/test/unittests/client/CServerProxyTests.cpp @@ -0,0 +1,90 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 + +#define TEST_ENV +#include "Global.h" + +#include "CServerProxy.h" +#include "CMockClient.h" +#include "CMockStream.h" +#include "CMockEventQueue.h" +#include "ProtocolTypes.h" + +using ::testing::_; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::AnyNumber; + +int streamReads = 0; + +UInt32 +streamRead(void* buffer, UInt32 n); + +// TODO: fix linking in windows (works in unix for some reason). +#if 0 +TEST(CServerProxyTests, parseMessage_mouseMove_valuesCorrect) +{ + NiceMock eventQueue; + CMockClient client(eventQueue); + CMockStream stream(eventQueue); + + ON_CALL(stream, read(_, _)).WillByDefault(Invoke(streamRead)); + EXPECT_CALL(stream, read(_, _)).Times(4); + EXPECT_CALL(stream, write(_, _)).Times(1); + EXPECT_CALL(stream, isReady()).Times(1); + EXPECT_CALL(stream, getEventTarget()).Times(AnyNumber()); + + CServerProxy serverProxy(&client, &stream, eventQueue); + + // skip handshake, go straight to normal parser. + serverProxy.m_parser = &CServerProxy::parseMessage; + + // assert + EXPECT_CALL(client, mouseMove(10, 20)); + + serverProxy.handleData(NULL, NULL); +} +#endif + +UInt32 +streamRead(void* buffer, UInt32 n) +{ + streamReads++; + UInt8* code = (UInt8*)buffer; + + if (streamReads == 1) { + code[0] = 'D'; + code[1] = 'M'; + code[2] = 'M'; + code[3] = 'V'; + return 4; + } + else if (streamReads == 2) { + code[0] = 0; + code[1] = 10; + return 2; + } + else if (streamReads == 3) { + code[0] = 0; + code[1] = 20; + return 2; + } + + return 0; +} diff --git a/src/test/unittests/io/CMockStream.h b/src/test/unittests/io/CMockStream.h new file mode 100644 index 00000000..7d75ee34 --- /dev/null +++ b/src/test/unittests/io/CMockStream.h @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include "IStream.h" + +class IEventQueue; + +class CMockStream : public IStream +{ +public: + CMockStream(IEventQueue& eventQueue) : IStream(eventQueue) { } + MOCK_METHOD0(close, void()); + MOCK_METHOD2(read, UInt32(void*, UInt32)); + MOCK_METHOD2(write, void(const void*, UInt32)); + MOCK_METHOD0(flush, void()); + MOCK_METHOD0(shutdownInput, void()); + MOCK_METHOD0(shutdownOutput, void()); + MOCK_CONST_METHOD0(getEventTarget, void*()); + MOCK_CONST_METHOD0(isReady, bool()); + MOCK_CONST_METHOD0(getSize, UInt32()); +}; diff --git a/src/test/unittests/synergy/CClipboardTests.cpp b/src/test/unittests/synergy/CClipboardTests.cpp new file mode 100644 index 00000000..cf14e382 --- /dev/null +++ b/src/test/unittests/synergy/CClipboardTests.cpp @@ -0,0 +1,402 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include "CClipboard.h" + +TEST(CClipboardTests, empty_openCalled_returnsTrue) +{ + CClipboard clipboard; + clipboard.open(0); + + bool actual = clipboard.empty(); + + EXPECT_EQ(true, actual); +} + +TEST(CClipboardTests, empty_singleFormat_hasReturnsFalse) +{ + CClipboard clipboard; + clipboard.open(0); + clipboard.add(CClipboard::kText, "synergy rocks!"); + + clipboard.empty(); + + bool actual = clipboard.has(CClipboard::kText); + EXPECT_FALSE(actual); +} + +TEST(CClipboardTests, add_newValue_valueWasStored) +{ + CClipboard clipboard; + clipboard.open(0); + + clipboard.add(IClipboard::kText, "synergy rocks!"); + + CString actual = clipboard.get(IClipboard::kText); + EXPECT_EQ("synergy rocks!", actual); +} + +TEST(CClipboardTests, add_replaceValue_valueWasReplaced) +{ + CClipboard clipboard; + clipboard.open(0); + + clipboard.add(IClipboard::kText, "synergy rocks!"); + clipboard.add(IClipboard::kText, "maxivista sucks"); // haha, just kidding. + + CString actual = clipboard.get(IClipboard::kText); + EXPECT_EQ("maxivista sucks", actual); +} + +TEST(CClipboardTests, open_timeIsZero_returnsTrue) +{ + CClipboard clipboard; + + bool actual = clipboard.open(0); + + EXPECT_EQ(true, actual); +} + +TEST(CClipboardTests, open_timeIsOne_returnsTrue) +{ + CClipboard clipboard; + + bool actual = clipboard.open(1); + + EXPECT_EQ(true, actual); +} + +TEST(CClipboardTests, close_isOpen_noErrors) +{ + CClipboard clipboard; + clipboard.open(0); + + clipboard.close(); + + // can't assert anything +} + +TEST(CClipboardTests, getTime_openWithNoEmpty_returnsZero) +{ + CClipboard clipboard; + clipboard.open(1); + + CClipboard::Time actual = clipboard.getTime(); + + EXPECT_EQ(0, actual); +} + +TEST(CClipboardTests, getTime_openAndEmpty_returnsOne) +{ + CClipboard clipboard; + clipboard.open(1); + clipboard.empty(); + + CClipboard::Time actual = clipboard.getTime(); + + EXPECT_EQ(1, actual); +} + +TEST(CClipboardTests, has_withFormatAdded_returnsTrue) +{ + CClipboard clipboard; + clipboard.open(0); + clipboard.add(IClipboard::kText, "synergy rocks!"); + + bool actual = clipboard.has(IClipboard::kText); + + EXPECT_EQ(true, actual); +} + +TEST(CClipboardTests, has_withNoFormats_returnsFalse) +{ + CClipboard clipboard; + clipboard.open(0); + + bool actual = clipboard.has(IClipboard::kText); + + EXPECT_FALSE(actual); +} + +TEST(CClipboardTests, get_withNoFormats_returnsEmpty) +{ + CClipboard clipboard; + clipboard.open(0); + + CString actual = clipboard.get(IClipboard::kText); + + EXPECT_EQ("", actual); +} + +TEST(CClipboardTests, get_withFormatAdded_returnsExpected) +{ + CClipboard clipboard; + clipboard.open(0); + clipboard.add(IClipboard::kText, "synergy rocks!"); + + CString actual = clipboard.get(IClipboard::kText); + + EXPECT_EQ("synergy rocks!", actual); +} + +TEST(CClipboardTests, marshall_addNotCalled_firstCharIsZero) +{ + CClipboard clipboard; + + CString actual = clipboard.marshall(); + + // seems to return "\0\0\0\0" but EXPECT_EQ can't assert this, + // so instead, just assert that first char is '\0'. + EXPECT_EQ(0, (int)actual[0]); +} + +TEST(CClipboardTests, marshall_withTextAdded_typeCharIsText) +{ + CClipboard clipboard; + clipboard.open(0); + clipboard.add(IClipboard::kText, "synergy rocks!"); + clipboard.close(); + + CString actual = clipboard.marshall(); + + // string contains other data, but 8th char should be kText. + EXPECT_EQ(IClipboard::kText, (int)actual[7]); +} + +TEST(CClipboardTests, marshall_withTextAdded_lastSizeCharIs14) +{ + CClipboard clipboard; + clipboard.open(0); + clipboard.add(IClipboard::kText, "synergy rocks!"); // 14 chars + clipboard.close(); + + CString actual = clipboard.marshall(); + + EXPECT_EQ(14, (int)actual[11]); +} + +// TODO: there's some integer -> char encoding going on here. i find it +// hard to believe that the clipboard is the only thing doing this. maybe +// we should refactor this stuff out of the clipboard. +TEST(CClipboardTests, marshall_withTextSize285_sizeCharsValid) +{ + // 285 chars + CString data; + data.append("Synergy is Free and Open Source Software that lets you "); + data.append("easily share your mouse and keyboard between multiple "); + data.append("computers, where each computer has it's own display. No "); + data.append("special hardware is required, all you need is a local area "); + data.append("network. Synergy is supported on Windows, Mac OS X and Linux."); + + CClipboard clipboard; + clipboard.open(0); + clipboard.add(IClipboard::kText, data); + clipboard.close(); + + CString actual = clipboard.marshall(); + + // 4 asserts here, but that's ok because we're really just asserting 1 + // thing. the 32-bit size value is split into 4 chars. if the size is 285 + // (29 more than the 8-bit max size), the last char "rolls over" to 29 + // (this is caused by a bit-wise & on 0xff and 8-bit truncation). each + // char before the last stores a bit-shifted version of the number, each + // 1 more power than the last, which is done by bit-shifting [0] by 24, + // [1] by 16, [2] by 8 ([3] is not bit-shifted). + EXPECT_EQ(0, actual[8]); // 285 >> 24 = 285 / (256^3) = 0 + EXPECT_EQ(0, actual[9]); // 285 >> 16 = 285 / (256^2) = 0 + EXPECT_EQ(1, actual[10]); // 285 >> 8 = 285 / (256^1) = 1(.11328125) + EXPECT_EQ(29, actual[11]); // 285 - 256 = 29 +} + +TEST(CClipboardTests, marshall_withHtmlAdded_typeCharIsHtml) +{ + CClipboard clipboard; + clipboard.open(0); + clipboard.add(IClipboard::kHTML, "html sucks"); + clipboard.close(); + + CString actual = clipboard.marshall(); + + // string contains other data, but 8th char should be kHTML. + EXPECT_EQ(IClipboard::kHTML, (int)actual[7]); +} + +TEST(CClipboardTests, marshall_withHtmlAndText_has2Formats) +{ + CClipboard clipboard; + clipboard.open(0); + clipboard.add(IClipboard::kText, "synergy rocks"); + clipboard.add(IClipboard::kHTML, "html sucks"); + clipboard.close(); + + CString actual = clipboard.marshall(); + + // the number of formats is stored inside the first 4 chars. + // the writeUInt32 function right-aligns numbers in 4 chars, + // so if you right align 2, it will be "\0\0\0\2" in a string. + // we assert that the char at the 4th index is 2 (the number of + // formats that we've added). + EXPECT_EQ(2, (int)actual[3]); +} + +TEST(CClipboardTests, marshall_withTextAdded_endsWithAdded) +{ + CClipboard clipboard; + clipboard.open(0); + clipboard.add(IClipboard::kText, "synergy rocks!"); + clipboard.close(); + + CString actual = clipboard.marshall(); + + // string contains other data, but should end in the string we added. + EXPECT_EQ("synergy rocks!", actual.substr(12)); +} + +TEST(CClipboardTests, unmarshall_emptyData_hasTextIsFalse) +{ + CClipboard clipboard; + + CString data; + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)0; // 0 formats added + + clipboard.unmarshall(data, 0); + + clipboard.open(0); + bool actual = clipboard.has(IClipboard::kText); + EXPECT_FALSE(actual); +} + +TEST(CClipboardTests, unmarshall_withTextSize285_getTextIsValid) +{ + CClipboard clipboard; + + // 285 chars + CString text; + text.append("Synergy is Free and Open Source Software that lets you "); + text.append("easily share your mouse and keyboard between multiple "); + text.append("computers, where each computer has it's own display. No "); + text.append("special hardware is required, all you need is a local area "); + text.append("network. Synergy is supported on Windows, Mac OS X and Linux."); + + CString data; + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)1; // 1 format added + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)IClipboard::kText; + data += (char)0; // 285 >> 24 = 285 / (256^3) = 0 + data += (char)0; // 285 >> 16 = 285 / (256^2) = 0 + data += (char)1; // 285 >> 8 = 285 / (256^1) = 1(.11328125) + data += (char)29; // 285 - 256 = 29 + data += text; + + clipboard.unmarshall(data, 0); + + clipboard.open(0); + CString actual = clipboard.get(IClipboard::kText); + EXPECT_EQ(text, actual); +} + +TEST(CClipboardTests, unmarshall_withTextAndHtml_getTextIsValid) +{ + CClipboard clipboard; + CString data; + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)2; // 2 formats added + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)IClipboard::kText; + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)14; + data += "synergy rocks!"; + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)IClipboard::kHTML; + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)10; + data += "html sucks"; + + clipboard.unmarshall(data, 0); + + clipboard.open(0); + CString actual = clipboard.get(IClipboard::kText); + EXPECT_EQ("synergy rocks!", actual); +} + +TEST(CClipboardTests, unmarshall_withTextAndHtml_getHtmlIsValid) +{ + CClipboard clipboard; + CString data; + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)2; // 2 formats added + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)IClipboard::kText; + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)14; + data += "synergy rocks!"; + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)IClipboard::kHTML; + data += (char)0; + data += (char)0; + data += (char)0; + data += (char)10; + data += "html sucks"; + + clipboard.unmarshall(data, 0); + + clipboard.open(0); + CString actual = clipboard.get(IClipboard::kHTML); + EXPECT_EQ("html sucks", actual); +} + +TEST(CClipboardTests, copy_withSingleText_clipboardsAreEqual) +{ + CClipboard clipboard1; + clipboard1.open(0); + clipboard1.add(CClipboard::kText, "synergy rocks!"); + clipboard1.close(); + + CClipboard clipboard2; + CClipboard::copy(&clipboard2, &clipboard1); + + clipboard2.open(0); + CString actual = clipboard2.get(CClipboard::kText); + EXPECT_EQ("synergy rocks!", actual); +} diff --git a/src/test/unittests/synergy/CKeyStateTests.cpp b/src/test/unittests/synergy/CKeyStateTests.cpp new file mode 100644 index 00000000..51e1caf5 --- /dev/null +++ b/src/test/unittests/synergy/CKeyStateTests.cpp @@ -0,0 +1,451 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 +#include +#include "CKeyStateTests.h" +#include "CMockEventQueue.h" +#include "CMockKeyMap.h" + +using ::testing::_; +using ::testing::NiceMock; +using ::testing::Invoke; +using ::testing::Return; +using ::testing::SaveArg; + +TEST(CKeyStateTests, onKey_aKeyDown_keyStateOne) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + keyState.onKey(1, true, KeyModifierAlt); + + EXPECT_EQ(1, keyState.getKeyState(1)); +} + +TEST(CKeyStateTests, onKey_aKeyUp_keyStateZero) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + keyState.onKey(1, false, KeyModifierAlt); + + EXPECT_EQ(0, keyState.getKeyState(1)); +} + +TEST(CKeyStateTests, onKey_invalidKey_keyStateZero) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + keyState.onKey(0, true, KeyModifierAlt); + + EXPECT_EQ(0, keyState.getKeyState(0)); +} + +TEST(CKeyStateTests, sendKeyEvent_halfDuplexAndRepeat_addEventNotCalled) +{ + NiceMock keyMap; + NiceMock eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + ON_CALL(keyMap, isHalfDuplex(_, _)).WillByDefault(Return(true)); + + EXPECT_CALL(eventQueue, addEvent(_)).Times(0); + + keyState.sendKeyEvent(NULL, false, true, kKeyCapsLock, 0, 0, 0); +} + +TEST(CKeyStateTests, sendKeyEvent_halfDuplex_addEventCalledTwice) +{ + NiceMock keyMap; + NiceMock eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + ON_CALL(keyMap, isHalfDuplex(_, _)).WillByDefault(Return(true)); + + EXPECT_CALL(eventQueue, addEvent(_)).Times(2); + + keyState.sendKeyEvent(NULL, false, false, kKeyCapsLock, 0, 0, 0); +} + +TEST(CKeyStateTests, sendKeyEvent_keyRepeat_addEventCalledOnce) +{ + NiceMock keyMap; + NiceMock eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + EXPECT_CALL(eventQueue, addEvent(_)).Times(1); + + keyState.sendKeyEvent(NULL, false, true, 1, 0, 0, 0); +} + +TEST(CKeyStateTests, sendKeyEvent_keyDown_addEventCalledOnce) +{ + NiceMock keyMap; + NiceMock eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + EXPECT_CALL(eventQueue, addEvent(_)).Times(1); + + keyState.sendKeyEvent(NULL, true, false, 1, 0, 0, 0); +} + +TEST(CKeyStateTests, sendKeyEvent_keyUp_addEventCalledOnce) +{ + NiceMock keyMap; + NiceMock eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + EXPECT_CALL(eventQueue, addEvent(_)).Times(1); + + keyState.sendKeyEvent(NULL, false, false, 1, 0, 0, 0); +} + +TEST(CKeyStateTests, updateKeyMap_mockKeyMap_keyMapGotMock) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + // key map member gets a new key map via swap() + EXPECT_CALL(keyMap, swap(_)); + + keyState.updateKeyMap(); +} + +TEST(CKeyStateTests, updateKeyState_pollInsertsSingleKey_keyIsDown) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + ON_CALL(keyState, pollPressedKeys(_)).WillByDefault(Invoke(stubPollPressedKeys)); + + keyState.updateKeyState(); + + bool actual = keyState.isKeyDown(1); + ASSERT_TRUE(actual); +} + +TEST(CKeyStateTests, updateKeyState_pollDoesNothing_keyNotSet) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + keyState.updateKeyState(); + + bool actual = keyState.isKeyDown(1); + ASSERT_FALSE(actual); +} + +TEST(CKeyStateTests, updateKeyState_activeModifiers_maskSet) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + ON_CALL(keyState, pollActiveModifiers()).WillByDefault(Return(KeyModifierAlt)); + + keyState.updateKeyState(); + + KeyModifierMask actual = keyState.getActiveModifiers(); + ASSERT_EQ(KeyModifierAlt, actual); +} + +TEST(CKeyStateTests, updateKeyState_activeModifiers_maskNotSet) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + keyState.updateKeyState(); + + KeyModifierMask actual = keyState.getActiveModifiers(); + ASSERT_EQ(0, actual); +} + +TEST(CKeyStateTests, updateKeyState_activeModifiers_keyMapGotModifers) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + ON_CALL(keyState, pollActiveModifiers()).WillByDefault(Return(1)); + ON_CALL(keyMap, foreachKey(_, _)).WillByDefault(Invoke(assertMaskIsOne)); + + // key map gets new modifiers via foreachKey() + EXPECT_CALL(keyMap, foreachKey(_, _)); + + keyState.updateKeyState(); +} + +TEST(CKeyStateTests, setHalfDuplexMask_capsLock_halfDuplexCapsLockAdded) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + EXPECT_CALL(keyMap, addHalfDuplexModifier(kKeyCapsLock)); + + keyState.setHalfDuplexMask(KeyModifierCapsLock); +} + +TEST(CKeyStateTests, setHalfDuplexMask_numLock_halfDuplexNumLockAdded) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + EXPECT_CALL(keyMap, addHalfDuplexModifier(kKeyNumLock)); + + keyState.setHalfDuplexMask(KeyModifierNumLock); +} + +TEST(CKeyStateTests, setHalfDuplexMask_scrollLock_halfDuplexScollLockAdded) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + EXPECT_CALL(keyMap, addHalfDuplexModifier(kKeyScrollLock)); + + keyState.setHalfDuplexMask(KeyModifierScrollLock); +} + +TEST(CKeyStateTests, fakeKeyDown_serverKeyAlreadyDown_fakeKeyCalledTwice) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + s_stubKeyItem.m_client = 0; + s_stubKeyItem.m_button = 1; + ON_CALL(keyMap, mapKey(_, _, _, _, _, _, _)).WillByDefault(Invoke(stubMapKey)); + + // 2 calls to fakeKeyDown should still call fakeKey, even though + // repeated keys are handled differently. + EXPECT_CALL(keyState, fakeKey(_)).Times(2); + + // call twice to simulate server key already down (a misreported autorepeat). + keyState.fakeKeyDown(1, 0, 0); + keyState.fakeKeyDown(1, 0, 0); +} + +TEST(CKeyStateTests, fakeKeyDown_isIgnoredKey_fakeKeyNotCalled) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + EXPECT_CALL(keyState, fakeKey(_)).Times(0); + + keyState.fakeKeyDown(kKeyCapsLock, 0, 0); +} + +TEST(CKeyStateTests, fakeKeyDown_mapReturnsKeystrokes_fakeKeyCalled) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + s_stubKeyItem.m_button = 0; + s_stubKeyItem.m_client = 0; + ON_CALL(keyMap, mapKey(_, _, _, _, _, _, _)).WillByDefault(Invoke(stubMapKey)); + + EXPECT_CALL(keyState, fakeKey(_)).Times(1); + + keyState.fakeKeyDown(1, 0, 0); +} + +TEST(CKeyStateTests, fakeKeyRepeat_invalidKey_returnsFalse) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + bool actual = keyState.fakeKeyRepeat(0, 0, 0, 0); + + ASSERT_FALSE(actual); +} + +TEST(CKeyStateTests, fakeKeyRepeat_nullKey_returnsFalse) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + // set the key to down (we need to make mapKey return a valid key to do this). + CKeyMap::KeyItem keyItem; + keyItem.m_client = 0; + keyItem.m_button = 1; + ON_CALL(keyMap, mapKey(_, _, _, _, _, _, _)).WillByDefault(Return(&keyItem)); + keyState.fakeKeyDown(1, 0, 0); + + // change mapKey to return NULL so that fakeKeyRepeat exits early. + CKeyMap::KeyItem* nullKeyItem = NULL; + ON_CALL(keyMap, mapKey(_, _, _, _, _, _, _)).WillByDefault(Return(nullKeyItem)); + + bool actual = keyState.fakeKeyRepeat(1, 0, 0, 0); + + ASSERT_FALSE(actual); +} + +TEST(CKeyStateTests, fakeKeyRepeat_invalidButton_returnsFalse) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + // set the key to down (we need to make mapKey return a valid key to do this). + CKeyMap::KeyItem keyItem; + keyItem.m_client = 0; + keyItem.m_button = 1; // set to 1 to make fakeKeyDown work. + ON_CALL(keyMap, mapKey(_, _, _, _, _, _, _)).WillByDefault(Return(&keyItem)); + keyState.fakeKeyDown(1, 0, 0); + + // change button to 0 so that fakeKeyRepeat will return early. + keyItem.m_button = 0; + ON_CALL(keyMap, mapKey(_, _, _, _, _, _, _)).WillByDefault(Return(&keyItem)); + + bool actual = keyState.fakeKeyRepeat(1, 0, 0, 0); + + ASSERT_FALSE(actual); +} + +TEST(CKeyStateTests, fakeKeyRepeat_validKey_returnsTrue) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + s_stubKeyItem.m_client = 0; + s_stubKeystroke.m_type = CKeyMap::Keystroke::kButton; + s_stubKeystroke.m_data.m_button.m_button = 2; + + // set the button to 1 for fakeKeyDown call + s_stubKeyItem.m_button = 1; + ON_CALL(keyMap, mapKey(_, _, _, _, _, _, _)).WillByDefault(Invoke(stubMapKey)); + keyState.fakeKeyDown(1, 0, 0); + + // change the button to 2 + s_stubKeyItem.m_button = 2; + ON_CALL(keyMap, mapKey(_, _, _, _, _, _, _)).WillByDefault(Invoke(stubMapKey)); + + bool actual = keyState.fakeKeyRepeat(1, 0, 0, 0); + + ASSERT_TRUE(actual); +} + +TEST(CKeyStateTests, fakeKeyUp_buttonNotDown_returnsFalse) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + bool actual = keyState.fakeKeyUp(0); + + ASSERT_FALSE(actual); +} + +TEST(CKeyStateTests, fakeKeyUp_buttonAlreadyDown_returnsTrue) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + // press alt down so we get full coverage. + ON_CALL(keyState, pollActiveModifiers()).WillByDefault(Return(KeyModifierAlt)); + keyState.updateKeyState(); + + // press button 1 down. + s_stubKeyItem.m_button = 1; + ON_CALL(keyMap, mapKey(_, _, _, _, _, _, _)).WillByDefault(Invoke(stubMapKey)); + keyState.fakeKeyDown(1, 0, 1); + + // this takes the button id, which is the 3rd arg of fakeKeyDown + bool actual = keyState.fakeKeyUp(1); + + ASSERT_TRUE(actual); +} + +TEST(CKeyStateTests, fakeAllKeysUp_keysWereDown_keysAreUp) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + // press button 1 down. + s_stubKeyItem.m_button = 1; + ON_CALL(keyMap, mapKey(_, _, _, _, _, _, _)).WillByDefault(Invoke(stubMapKey)); + keyState.fakeKeyDown(1, 0, 1); + + // method under test + keyState.fakeAllKeysUp(); + + bool actual = keyState.isKeyDown(1); + ASSERT_FALSE(actual); +} + +TEST(CKeyStateTests, isKeyDown_keyDown_returnsTrue) +{ + NiceMock keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + // press button 1 down. + s_stubKeyItem.m_button = 1; + ON_CALL(keyMap, mapKey(_, _, _, _, _, _, _)).WillByDefault(Invoke(stubMapKey)); + keyState.fakeKeyDown(1, 0, 1); + + // method under test + bool actual = keyState.isKeyDown(1); + + ASSERT_TRUE(actual); +} + +TEST(CKeyStateTests, isKeyDown_noKeysDown_returnsFalse) +{ + CMockKeyMap keyMap; + CMockEventQueue eventQueue; + CKeyStateImpl keyState(eventQueue, keyMap); + + // method under test + bool actual = keyState.isKeyDown(1); + + ASSERT_FALSE(actual); +} + +void +stubPollPressedKeys(IKeyState::KeyButtonSet& pressedKeys) +{ + pressedKeys.insert(1); +} + +void +assertMaskIsOne(ForeachKeyCallback cb, void* userData) +{ + ASSERT_EQ(1, ((CKeyState::CAddActiveModifierContext*)userData)->m_mask); +} + +const CKeyMap::KeyItem* +stubMapKey( + CKeyMap::Keystrokes& keys, KeyID id, SInt32 group, + CKeyMap::ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask desiredMask, + bool isAutoRepeat) +{ + keys.push_back(s_stubKeystroke); + return &s_stubKeyItem; +} diff --git a/src/test/unittests/synergy/CKeyStateTests.h b/src/test/unittests/synergy/CKeyStateTests.h new file mode 100644 index 00000000..fea2f6f8 --- /dev/null +++ b/src/test/unittests/synergy/CKeyStateTests.h @@ -0,0 +1,73 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CKEYSTATETESTS_H +#define CKEYSTATETESTS_H + +#include "CKeyState.h" +#include "gmock/gmock.h" + +class CMockKeyMap; +class CMockEventQueue; + +// NOTE: do not mock methods that are not pure virtual. this mock exists only +// to provide an implementation of the CKeyState abstract class. +class CMockKeyState : public CKeyState +{ +public: + CMockKeyState() : CKeyState() + { + } + + CMockKeyState(const CMockEventQueue& eventQueue, const CMockKeyMap& keyMap) : + CKeyState((IEventQueue&)eventQueue, (CKeyMap&)keyMap) + { + } + + MOCK_CONST_METHOD0(pollActiveGroup, SInt32()); + MOCK_CONST_METHOD0(pollActiveModifiers, KeyModifierMask()); + MOCK_METHOD0(fakeCtrlAltDel, bool()); + MOCK_METHOD1(getKeyMap, void(CKeyMap&)); + MOCK_METHOD1(fakeKey, void(const Keystroke&)); + MOCK_CONST_METHOD1(pollPressedKeys, void(KeyButtonSet&)); +}; + +typedef ::testing::NiceMock CKeyStateImpl; + +typedef UInt32 KeyID; + +typedef void (*ForeachKeyCallback)( + KeyID, SInt32 group, CKeyMap::KeyItem&, void* userData); + +void +stubPollPressedKeys(IKeyState::KeyButtonSet& pressedKeys); + +void +assertMaskIsOne(ForeachKeyCallback cb, void* userData); + +const CKeyMap::KeyItem* +stubMapKey( + CKeyMap::Keystrokes& keys, KeyID id, SInt32 group, + CKeyMap::ModifierToKeys& activeModifiers, + KeyModifierMask& currentState, + KeyModifierMask desiredMask, + bool isAutoRepeat); + +CKeyMap::Keystroke s_stubKeystroke(1, false, false); +CKeyMap::KeyItem s_stubKeyItem; + +#endif diff --git a/src/test/unittests/synergy/CMockEventQueue.h b/src/test/unittests/synergy/CMockEventQueue.h new file mode 100644 index 00000000..ae6145b6 --- /dev/null +++ b/src/test/unittests/synergy/CMockEventQueue.h @@ -0,0 +1,45 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMOCKEVENTQUEUE_H +#define CMOCKEVENTQUEUE_H + +#include +#include "IEventQueue.h" + +class CMockEventQueue : public IEventQueue +{ +public: + MOCK_METHOD2(newOneShotTimer, CEventQueueTimer*(double, void*)); + MOCK_METHOD2(newTimer, CEventQueueTimer*(double, void*)); + MOCK_METHOD2(getEvent, bool(CEvent&, double)); + MOCK_METHOD1(adoptBuffer, void(IEventQueueBuffer*)); + MOCK_METHOD2(registerTypeOnce, CEvent::Type(CEvent::Type&, const char*)); + MOCK_METHOD1(removeHandlers, void(void*)); + MOCK_METHOD1(registerType, CEvent::Type(const char*)); + MOCK_CONST_METHOD0(isEmpty, bool()); + MOCK_METHOD3(adoptHandler, void(CEvent::Type, void*, IEventJob*)); + MOCK_METHOD1(getTypeName, const char*(CEvent::Type)); + MOCK_METHOD1(addEvent, void(const CEvent&)); + MOCK_METHOD2(removeHandler, void(CEvent::Type, void*)); + MOCK_METHOD1(dispatchEvent, bool(const CEvent&)); + MOCK_CONST_METHOD2(getHandler, IEventJob*(CEvent::Type, void*)); + MOCK_METHOD1(deleteTimer, void(CEventQueueTimer*)); + MOCK_CONST_METHOD1(getRegisteredType, CEvent::Type(const CString&)); +}; + +#endif diff --git a/src/test/unittests/synergy/CMockKeyMap.h b/src/test/unittests/synergy/CMockKeyMap.h new file mode 100644 index 00000000..2fbf88d7 --- /dev/null +++ b/src/test/unittests/synergy/CMockKeyMap.h @@ -0,0 +1,37 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2011 Chris Schoeneman, Nick Bolton, Sorin Sbarnea + * + * 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 COPYING 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 . + */ + +#ifndef CMOCKKEYMAP_H +#define CMOCKKEYMAP_H + +#include +#include "CKeyMap.h" + +class CMockKeyMap : public CKeyMap +{ +public: + MOCK_METHOD1(swap, void(CKeyMap&)); + MOCK_METHOD0(finish, void()); + MOCK_METHOD2(foreachKey, void(ForeachKeyCallback, void*)); + MOCK_METHOD1(addHalfDuplexModifier, void(KeyID)); + MOCK_CONST_METHOD2(isHalfDuplex, bool(KeyID, KeyButton)); + MOCK_CONST_METHOD7(mapKey, const CKeyMap::KeyItem*( + Keystrokes&, KeyID, SInt32, ModifierToKeys&, KeyModifierMask&, + KeyModifierMask, bool)); +}; + +#endif diff --git a/src/vnc/CMakeLists.txt b/src/vnc/CMakeLists.txt new file mode 100644 index 00000000..56fce6da --- /dev/null +++ b/src/vnc/CMakeLists.txt @@ -0,0 +1,23 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2012 Nick Bolton +# +# 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 COPYING 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 . + + +if (WIN32) +add_subdirectory(common) + add_subdirectory(win) +elseif (UNIX) + #add_subdirectory(unix) +endif() + diff --git a/src/vnc/LICENCE.txt b/src/vnc/LICENCE.txt new file mode 100644 index 00000000..ae3b5319 --- /dev/null +++ b/src/vnc/LICENCE.txt @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + Appendix: How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) 19yy + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19yy name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/src/vnc/common/CMakeLists.txt b/src/vnc/common/CMakeLists.txt new file mode 100644 index 00000000..2754c024 --- /dev/null +++ b/src/vnc/common/CMakeLists.txt @@ -0,0 +1,20 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2012 Nick Bolton +# +# 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 COPYING 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 . + +add_subdirectory(rfb) +add_subdirectory(network) +add_subdirectory(rdr) +add_subdirectory(Xregion) +add_subdirectory(zlib) diff --git a/src/vnc/common/Makefile.in b/src/vnc/common/Makefile.in new file mode 100644 index 00000000..b55d23e3 --- /dev/null +++ b/src/vnc/common/Makefile.in @@ -0,0 +1,4 @@ + +SUBDIRS = @ZLIB_DIR@ rdr network Xregion rfb + +# followed by boilerplate.mk diff --git a/src/vnc/common/Xregion/CMakeLists.txt b/src/vnc/common/Xregion/CMakeLists.txt new file mode 100644 index 00000000..3276ce35 --- /dev/null +++ b/src/vnc/common/Xregion/CMakeLists.txt @@ -0,0 +1,36 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2012 Nick Bolton +# +# 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 COPYING 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 . + +set(inc + region.h + + Xregion.h +) + +set(src + Region.c +) + +if (WIN32) + list(APPEND src ${inc}) +endif() + +set(inc + .. + ../../win +) + +include_directories(${inc}) +add_library(vnc_Xregion STATIC ${src}) diff --git a/src/vnc/common/Xregion/Makefile.in b/src/vnc/common/Xregion/Makefile.in new file mode 100644 index 00000000..878a29b6 --- /dev/null +++ b/src/vnc/common/Xregion/Makefile.in @@ -0,0 +1,15 @@ + +SRCS = Region.c + +OBJS = $(SRCS:.c=.o) + +library = libXregion.a + +all:: $(library) + +$(library): $(OBJS) + rm -f $(library) + $(AR) $(library) $(OBJS) + $(RANLIB) $(library) + +# followed by boilerplate.mk diff --git a/src/vnc/common/Xregion/Region.c b/src/vnc/common/Xregion/Region.c new file mode 100644 index 00000000..bf2d123b --- /dev/null +++ b/src/vnc/common/Xregion/Region.c @@ -0,0 +1,1687 @@ +/* $Xorg: Region.c,v 1.6 2001/02/09 02:03:35 xorgcvs Exp $ */ +/************************************************************************ + +Copyright 1987, 1988, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. + + +Copyright 1987, 1988 by Digital Equipment Corporation, Maynard, Massachusetts. + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Digital not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL +DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + +************************************************************************/ +/* $XFree86: xc/lib/X11/Region.c,v 1.8 2001/12/14 19:54:05 dawes Exp $ */ +/* + * The functions in this file implement the Region abstraction, similar to one + * used in the X11 sample server. A Region is simply an area, as the name + * implies, and is implemented as a "y-x-banded" array of rectangles. To + * explain: Each Region is made up of a certain number of rectangles sorted + * by y coordinate first, and then by x coordinate. + * + * Furthermore, the rectangles are banded such that every rectangle with a + * given upper-left y coordinate (y1) will have the same lower-right y + * coordinate (y2) and vice versa. If a rectangle has scanlines in a band, it + * will span the entire vertical distance of the band. This means that some + * areas that could be merged into a taller rectangle will be represented as + * several shorter rectangles to account for shorter rectangles to its left + * or right but within its "vertical scope". + * + * An added constraint on the rectangles is that they must cover as much + * horizontal area as possible. E.g. no two rectangles in a band are allowed + * to touch. + * + * Whenever possible, bands will be merged together to cover a greater vertical + * distance (and thus reduce the number of rectangles). Two bands can be merged + * only if the bottom of one touches the top of the other and they have + * rectangles in the same places (of the same width, of course). This maintains + * the y-x-banding that's so nice to have... + */ + +#include "Xregion.h" +//#include "Xlibint.h" +//#include "Xutil.h" +#include "region.h" +//#include "poly.h" + +#ifdef DEBUG +#include +#define assert(expr) {if (!(expr)) fprintf(stderr,\ +"Assertion failed file %s, line %d: expr\n", __FILE__, __LINE__); } +#else +#define assert(expr) +#endif + +typedef void (*voidProcp)(); + +static void miRegionOp(); +/* Create a new empty region */ +Region +XCreateRegion() +{ + Region temp; + + if (! (temp = ( Region )Xmalloc( (unsigned) sizeof( REGION )))) + return (Region) NULL; + if (! (temp->rects = ( BOX * )Xmalloc( (unsigned) sizeof( BOX )))) { + Xfree((char *) temp); + return (Region) NULL; + } + temp->numRects = 0; + temp->extents.x1 = 0; + temp->extents.y1 = 0; + temp->extents.x2 = 0; + temp->extents.y2 = 0; + temp->size = 1; + return( temp ); +} + +int +XClipBox( r, rect ) + Region r; + XRectangle *rect; +{ + rect->x = r->extents.x1; + rect->y = r->extents.y1; + rect->width = r->extents.x2 - r->extents.x1; + rect->height = r->extents.y2 - r->extents.y1; + return 1; +} + +int +XUnionRectWithRegion(rect, source, dest) + register XRectangle *rect; + Region source, dest; +{ + REGION region; + + if (!rect->width || !rect->height) + return 0; + region.rects = ®ion.extents; + region.numRects = 1; + region.extents.x1 = rect->x; + region.extents.y1 = rect->y; + region.extents.x2 = rect->x + rect->width; + region.extents.y2 = rect->y + rect->height; + region.size = 1; + + return XUnionRegion(®ion, source, dest); +} + +/*- + *----------------------------------------------------------------------- + * miSetExtents -- + * Reset the extents of a region to what they should be. Called by + * miSubtract and miIntersect b/c they can't figure it out along the + * way or do so easily, as miUnion can. + * + * Results: + * None. + * + * Side Effects: + * The region's 'extents' structure is overwritten. + * + *----------------------------------------------------------------------- + */ +static void +miSetExtents (pReg) + Region pReg; +{ + register BoxPtr pBox, + pBoxEnd, + pExtents; + + if (pReg->numRects == 0) + { + pReg->extents.x1 = 0; + pReg->extents.y1 = 0; + pReg->extents.x2 = 0; + pReg->extents.y2 = 0; + return; + } + + pExtents = &pReg->extents; + pBox = pReg->rects; + pBoxEnd = &pBox[pReg->numRects - 1]; + + /* + * Since pBox is the first rectangle in the region, it must have the + * smallest y1 and since pBoxEnd is the last rectangle in the region, + * it must have the largest y2, because of banding. Initialize x1 and + * x2 from pBox and pBoxEnd, resp., as good things to initialize them + * to... + */ + pExtents->x1 = pBox->x1; + pExtents->y1 = pBox->y1; + pExtents->x2 = pBoxEnd->x2; + pExtents->y2 = pBoxEnd->y2; + + assert(pExtents->y1 < pExtents->y2); + while (pBox <= pBoxEnd) + { + if (pBox->x1 < pExtents->x1) + { + pExtents->x1 = pBox->x1; + } + if (pBox->x2 > pExtents->x2) + { + pExtents->x2 = pBox->x2; + } + pBox++; + } + assert(pExtents->x1 < pExtents->x2); +} + +extern void _XSetClipRectangles(); + +#if 0 +int +XSetRegion( dpy, gc, r ) + Display *dpy; + GC gc; + register Region r; +{ + register int i; + register XRectangle *xr, *pr; + register BOX *pb; + unsigned long total; + + LockDisplay (dpy); + total = r->numRects * sizeof (XRectangle); + if ((xr = (XRectangle *) _XAllocTemp(dpy, total))) { + for (pr = xr, pb = r->rects, i = r->numRects; --i >= 0; pr++, pb++) { + pr->x = pb->x1; + pr->y = pb->y1; + pr->width = pb->x2 - pb->x1; + pr->height = pb->y2 - pb->y1; + } + } + if (xr || !r->numRects) + _XSetClipRectangles(dpy, gc, 0, 0, xr, r->numRects, YXBanded); + if (xr) + _XFreeTemp(dpy, (char *)xr, total); + UnlockDisplay(dpy); + SyncHandle(); + return 1; +} +#endif + +int +XDestroyRegion( r ) + Region r; +{ + Xfree( (char *) r->rects ); + Xfree( (char *) r ); + return 1; +} + + +/* TranslateRegion(pRegion, x, y) + translates in place + added by raymond +*/ + +int +XOffsetRegion(pRegion, x, y) + register Region pRegion; + register int x; + register int y; +{ + register int nbox; + register BOX *pbox; + + pbox = pRegion->rects; + nbox = pRegion->numRects; + + while(nbox--) + { + pbox->x1 += x; + pbox->x2 += x; + pbox->y1 += y; + pbox->y2 += y; + pbox++; + } + pRegion->extents.x1 += x; + pRegion->extents.x2 += x; + pRegion->extents.y1 += y; + pRegion->extents.y2 += y; + return 1; +} + +/* + Utility procedure Compress: + Replace r by the region r', where + p in r' iff (Quantifer m <= dx) (p + m in r), and + Quantifier is Exists if grow is TRUE, For all if grow is FALSE, and + (x,y) + m = (x+m,y) if xdir is TRUE; (x,y+m) if xdir is FALSE. + + Thus, if xdir is TRUE and grow is FALSE, r is replaced by the region + of all points p such that p and the next dx points on the same + horizontal scan line are all in r. We do this using by noting + that p is the head of a run of length 2^i + k iff p is the head + of a run of length 2^i and p+2^i is the head of a run of length + k. Thus, the loop invariant: s contains the region corresponding + to the runs of length shift. r contains the region corresponding + to the runs of length 1 + dxo & (shift-1), where dxo is the original + value of dx. dx = dxo & ~(shift-1). As parameters, s and t are + scratch regions, so that we don't have to allocate them on every + call. +*/ + +#define ZOpRegion(a,b,c) if (grow) XUnionRegion(a,b,c); \ + else XIntersectRegion(a,b,c) +#define ZShiftRegion(a,b) if (xdir) XOffsetRegion(a,b,0); \ + else XOffsetRegion(a,0,b) +#define ZCopyRegion(a,b) XUnionRegion(a,a,b) + +static void +Compress(r, s, t, dx, xdir, grow) + Region r, s, t; + register unsigned dx; + register int xdir, grow; +{ + register unsigned shift = 1; + + ZCopyRegion(r, s); + while (dx) { + if (dx & shift) { + ZShiftRegion(r, -(int)shift); + ZOpRegion(r, s, r); + dx -= shift; + if (!dx) break; + } + ZCopyRegion(s, t); + ZShiftRegion(s, -(int)shift); + ZOpRegion(s, t, s); + shift <<= 1; + } +} + +#undef ZOpRegion +#undef ZShiftRegion +#undef ZCopyRegion + +int +XShrinkRegion(r, dx, dy) + Region r; + int dx, dy; +{ + Region s, t; + int grow; + + if (!dx && !dy) return 0; + if ((! (s = XCreateRegion())) || (! (t = XCreateRegion()))) return 0; + if ((grow = (dx < 0))) dx = -dx; + if (dx) Compress(r, s, t, (unsigned) 2*dx, TRUE, grow); + if ((grow = (dy < 0))) dy = -dy; + if (dy) Compress(r, s, t, (unsigned) 2*dy, FALSE, grow); + XOffsetRegion(r, dx, dy); + XDestroyRegion(s); + XDestroyRegion(t); + return 0; +} + +#ifdef notdef +/*********************************************************** + * Bop down the array of rects until we have passed + * scanline y. numRects is the size of the array. + ***********************************************************/ + +static BOX +*IndexRects(rects, numRects, y) + register BOX *rects; + register int numRects; + register int y; +{ + while ((numRects--) && (rects->y2 <= y)) + rects++; + return(rects); +} +#endif + +/*====================================================================== + * Region Intersection + *====================================================================*/ +/*- + *----------------------------------------------------------------------- + * miIntersectO -- + * Handle an overlapping band for miIntersect. + * + * Results: + * None. + * + * Side Effects: + * Rectangles may be added to the region. + * + *----------------------------------------------------------------------- + */ +/* static void*/ +static int +miIntersectO (pReg, r1, r1End, r2, r2End, y1, y2) + register Region pReg; + register BoxPtr r1; + BoxPtr r1End; + register BoxPtr r2; + BoxPtr r2End; + short y1; + short y2; +{ + register short x1; + register short x2; + register BoxPtr pNextRect; + + pNextRect = &pReg->rects[pReg->numRects]; + + while ((r1 != r1End) && (r2 != r2End)) + { + x1 = max(r1->x1,r2->x1); + x2 = min(r1->x2,r2->x2); + + /* + * If there's any overlap between the two rectangles, add that + * overlap to the new region. + * There's no need to check for subsumption because the only way + * such a need could arise is if some region has two rectangles + * right next to each other. Since that should never happen... + */ + if (x1 < x2) + { + assert(y1rects); + pNextRect->x1 = x1; + pNextRect->y1 = y1; + pNextRect->x2 = x2; + pNextRect->y2 = y2; + pReg->numRects += 1; + pNextRect++; + assert(pReg->numRects <= pReg->size); + } + + /* + * Need to advance the pointers. Shift the one that extends + * to the right the least, since the other still has a chance to + * overlap with that region's next rectangle, if you see what I mean. + */ + if (r1->x2 < r2->x2) + { + r1++; + } + else if (r2->x2 < r1->x2) + { + r2++; + } + else + { + r1++; + r2++; + } + } + return 0; /* lint */ +} + +int +XIntersectRegion(reg1, reg2, newReg) + Region reg1; + Region reg2; /* source regions */ + register Region newReg; /* destination Region */ +{ + /* check for trivial reject */ + if ( (!(reg1->numRects)) || (!(reg2->numRects)) || + (!EXTENTCHECK(®1->extents, ®2->extents))) + newReg->numRects = 0; + else + miRegionOp (newReg, reg1, reg2, + (voidProcp) miIntersectO, (voidProcp) NULL, (voidProcp) NULL); + + /* + * Can't alter newReg's extents before we call miRegionOp because + * it might be one of the source regions and miRegionOp depends + * on the extents of those regions being the same. Besides, this + * way there's no checking against rectangles that will be nuked + * due to coalescing, so we have to examine fewer rectangles. + */ + miSetExtents(newReg); + return 1; +} + +static void +miRegionCopy(dstrgn, rgn) + register Region dstrgn; + register Region rgn; + +{ + if (dstrgn != rgn) /* don't want to copy to itself */ + { + if (dstrgn->size < rgn->numRects) + { + if (dstrgn->rects) + { + BOX *prevRects = dstrgn->rects; + + if (! (dstrgn->rects = (BOX *) + Xrealloc((char *) dstrgn->rects, + (unsigned) rgn->numRects * (sizeof(BOX))))) { + Xfree(prevRects); + return; + } + } + dstrgn->size = rgn->numRects; + } + dstrgn->numRects = rgn->numRects; + dstrgn->extents.x1 = rgn->extents.x1; + dstrgn->extents.y1 = rgn->extents.y1; + dstrgn->extents.x2 = rgn->extents.x2; + dstrgn->extents.y2 = rgn->extents.y2; + + memcpy((char *) dstrgn->rects, (char *) rgn->rects, + (int) (rgn->numRects * sizeof(BOX))); + } +} + +#ifdef notdef + +/* + * combinRegs(newReg, reg1, reg2) + * if one region is above or below the other. +*/ + +static void +combineRegs(newReg, reg1, reg2) + register Region newReg; + Region reg1; + Region reg2; +{ + register Region tempReg; + register BOX *rects; + register BOX *rects1; + register BOX *rects2; + register int total; + + rects1 = reg1->rects; + rects2 = reg2->rects; + + total = reg1->numRects + reg2->numRects; + if (! (tempReg = XCreateRegion())) + return; + tempReg->size = total; + /* region 1 is below region 2 */ + if (reg1->extents.y1 > reg2->extents.y1) + { + miRegionCopy(tempReg, reg2); + rects = &tempReg->rects[tempReg->numRects]; + total -= tempReg->numRects; + while (total--) + *rects++ = *rects1++; + } + else + { + miRegionCopy(tempReg, reg1); + rects = &tempReg->rects[tempReg->numRects]; + total -= tempReg->numRects; + while (total--) + *rects++ = *rects2++; + } + tempReg->extents = reg1->extents; + tempReg->numRects = reg1->numRects + reg2->numRects; + EXTENTS(®2->extents, tempReg); + miRegionCopy(newReg, tempReg); + Xfree((char *)tempReg); +} + +/* + * QuickCheck checks to see if it does not have to go through all the + * the ugly code for the region call. It returns 1 if it did all + * the work for Union, otherwise 0 - still work to be done. +*/ + +static int +QuickCheck(newReg, reg1, reg2) + Region newReg, reg1, reg2; +{ + + /* if unioning with itself or no rects to union with */ + if ( (reg1 == reg2) || (!(reg1->numRects)) ) + { + miRegionCopy(newReg, reg2); + return TRUE; + } + + /* if nothing to union */ + if (!(reg2->numRects)) + { + miRegionCopy(newReg, reg1); + return TRUE; + } + + /* could put an extent check to see if add above or below */ + + if ((reg1->extents.y1 >= reg2->extents.y2) || + (reg2->extents.y1 >= reg1->extents.y2) ) + { + combineRegs(newReg, reg1, reg2); + return TRUE; + } + return FALSE; +} + +/* TopRects(rects, reg1, reg2) + * N.B. We now assume that reg1 and reg2 intersect. Therefore we are + * NOT checking in the two while loops for stepping off the end of the + * region. + */ + +static int +TopRects(newReg, rects, reg1, reg2, FirstRect) + register Region newReg; + register BOX *rects; + register Region reg1; + register Region reg2; + BOX *FirstRect; +{ + register BOX *tempRects; + + /* need to add some rects from region 1 */ + if (reg1->extents.y1 < reg2->extents.y1) + { + tempRects = reg1->rects; + while(tempRects->y1 < reg2->extents.y1) + { + MEMCHECK(newReg, rects, FirstRect); + ADDRECTNOX(newReg,rects, tempRects->x1, tempRects->y1, + tempRects->x2, MIN(tempRects->y2, reg2->extents.y1)); + tempRects++; + } + } + /* need to add some rects from region 2 */ + if (reg2->extents.y1 < reg1->extents.y1) + { + tempRects = reg2->rects; + while (tempRects->y1 < reg1->extents.y1) + { + MEMCHECK(newReg, rects, FirstRect); + ADDRECTNOX(newReg, rects, tempRects->x1,tempRects->y1, + tempRects->x2, MIN(tempRects->y2, reg1->extents.y1)); + tempRects++; + } + } + return 1; +} +#endif + +/*====================================================================== + * Generic Region Operator + *====================================================================*/ + +/*- + *----------------------------------------------------------------------- + * miCoalesce -- + * Attempt to merge the boxes in the current band with those in the + * previous one. Used only by miRegionOp. + * + * Results: + * The new index for the previous band. + * + * Side Effects: + * If coalescing takes place: + * - rectangles in the previous band will have their y2 fields + * altered. + * - pReg->numRects will be decreased. + * + *----------------------------------------------------------------------- + */ +/* static int*/ +static int +miCoalesce (pReg, prevStart, curStart) + register Region pReg; /* Region to coalesce */ + int prevStart; /* Index of start of previous band */ + int curStart; /* Index of start of current band */ +{ + register BoxPtr pPrevBox; /* Current box in previous band */ + register BoxPtr pCurBox; /* Current box in current band */ + register BoxPtr pRegEnd; /* End of region */ + int curNumRects; /* Number of rectangles in current + * band */ + int prevNumRects; /* Number of rectangles in previous + * band */ + int bandY1; /* Y1 coordinate for current band */ + + pRegEnd = &pReg->rects[pReg->numRects]; + + pPrevBox = &pReg->rects[prevStart]; + prevNumRects = curStart - prevStart; + + /* + * Figure out how many rectangles are in the current band. Have to do + * this because multiple bands could have been added in miRegionOp + * at the end when one region has been exhausted. + */ + pCurBox = &pReg->rects[curStart]; + bandY1 = pCurBox->y1; + for (curNumRects = 0; + (pCurBox != pRegEnd) && (pCurBox->y1 == bandY1); + curNumRects++) + { + pCurBox++; + } + + if (pCurBox != pRegEnd) + { + /* + * If more than one band was added, we have to find the start + * of the last band added so the next coalescing job can start + * at the right place... (given when multiple bands are added, + * this may be pointless -- see above). + */ + pRegEnd--; + while (pRegEnd[-1].y1 == pRegEnd->y1) + { + pRegEnd--; + } + curStart = pRegEnd - pReg->rects; + pRegEnd = pReg->rects + pReg->numRects; + } + + if ((curNumRects == prevNumRects) && (curNumRects != 0)) { + pCurBox -= curNumRects; + /* + * The bands may only be coalesced if the bottom of the previous + * matches the top scanline of the current. + */ + if (pPrevBox->y2 == pCurBox->y1) + { + /* + * Make sure the bands have boxes in the same places. This + * assumes that boxes have been added in such a way that they + * cover the most area possible. I.e. two boxes in a band must + * have some horizontal space between them. + */ + do + { + if ((pPrevBox->x1 != pCurBox->x1) || + (pPrevBox->x2 != pCurBox->x2)) + { + /* + * The bands don't line up so they can't be coalesced. + */ + return (curStart); + } + pPrevBox++; + pCurBox++; + prevNumRects -= 1; + } while (prevNumRects != 0); + + pReg->numRects -= curNumRects; + pCurBox -= curNumRects; + pPrevBox -= curNumRects; + + /* + * The bands may be merged, so set the bottom y of each box + * in the previous band to that of the corresponding box in + * the current band. + */ + do + { + pPrevBox->y2 = pCurBox->y2; + pPrevBox++; + pCurBox++; + curNumRects -= 1; + } while (curNumRects != 0); + + /* + * If only one band was added to the region, we have to backup + * curStart to the start of the previous band. + * + * If more than one band was added to the region, copy the + * other bands down. The assumption here is that the other bands + * came from the same region as the current one and no further + * coalescing can be done on them since it's all been done + * already... curStart is already in the right place. + */ + if (pCurBox == pRegEnd) + { + curStart = prevStart; + } + else + { + do + { + *pPrevBox++ = *pCurBox++; + } while (pCurBox != pRegEnd); + } + + } + } + return (curStart); +} + +/*- + *----------------------------------------------------------------------- + * miRegionOp -- + * Apply an operation to two regions. Called by miUnion, miInverse, + * miSubtract, miIntersect... + * + * Results: + * None. + * + * Side Effects: + * The new region is overwritten. + * + * Notes: + * The idea behind this function is to view the two regions as sets. + * Together they cover a rectangle of area that this function divides + * into horizontal bands where points are covered only by one region + * or by both. For the first case, the nonOverlapFunc is called with + * each the band and the band's upper and lower extents. For the + * second, the overlapFunc is called to process the entire band. It + * is responsible for clipping the rectangles in the band, though + * this function provides the boundaries. + * At the end of each band, the new region is coalesced, if possible, + * to reduce the number of rectangles in the region. + * + *----------------------------------------------------------------------- + */ +/* static void*/ +static void +miRegionOp(newReg, reg1, reg2, overlapFunc, nonOverlap1Func, nonOverlap2Func) + register Region newReg; /* Place to store result */ + Region reg1; /* First region in operation */ + Region reg2; /* 2d region in operation */ + void (*overlapFunc)(); /* Function to call for over- + * lapping bands */ + void (*nonOverlap1Func)(); /* Function to call for non- + * overlapping bands in region + * 1 */ + void (*nonOverlap2Func)(); /* Function to call for non- + * overlapping bands in region + * 2 */ +{ + register BoxPtr r1; /* Pointer into first region */ + register BoxPtr r2; /* Pointer into 2d region */ + BoxPtr r1End; /* End of 1st region */ + BoxPtr r2End; /* End of 2d region */ + register short ybot; /* Bottom of intersection */ + register short ytop; /* Top of intersection */ + BoxPtr oldRects; /* Old rects for newReg */ + int prevBand; /* Index of start of + * previous band in newReg */ + int curBand; /* Index of start of current + * band in newReg */ + register BoxPtr r1BandEnd; /* End of current band in r1 */ + register BoxPtr r2BandEnd; /* End of current band in r2 */ + short top; /* Top of non-overlapping + * band */ + short bot; /* Bottom of non-overlapping + * band */ + + /* + * Initialization: + * set r1, r2, r1End and r2End appropriately, preserve the important + * parts of the destination region until the end in case it's one of + * the two source regions, then mark the "new" region empty, allocating + * another array of rectangles for it to use. + */ + r1 = reg1->rects; + r2 = reg2->rects; + r1End = r1 + reg1->numRects; + r2End = r2 + reg2->numRects; + + oldRects = newReg->rects; + + EMPTY_REGION(newReg); + + /* + * Allocate a reasonable number of rectangles for the new region. The idea + * is to allocate enough so the individual functions don't need to + * reallocate and copy the array, which is time consuming, yet we don't + * have to worry about using too much memory. I hope to be able to + * nuke the Xrealloc() at the end of this function eventually. + */ + newReg->size = max(reg1->numRects,reg2->numRects) * 2; + + if (! (newReg->rects = (BoxPtr) + Xmalloc ((unsigned) (sizeof(BoxRec) * newReg->size)))) { + newReg->size = 0; + return; + } + + /* + * Initialize ybot and ytop. + * In the upcoming loop, ybot and ytop serve different functions depending + * on whether the band being handled is an overlapping or non-overlapping + * band. + * In the case of a non-overlapping band (only one of the regions + * has points in the band), ybot is the bottom of the most recent + * intersection and thus clips the top of the rectangles in that band. + * ytop is the top of the next intersection between the two regions and + * serves to clip the bottom of the rectangles in the current band. + * For an overlapping band (where the two regions intersect), ytop clips + * the top of the rectangles of both regions and ybot clips the bottoms. + */ + if (reg1->extents.y1 < reg2->extents.y1) + ybot = reg1->extents.y1; + else + ybot = reg2->extents.y1; + + /* + * prevBand serves to mark the start of the previous band so rectangles + * can be coalesced into larger rectangles. qv. miCoalesce, above. + * In the beginning, there is no previous band, so prevBand == curBand + * (curBand is set later on, of course, but the first band will always + * start at index 0). prevBand and curBand must be indices because of + * the possible expansion, and resultant moving, of the new region's + * array of rectangles. + */ + prevBand = 0; + + do + { + curBand = newReg->numRects; + + /* + * This algorithm proceeds one source-band (as opposed to a + * destination band, which is determined by where the two regions + * intersect) at a time. r1BandEnd and r2BandEnd serve to mark the + * rectangle after the last one in the current band for their + * respective regions. + */ + r1BandEnd = r1; + while ((r1BandEnd != r1End) && (r1BandEnd->y1 == r1->y1)) + { + r1BandEnd++; + } + + r2BandEnd = r2; + while ((r2BandEnd != r2End) && (r2BandEnd->y1 == r2->y1)) + { + r2BandEnd++; + } + + /* + * First handle the band that doesn't intersect, if any. + * + * Note that attention is restricted to one band in the + * non-intersecting region at once, so if a region has n + * bands between the current position and the next place it overlaps + * the other, this entire loop will be passed through n times. + */ + if (r1->y1 < r2->y1) + { + top = max(r1->y1,ybot); + bot = min(r1->y2,r2->y1); + + if ((top != bot) && (nonOverlap1Func != (void (*)())NULL)) + { + (* nonOverlap1Func) (newReg, r1, r1BandEnd, top, bot); + } + + ytop = r2->y1; + } + else if (r2->y1 < r1->y1) + { + top = max(r2->y1,ybot); + bot = min(r2->y2,r1->y1); + + if ((top != bot) && (nonOverlap2Func != (void (*)())NULL)) + { + (* nonOverlap2Func) (newReg, r2, r2BandEnd, top, bot); + } + + ytop = r1->y1; + } + else + { + ytop = r1->y1; + } + + /* + * If any rectangles got added to the region, try and coalesce them + * with rectangles from the previous band. Note we could just do + * this test in miCoalesce, but some machines incur a not + * inconsiderable cost for function calls, so... + */ + if (newReg->numRects != curBand) + { + prevBand = miCoalesce (newReg, prevBand, curBand); + } + + /* + * Now see if we've hit an intersecting band. The two bands only + * intersect if ybot > ytop + */ + ybot = min(r1->y2, r2->y2); + curBand = newReg->numRects; + if (ybot > ytop) + { + (* overlapFunc) (newReg, r1, r1BandEnd, r2, r2BandEnd, ytop, ybot); + + } + + if (newReg->numRects != curBand) + { + prevBand = miCoalesce (newReg, prevBand, curBand); + } + + /* + * If we've finished with a band (y2 == ybot) we skip forward + * in the region to the next band. + */ + if (r1->y2 == ybot) + { + r1 = r1BandEnd; + } + if (r2->y2 == ybot) + { + r2 = r2BandEnd; + } + } while ((r1 != r1End) && (r2 != r2End)); + + /* + * Deal with whichever region still has rectangles left. + */ + curBand = newReg->numRects; + if (r1 != r1End) + { + if (nonOverlap1Func != (void (*)())NULL) + { + do + { + r1BandEnd = r1; + while ((r1BandEnd < r1End) && (r1BandEnd->y1 == r1->y1)) + { + r1BandEnd++; + } + (* nonOverlap1Func) (newReg, r1, r1BandEnd, + max(r1->y1,ybot), r1->y2); + r1 = r1BandEnd; + } while (r1 != r1End); + } + } + else if ((r2 != r2End) && (nonOverlap2Func != (void (*)())NULL)) + { + do + { + r2BandEnd = r2; + while ((r2BandEnd < r2End) && (r2BandEnd->y1 == r2->y1)) + { + r2BandEnd++; + } + (* nonOverlap2Func) (newReg, r2, r2BandEnd, + max(r2->y1,ybot), r2->y2); + r2 = r2BandEnd; + } while (r2 != r2End); + } + + if (newReg->numRects != curBand) + { + (void) miCoalesce (newReg, prevBand, curBand); + } + + /* + * A bit of cleanup. To keep regions from growing without bound, + * we shrink the array of rectangles to match the new number of + * rectangles in the region. This never goes to 0, however... + * + * Only do this stuff if the number of rectangles allocated is more than + * twice the number of rectangles in the region (a simple optimization...). + */ + if (newReg->numRects < (newReg->size >> 1)) + { + if (REGION_NOT_EMPTY(newReg)) + { + BoxPtr prev_rects = newReg->rects; + newReg->size = newReg->numRects; + newReg->rects = (BoxPtr) Xrealloc ((char *) newReg->rects, + (unsigned) (sizeof(BoxRec) * newReg->size)); + if (! newReg->rects) + newReg->rects = prev_rects; + } + else + { + /* + * No point in doing the extra work involved in an Xrealloc if + * the region is empty + */ + newReg->size = 1; + Xfree((char *) newReg->rects); + newReg->rects = (BoxPtr) Xmalloc(sizeof(BoxRec)); + } + } + Xfree ((char *) oldRects); + return; +} + + +/*====================================================================== + * Region Union + *====================================================================*/ + +/*- + *----------------------------------------------------------------------- + * miUnionNonO -- + * Handle a non-overlapping band for the union operation. Just + * Adds the rectangles into the region. Doesn't have to check for + * subsumption or anything. + * + * Results: + * None. + * + * Side Effects: + * pReg->numRects is incremented and the final rectangles overwritten + * with the rectangles we're passed. + * + *----------------------------------------------------------------------- + */ +/* static void*/ +static int +miUnionNonO (pReg, r, rEnd, y1, y2) + register Region pReg; + register BoxPtr r; + BoxPtr rEnd; + register short y1; + register short y2; +{ + register BoxPtr pNextRect; + + pNextRect = &pReg->rects[pReg->numRects]; + + assert(y1 < y2); + + while (r != rEnd) + { + assert(r->x1 < r->x2); + MEMCHECK(pReg, pNextRect, pReg->rects); + pNextRect->x1 = r->x1; + pNextRect->y1 = y1; + pNextRect->x2 = r->x2; + pNextRect->y2 = y2; + pReg->numRects += 1; + pNextRect++; + + assert(pReg->numRects<=pReg->size); + r++; + } + return 0; /* lint */ +} + + +/*- + *----------------------------------------------------------------------- + * miUnionO -- + * Handle an overlapping band for the union operation. Picks the + * left-most rectangle each time and merges it into the region. + * + * Results: + * None. + * + * Side Effects: + * Rectangles are overwritten in pReg->rects and pReg->numRects will + * be changed. + * + *----------------------------------------------------------------------- + */ + +/* static void*/ +static int +miUnionO (pReg, r1, r1End, r2, r2End, y1, y2) + register Region pReg; + register BoxPtr r1; + BoxPtr r1End; + register BoxPtr r2; + BoxPtr r2End; + register short y1; + register short y2; +{ + register BoxPtr pNextRect; + + pNextRect = &pReg->rects[pReg->numRects]; + +#define MERGERECT(r) \ + if ((pReg->numRects != 0) && \ + (pNextRect[-1].y1 == y1) && \ + (pNextRect[-1].y2 == y2) && \ + (pNextRect[-1].x2 >= r->x1)) \ + { \ + if (pNextRect[-1].x2 < r->x2) \ + { \ + pNextRect[-1].x2 = r->x2; \ + assert(pNextRect[-1].x1rects); \ + pNextRect->y1 = y1; \ + pNextRect->y2 = y2; \ + pNextRect->x1 = r->x1; \ + pNextRect->x2 = r->x2; \ + pReg->numRects += 1; \ + pNextRect += 1; \ + } \ + assert(pReg->numRects<=pReg->size);\ + r++; + + assert (y1x1 < r2->x1) + { + MERGERECT(r1); + } + else + { + MERGERECT(r2); + } + } + + if (r1 != r1End) + { + do + { + MERGERECT(r1); + } while (r1 != r1End); + } + else while (r2 != r2End) + { + MERGERECT(r2); + } + return 0; /* lint */ +} + +int +XUnionRegion(reg1, reg2, newReg) + Region reg1; + Region reg2; /* source regions */ + Region newReg; /* destination Region */ +{ + /* checks all the simple cases */ + + /* + * Region 1 and 2 are the same or region 1 is empty + */ + if ( (reg1 == reg2) || (!(reg1->numRects)) ) + { + if (newReg != reg2) + miRegionCopy(newReg, reg2); + return 1; + } + + /* + * if nothing to union (region 2 empty) + */ + if (!(reg2->numRects)) + { + if (newReg != reg1) + miRegionCopy(newReg, reg1); + return 1; + } + + /* + * Region 1 completely subsumes region 2 + */ + if ((reg1->numRects == 1) && + (reg1->extents.x1 <= reg2->extents.x1) && + (reg1->extents.y1 <= reg2->extents.y1) && + (reg1->extents.x2 >= reg2->extents.x2) && + (reg1->extents.y2 >= reg2->extents.y2)) + { + if (newReg != reg1) + miRegionCopy(newReg, reg1); + return 1; + } + + /* + * Region 2 completely subsumes region 1 + */ + if ((reg2->numRects == 1) && + (reg2->extents.x1 <= reg1->extents.x1) && + (reg2->extents.y1 <= reg1->extents.y1) && + (reg2->extents.x2 >= reg1->extents.x2) && + (reg2->extents.y2 >= reg1->extents.y2)) + { + if (newReg != reg2) + miRegionCopy(newReg, reg2); + return 1; + } + + miRegionOp (newReg, reg1, reg2, (voidProcp) miUnionO, + (voidProcp) miUnionNonO, (voidProcp) miUnionNonO); + + newReg->extents.x1 = min(reg1->extents.x1, reg2->extents.x1); + newReg->extents.y1 = min(reg1->extents.y1, reg2->extents.y1); + newReg->extents.x2 = max(reg1->extents.x2, reg2->extents.x2); + newReg->extents.y2 = max(reg1->extents.y2, reg2->extents.y2); + + return 1; +} + + +/*====================================================================== + * Region Subtraction + *====================================================================*/ + +/*- + *----------------------------------------------------------------------- + * miSubtractNonO -- + * Deal with non-overlapping band for subtraction. Any parts from + * region 2 we discard. Anything from region 1 we add to the region. + * + * Results: + * None. + * + * Side Effects: + * pReg may be affected. + * + *----------------------------------------------------------------------- + */ +/* static void*/ +static int +miSubtractNonO1 (pReg, r, rEnd, y1, y2) + register Region pReg; + register BoxPtr r; + BoxPtr rEnd; + register short y1; + register short y2; +{ + register BoxPtr pNextRect; + + pNextRect = &pReg->rects[pReg->numRects]; + + assert(y1x1x2); + MEMCHECK(pReg, pNextRect, pReg->rects); + pNextRect->x1 = r->x1; + pNextRect->y1 = y1; + pNextRect->x2 = r->x2; + pNextRect->y2 = y2; + pReg->numRects += 1; + pNextRect++; + + assert(pReg->numRects <= pReg->size); + + r++; + } + return 0; /* lint */ +} + +/*- + *----------------------------------------------------------------------- + * miSubtractO -- + * Overlapping band subtraction. x1 is the left-most point not yet + * checked. + * + * Results: + * None. + * + * Side Effects: + * pReg may have rectangles added to it. + * + *----------------------------------------------------------------------- + */ +/* static void*/ +static int +miSubtractO (pReg, r1, r1End, r2, r2End, y1, y2) + register Region pReg; + register BoxPtr r1; + BoxPtr r1End; + register BoxPtr r2; + BoxPtr r2End; + register short y1; + register short y2; +{ + register BoxPtr pNextRect; + register int x1; + + x1 = r1->x1; + + assert(y1rects[pReg->numRects]; + + while ((r1 != r1End) && (r2 != r2End)) + { + if (r2->x2 <= x1) + { + /* + * Subtrahend missed the boat: go to next subtrahend. + */ + r2++; + } + else if (r2->x1 <= x1) + { + /* + * Subtrahend preceeds minuend: nuke left edge of minuend. + */ + x1 = r2->x2; + if (x1 >= r1->x2) + { + /* + * Minuend completely covered: advance to next minuend and + * reset left fence to edge of new minuend. + */ + r1++; + if (r1 != r1End) + x1 = r1->x1; + } + else + { + /* + * Subtrahend now used up since it doesn't extend beyond + * minuend + */ + r2++; + } + } + else if (r2->x1 < r1->x2) + { + /* + * Left part of subtrahend covers part of minuend: add uncovered + * part of minuend to region and skip to next subtrahend. + */ + assert(x1x1); + MEMCHECK(pReg, pNextRect, pReg->rects); + pNextRect->x1 = x1; + pNextRect->y1 = y1; + pNextRect->x2 = r2->x1; + pNextRect->y2 = y2; + pReg->numRects += 1; + pNextRect++; + + assert(pReg->numRects<=pReg->size); + + x1 = r2->x2; + if (x1 >= r1->x2) + { + /* + * Minuend used up: advance to new... + */ + r1++; + if (r1 != r1End) + x1 = r1->x1; + } + else + { + /* + * Subtrahend used up + */ + r2++; + } + } + else + { + /* + * Minuend used up: add any remaining piece before advancing. + */ + if (r1->x2 > x1) + { + MEMCHECK(pReg, pNextRect, pReg->rects); + pNextRect->x1 = x1; + pNextRect->y1 = y1; + pNextRect->x2 = r1->x2; + pNextRect->y2 = y2; + pReg->numRects += 1; + pNextRect++; + assert(pReg->numRects<=pReg->size); + } + r1++; + if (r1 != r1End) + x1 = r1->x1; + } + } + + /* + * Add remaining minuend rectangles to region. + */ + while (r1 != r1End) + { + assert(x1x2); + MEMCHECK(pReg, pNextRect, pReg->rects); + pNextRect->x1 = x1; + pNextRect->y1 = y1; + pNextRect->x2 = r1->x2; + pNextRect->y2 = y2; + pReg->numRects += 1; + pNextRect++; + + assert(pReg->numRects<=pReg->size); + + r1++; + if (r1 != r1End) + { + x1 = r1->x1; + } + } + return 0; /* lint */ +} + +/*- + *----------------------------------------------------------------------- + * miSubtract -- + * Subtract regS from regM and leave the result in regD. + * S stands for subtrahend, M for minuend and D for difference. + * + * Results: + * TRUE. + * + * Side Effects: + * regD is overwritten. + * + *----------------------------------------------------------------------- + */ + +int +XSubtractRegion(regM, regS, regD) + Region regM; + Region regS; + register Region regD; +{ + /* check for trivial reject */ + if ( (!(regM->numRects)) || (!(regS->numRects)) || + (!EXTENTCHECK(®M->extents, ®S->extents)) ) + { + miRegionCopy(regD, regM); + return 1; + } + + miRegionOp (regD, regM, regS, (voidProcp) miSubtractO, + (voidProcp) miSubtractNonO1, (voidProcp) NULL); + + /* + * Can't alter newReg's extents before we call miRegionOp because + * it might be one of the source regions and miRegionOp depends + * on the extents of those regions being the unaltered. Besides, this + * way there's no checking against rectangles that will be nuked + * due to coalescing, so we have to examine fewer rectangles. + */ + miSetExtents (regD); + return 1; +} + +int +XXorRegion( sra, srb, dr ) + Region sra, srb, dr; +{ + Region tra, trb; + + if ((! (tra = XCreateRegion())) || (! (trb = XCreateRegion()))) + return 0; + (void) XSubtractRegion(sra,srb,tra); + (void) XSubtractRegion(srb,sra,trb); + (void) XUnionRegion(tra,trb,dr); + XDestroyRegion(tra); + XDestroyRegion(trb); + return 0; +} + +/* + * Check to see if the region is empty. Assumes a region is passed + * as a parameter + */ +int +XEmptyRegion( r ) + Region r; +{ + if( r->numRects == 0 ) return TRUE; + else return FALSE; +} + +/* + * Check to see if two regions are equal + */ +int +XEqualRegion( r1, r2 ) + Region r1, r2; +{ + int i; + + if( r1->numRects != r2->numRects ) return FALSE; + else if( r1->numRects == 0 ) return TRUE; + else if ( r1->extents.x1 != r2->extents.x1 ) return FALSE; + else if ( r1->extents.x2 != r2->extents.x2 ) return FALSE; + else if ( r1->extents.y1 != r2->extents.y1 ) return FALSE; + else if ( r1->extents.y2 != r2->extents.y2 ) return FALSE; + else for( i=0; i < r1->numRects; i++ ) { + if ( r1->rects[i].x1 != r2->rects[i].x1 ) return FALSE; + else if ( r1->rects[i].x2 != r2->rects[i].x2 ) return FALSE; + else if ( r1->rects[i].y1 != r2->rects[i].y1 ) return FALSE; + else if ( r1->rects[i].y2 != r2->rects[i].y2 ) return FALSE; + } + return TRUE; +} + +int +XPointInRegion( pRegion, x, y ) + Region pRegion; + int x, y; +{ + int i; + + if (pRegion->numRects == 0) + return FALSE; + if (!INBOX(pRegion->extents, x, y)) + return FALSE; + for (i=0; inumRects; i++) + { + if (INBOX (pRegion->rects[i], x, y)) + return TRUE; + } + return FALSE; +} + +int +XRectInRegion(region, rx, ry, rwidth, rheight) + register Region region; + int rx, ry; + unsigned int rwidth, rheight; +{ + register BoxPtr pbox; + register BoxPtr pboxEnd; + Box rect; + register BoxPtr prect = ▭ + int partIn, partOut; + + prect->x1 = rx; + prect->y1 = ry; + prect->x2 = rwidth + rx; + prect->y2 = rheight + ry; + + /* this is (just) a useful optimization */ + if ((region->numRects == 0) || !EXTENTCHECK(®ion->extents, prect)) + return(RectangleOut); + + partOut = FALSE; + partIn = FALSE; + + /* can stop when both partOut and partIn are TRUE, or we reach prect->y2 */ + for (pbox = region->rects, pboxEnd = pbox + region->numRects; + pbox < pboxEnd; + pbox++) + { + + if (pbox->y2 <= ry) + continue; /* getting up to speed or skipping remainder of band */ + + if (pbox->y1 > ry) + { + partOut = TRUE; /* missed part of rectangle above */ + if (partIn || (pbox->y1 >= prect->y2)) + break; + ry = pbox->y1; /* x guaranteed to be == prect->x1 */ + } + + if (pbox->x2 <= rx) + continue; /* not far enough over yet */ + + if (pbox->x1 > rx) + { + partOut = TRUE; /* missed part of rectangle to left */ + if (partIn) + break; + } + + if (pbox->x1 < prect->x2) + { + partIn = TRUE; /* definitely overlap */ + if (partOut) + break; + } + + if (pbox->x2 >= prect->x2) + { + ry = pbox->y2; /* finished with this band */ + if (ry >= prect->y2) + break; + rx = prect->x1; /* reset x out to left again */ + } else + { + /* + * Because boxes in a band are maximal width, if the first box + * to overlap the rectangle doesn't completely cover it in that + * band, the rectangle must be partially out, since some of it + * will be uncovered in that band. partIn will have been set true + * by now... + */ + break; + } + + } + + return(partIn ? ((ry < prect->y2) ? RectanglePart : RectangleIn) : + RectangleOut); +} diff --git a/src/vnc/common/Xregion/Xregion.h b/src/vnc/common/Xregion/Xregion.h new file mode 100644 index 00000000..7fa44d96 --- /dev/null +++ b/src/vnc/common/Xregion/Xregion.h @@ -0,0 +1,220 @@ +/* $Xorg: Xutil.h,v 1.8 2001/02/09 02:03:39 xorgcvs Exp $ */ + +/*********************************************************** + +Copyright 1987, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. + + +Copyright 1987 by Digital Equipment Corporation, Maynard, Massachusetts. + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Digital not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL +DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + +******************************************************************/ +/* $XFree86: xc/lib/X11/Xutil.h,v 3.4 2001/12/14 19:54:10 dawes Exp $ */ + +#ifndef _XREGION_H_ +#define _XREGION_H_ + +// - Faked defines to fool the X11 region code + +#include +#include + +#define Bool int +#define Xmalloc malloc +#define Xfree free +#define Xrealloc realloc + +#ifndef max +#define max(a,b) (((a) > (b)) ? (a) : (b)) +#endif + +#ifndef min +#define min(a,b) (((a) < (b)) ? (a) : (b)) +#endif + +#define NeedFunctionPrototypes 1 + +// - Cribbed from Xlib.h + +typedef struct { + short x, y; +} XPoint; + +typedef struct { + short x, y; + unsigned short width, height; +} XRectangle; + +/* + * opaque reference to Region data type + */ +typedef struct _XRegion *Region; + +/* Return values from XRectInRegion() */ + +#define RectangleOut 0 +#define RectangleIn 1 +#define RectanglePart 2 + +#ifdef __cplusplus +extern "C" { +#endif + +extern int XClipBox( +#if NeedFunctionPrototypes + Region /* r */, + XRectangle* /* rect_return */ +#endif +); + +extern Region XCreateRegion( +#if NeedFunctionPrototypes + void +#endif +); + +extern const char *XDefaultString (void); + +extern int XDestroyRegion( +#if NeedFunctionPrototypes + Region /* r */ +#endif +); + +extern int XEmptyRegion( +#if NeedFunctionPrototypes + Region /* r */ +#endif +); + +extern int XEqualRegion( +#if NeedFunctionPrototypes + Region /* r1 */, + Region /* r2 */ +#endif +); + +extern int XIntersectRegion( +#if NeedFunctionPrototypes + Region /* sra */, + Region /* srb */, + Region /* dr_return */ +#endif +); + +extern int XOffsetRegion( +#if NeedFunctionPrototypes + Region /* r */, + int /* dx */, + int /* dy */ +#endif +); + +extern Bool XPointInRegion( +#if NeedFunctionPrototypes + Region /* r */, + int /* x */, + int /* y */ +#endif +); + +extern Region XPolygonRegion( +#if NeedFunctionPrototypes + XPoint* /* points */, + int /* n */, + int /* fill_rule */ +#endif +); + +extern int XRectInRegion( +#if NeedFunctionPrototypes + Region /* r */, + int /* x */, + int /* y */, + unsigned int /* width */, + unsigned int /* height */ +#endif +); + +extern int XShrinkRegion( +#if NeedFunctionPrototypes + Region /* r */, + int /* dx */, + int /* dy */ +#endif +); + +extern int XSubtractRegion( +#if NeedFunctionPrototypes + Region /* sra */, + Region /* srb */, + Region /* dr_return */ +#endif +); + +extern int XUnionRectWithRegion( +#if NeedFunctionPrototypes + XRectangle* /* rectangle */, + Region /* src_region */, + Region /* dest_region_return */ +#endif +); + +extern int XUnionRegion( +#if NeedFunctionPrototypes + Region /* sra */, + Region /* srb */, + Region /* dr_return */ +#endif +); + +extern int XXorRegion( +#if NeedFunctionPrototypes + Region /* sra */, + Region /* srb */, + Region /* dr_return */ +#endif +); + +#ifdef __cplusplus +}; +#endif + +#endif /* _XUTIL_H_ */ diff --git a/src/vnc/common/Xregion/region.h b/src/vnc/common/Xregion/region.h new file mode 100644 index 00000000..2ddf12ca --- /dev/null +++ b/src/vnc/common/Xregion/region.h @@ -0,0 +1,190 @@ +/* $Xorg: region.h,v 1.4 2001/02/09 02:03:40 xorgcvs Exp $ */ +/************************************************************************ + +Copyright 1987, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. + + +Copyright 1987 by Digital Equipment Corporation, Maynard, Massachusetts. + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Digital not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL +DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + +************************************************************************/ + +#ifndef _XREGION_H +#define _XREGION_H + +typedef struct { + short x1, x2, y1, y2; +} Box, BOX, BoxRec, *BoxPtr; + +typedef struct { + short x, y, width, height; +}RECTANGLE, RectangleRec, *RectanglePtr; + +#define TRUE 1 +#define FALSE 0 +#define MAXSHORT 32767 +#define MINSHORT -MAXSHORT +#ifndef MAX +#define MAX(a,b) (((a) > (b)) ? (a) : (b)) +#endif +#ifndef MIN +#define MIN(a,b) (((a) < (b)) ? (a) : (b)) +#endif + + +/* + * clip region + */ + +typedef struct _XRegion { + long size; + long numRects; + BOX *rects; + BOX extents; +} REGION; + +/* Xutil.h contains the declaration: + * typedef struct _XRegion *Region; + */ + +/* 1 if two BOXs overlap. + * 0 if two BOXs do not overlap. + * Remember, x2 and y2 are not in the region + */ +#define EXTENTCHECK(r1, r2) \ + ((r1)->x2 > (r2)->x1 && \ + (r1)->x1 < (r2)->x2 && \ + (r1)->y2 > (r2)->y1 && \ + (r1)->y1 < (r2)->y2) + +/* + * update region extents + */ +#define EXTENTS(r,idRect){\ + if((r)->x1 < (idRect)->extents.x1)\ + (idRect)->extents.x1 = (r)->x1;\ + if((r)->y1 < (idRect)->extents.y1)\ + (idRect)->extents.y1 = (r)->y1;\ + if((r)->x2 > (idRect)->extents.x2)\ + (idRect)->extents.x2 = (r)->x2;\ + if((r)->y2 > (idRect)->extents.y2)\ + (idRect)->extents.y2 = (r)->y2;\ + } + +/* + * Check to see if there is enough memory in the present region. + */ +#define MEMCHECK(reg, rect, firstrect){\ + if ((reg)->numRects >= ((reg)->size - 1)){\ + (firstrect) = (BOX *) Xrealloc \ + ((char *)(firstrect), (unsigned) (2 * (sizeof(BOX)) * ((reg)->size)));\ + if ((firstrect) == 0)\ + return(0);\ + (reg)->size *= 2;\ + (rect) = &(firstrect)[(reg)->numRects];\ + }\ + } + +/* this routine checks to see if the previous rectangle is the same + * or subsumes the new rectangle to add. + */ + +#define CHECK_PREVIOUS(Reg, R, Rx1, Ry1, Rx2, Ry2)\ + (!(((Reg)->numRects > 0)&&\ + ((R-1)->y1 == (Ry1)) &&\ + ((R-1)->y2 == (Ry2)) &&\ + ((R-1)->x1 <= (Rx1)) &&\ + ((R-1)->x2 >= (Rx2)))) + +/* add a rectangle to the given Region */ +#define ADDRECT(reg, r, rx1, ry1, rx2, ry2){\ + if (((rx1) < (rx2)) && ((ry1) < (ry2)) &&\ + CHECK_PREVIOUS((reg), (r), (rx1), (ry1), (rx2), (ry2))){\ + (r)->x1 = (rx1);\ + (r)->y1 = (ry1);\ + (r)->x2 = (rx2);\ + (r)->y2 = (ry2);\ + EXTENTS((r), (reg));\ + (reg)->numRects++;\ + (r)++;\ + }\ + } + + + +/* add a rectangle to the given Region */ +#define ADDRECTNOX(reg, r, rx1, ry1, rx2, ry2){\ + if ((rx1 < rx2) && (ry1 < ry2) &&\ + CHECK_PREVIOUS((reg), (r), (rx1), (ry1), (rx2), (ry2))){\ + (r)->x1 = (rx1);\ + (r)->y1 = (ry1);\ + (r)->x2 = (rx2);\ + (r)->y2 = (ry2);\ + (reg)->numRects++;\ + (r)++;\ + }\ + } + +#define EMPTY_REGION(pReg) pReg->numRects = 0 + +#define REGION_NOT_EMPTY(pReg) pReg->numRects + +#define INBOX(r, x, y) \ + ( ( ((r).x2 > x)) && \ + ( ((r).x1 <= x)) && \ + ( ((r).y2 > y)) && \ + ( ((r).y1 <= y)) ) + +/* + * number of points to buffer before sending them off + * to scanlines() : Must be an even number + */ +#define NUMPTSTOBUFFER 200 + +/* + * used to allocate buffers for points and link + * the buffers together + */ +typedef struct _POINTBLOCK { + XPoint pts[NUMPTSTOBUFFER]; + struct _POINTBLOCK *next; +} POINTBLOCK; + +#endif diff --git a/src/vnc/common/boilerplate.mk b/src/vnc/common/boilerplate.mk new file mode 100644 index 00000000..979731c5 --- /dev/null +++ b/src/vnc/common/boilerplate.mk @@ -0,0 +1,35 @@ + +all:: + @subdirs="$(SUBDIRS)"; for d in $$subdirs; do (cd $$d; $(MAKE) $@) || exit 1; done + +clean:: + @subdirs="$(SUBDIRS)"; for d in $$subdirs; do (cd $$d; $(MAKE) $@) || exit 1; done + +clean:: + rm -f $(program) $(library) *.o + +SHELL = @SHELL@ +top_srcdir = @top_srcdir@ +@SET_MAKE@ +CC = @CC@ +CFLAGS = @CFLAGS@ $(DIR_CFLAGS) +CCLD = $(CC) +CXX = @CXX@ +CXXFLAGS = @CXXFLAGS@ +CXXLD = $(CXX) +CPPFLAGS = @CPPFLAGS@ +DEFS = @DEFS@ +ALL_CPPFLAGS = $(CPPFLAGS) $(DEFS) $(DIR_CPPFLAGS) +LIBS = @LIBS@ +LDFLAGS = @LDFLAGS@ +RANLIB = @RANLIB@ +AR = ar cq + +.SUFFIXES: +.SUFFIXES: .cxx .c .o + +.c.o: + $(CC) $(ALL_CPPFLAGS) $(CFLAGS) -c $< + +.cxx.o: + $(CXX) $(ALL_CPPFLAGS) $(CXXFLAGS) -c $< diff --git a/src/vnc/common/configure b/src/vnc/common/configure new file mode 100644 index 00000000..c7be2edd --- /dev/null +++ b/src/vnc/common/configure @@ -0,0 +1,2344 @@ +#! /bin/sh + +# Guess values for system-dependent variables and create Makefiles. +# Generated automatically using autoconf version 2.13 +# Copyright (C) 1992, 93, 94, 95, 96 Free Software Foundation, Inc. +# +# This configure script is free software; the Free Software Foundation +# gives unlimited permission to copy, distribute and modify it. + +# Defaults: +ac_help= +ac_default_prefix=/usr/local +# Any additions from configure.in: +ac_help="$ac_help + --with-x use the X Window System" +ac_help="$ac_help + --with-installed-zlib use the version of zlib which is installed on the + system instead of the one distributed with VNC" + +# Initialize some variables set by options. +# The variables have the same names as the options, with +# dashes changed to underlines. +build=NONE +cache_file=./config.cache +exec_prefix=NONE +host=NONE +no_create= +nonopt=NONE +no_recursion= +prefix=NONE +program_prefix=NONE +program_suffix=NONE +program_transform_name=s,x,x, +silent= +site= +srcdir= +target=NONE +verbose= +x_includes=NONE +x_libraries=NONE +bindir='${exec_prefix}/bin' +sbindir='${exec_prefix}/sbin' +libexecdir='${exec_prefix}/libexec' +datadir='${prefix}/share' +sysconfdir='${prefix}/etc' +sharedstatedir='${prefix}/com' +localstatedir='${prefix}/var' +libdir='${exec_prefix}/lib' +includedir='${prefix}/include' +oldincludedir='/usr/include' +infodir='${prefix}/info' +mandir='${prefix}/man' + +# Initialize some other variables. +subdirs= +MFLAGS= MAKEFLAGS= +SHELL=${CONFIG_SHELL-/bin/sh} +# Maximum number of lines to put in a shell here document. +ac_max_here_lines=12 + +ac_prev= +for ac_option +do + + # If the previous option needs an argument, assign it. + if test -n "$ac_prev"; then + eval "$ac_prev=\$ac_option" + ac_prev= + continue + fi + + case "$ac_option" in + -*=*) ac_optarg=`echo "$ac_option" | sed 's/[-_a-zA-Z0-9]*=//'` ;; + *) ac_optarg= ;; + esac + + # Accept the important Cygnus configure options, so we can diagnose typos. + + case "$ac_option" in + + -bindir | --bindir | --bindi | --bind | --bin | --bi) + ac_prev=bindir ;; + -bindir=* | --bindir=* | --bindi=* | --bind=* | --bin=* | --bi=*) + bindir="$ac_optarg" ;; + + -build | --build | --buil | --bui | --bu) + ac_prev=build ;; + -build=* | --build=* | --buil=* | --bui=* | --bu=*) + build="$ac_optarg" ;; + + -cache-file | --cache-file | --cache-fil | --cache-fi \ + | --cache-f | --cache- | --cache | --cach | --cac | --ca | --c) + ac_prev=cache_file ;; + -cache-file=* | --cache-file=* | --cache-fil=* | --cache-fi=* \ + | --cache-f=* | --cache-=* | --cache=* | --cach=* | --cac=* | --ca=* | --c=*) + cache_file="$ac_optarg" ;; + + -datadir | --datadir | --datadi | --datad | --data | --dat | --da) + ac_prev=datadir ;; + -datadir=* | --datadir=* | --datadi=* | --datad=* | --data=* | --dat=* \ + | --da=*) + datadir="$ac_optarg" ;; + + -disable-* | --disable-*) + ac_feature=`echo $ac_option|sed -e 's/-*disable-//'` + # Reject names that are not valid shell variable names. + if test -n "`echo $ac_feature| sed 's/[-a-zA-Z0-9_]//g'`"; then + { echo "configure: error: $ac_feature: invalid feature name" 1>&2; exit 1; } + fi + ac_feature=`echo $ac_feature| sed 's/-/_/g'` + eval "enable_${ac_feature}=no" ;; + + -enable-* | --enable-*) + ac_feature=`echo $ac_option|sed -e 's/-*enable-//' -e 's/=.*//'` + # Reject names that are not valid shell variable names. + if test -n "`echo $ac_feature| sed 's/[-_a-zA-Z0-9]//g'`"; then + { echo "configure: error: $ac_feature: invalid feature name" 1>&2; exit 1; } + fi + ac_feature=`echo $ac_feature| sed 's/-/_/g'` + case "$ac_option" in + *=*) ;; + *) ac_optarg=yes ;; + esac + eval "enable_${ac_feature}='$ac_optarg'" ;; + + -exec-prefix | --exec_prefix | --exec-prefix | --exec-prefi \ + | --exec-pref | --exec-pre | --exec-pr | --exec-p | --exec- \ + | --exec | --exe | --ex) + ac_prev=exec_prefix ;; + -exec-prefix=* | --exec_prefix=* | --exec-prefix=* | --exec-prefi=* \ + | --exec-pref=* | --exec-pre=* | --exec-pr=* | --exec-p=* | --exec-=* \ + | --exec=* | --exe=* | --ex=*) + exec_prefix="$ac_optarg" ;; + + -gas | --gas | --ga | --g) + # Obsolete; use --with-gas. + with_gas=yes ;; + + -help | --help | --hel | --he) + # Omit some internal or obsolete options to make the list less imposing. + # This message is too long to be a string in the A/UX 3.1 sh. + cat << EOF +Usage: configure [options] [host] +Options: [defaults in brackets after descriptions] +Configuration: + --cache-file=FILE cache test results in FILE + --help print this message + --no-create do not create output files + --quiet, --silent do not print \`checking...' messages + --version print the version of autoconf that created configure +Directory and file names: + --prefix=PREFIX install architecture-independent files in PREFIX + [$ac_default_prefix] + --exec-prefix=EPREFIX install architecture-dependent files in EPREFIX + [same as prefix] + --bindir=DIR user executables in DIR [EPREFIX/bin] + --sbindir=DIR system admin executables in DIR [EPREFIX/sbin] + --libexecdir=DIR program executables in DIR [EPREFIX/libexec] + --datadir=DIR read-only architecture-independent data in DIR + [PREFIX/share] + --sysconfdir=DIR read-only single-machine data in DIR [PREFIX/etc] + --sharedstatedir=DIR modifiable architecture-independent data in DIR + [PREFIX/com] + --localstatedir=DIR modifiable single-machine data in DIR [PREFIX/var] + --libdir=DIR object code libraries in DIR [EPREFIX/lib] + --includedir=DIR C header files in DIR [PREFIX/include] + --oldincludedir=DIR C header files for non-gcc in DIR [/usr/include] + --infodir=DIR info documentation in DIR [PREFIX/info] + --mandir=DIR man documentation in DIR [PREFIX/man] + --srcdir=DIR find the sources in DIR [configure dir or ..] + --program-prefix=PREFIX prepend PREFIX to installed program names + --program-suffix=SUFFIX append SUFFIX to installed program names + --program-transform-name=PROGRAM + run sed PROGRAM on installed program names +EOF + cat << EOF +Host type: + --build=BUILD configure for building on BUILD [BUILD=HOST] + --host=HOST configure for HOST [guessed] + --target=TARGET configure for TARGET [TARGET=HOST] +Features and packages: + --disable-FEATURE do not include FEATURE (same as --enable-FEATURE=no) + --enable-FEATURE[=ARG] include FEATURE [ARG=yes] + --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] + --without-PACKAGE do not use PACKAGE (same as --with-PACKAGE=no) + --x-includes=DIR X include files are in DIR + --x-libraries=DIR X library files are in DIR +EOF + if test -n "$ac_help"; then + echo "--enable and --with options recognized:$ac_help" + fi + exit 0 ;; + + -host | --host | --hos | --ho) + ac_prev=host ;; + -host=* | --host=* | --hos=* | --ho=*) + host="$ac_optarg" ;; + + -includedir | --includedir | --includedi | --included | --include \ + | --includ | --inclu | --incl | --inc) + ac_prev=includedir ;; + -includedir=* | --includedir=* | --includedi=* | --included=* | --include=* \ + | --includ=* | --inclu=* | --incl=* | --inc=*) + includedir="$ac_optarg" ;; + + -infodir | --infodir | --infodi | --infod | --info | --inf) + ac_prev=infodir ;; + -infodir=* | --infodir=* | --infodi=* | --infod=* | --info=* | --inf=*) + infodir="$ac_optarg" ;; + + -libdir | --libdir | --libdi | --libd) + ac_prev=libdir ;; + -libdir=* | --libdir=* | --libdi=* | --libd=*) + libdir="$ac_optarg" ;; + + -libexecdir | --libexecdir | --libexecdi | --libexecd | --libexec \ + | --libexe | --libex | --libe) + ac_prev=libexecdir ;; + -libexecdir=* | --libexecdir=* | --libexecdi=* | --libexecd=* | --libexec=* \ + | --libexe=* | --libex=* | --libe=*) + libexecdir="$ac_optarg" ;; + + -localstatedir | --localstatedir | --localstatedi | --localstated \ + | --localstate | --localstat | --localsta | --localst \ + | --locals | --local | --loca | --loc | --lo) + ac_prev=localstatedir ;; + -localstatedir=* | --localstatedir=* | --localstatedi=* | --localstated=* \ + | --localstate=* | --localstat=* | --localsta=* | --localst=* \ + | --locals=* | --local=* | --loca=* | --loc=* | --lo=*) + localstatedir="$ac_optarg" ;; + + -mandir | --mandir | --mandi | --mand | --man | --ma | --m) + ac_prev=mandir ;; + -mandir=* | --mandir=* | --mandi=* | --mand=* | --man=* | --ma=* | --m=*) + mandir="$ac_optarg" ;; + + -nfp | --nfp | --nf) + # Obsolete; use --without-fp. + with_fp=no ;; + + -no-create | --no-create | --no-creat | --no-crea | --no-cre \ + | --no-cr | --no-c) + no_create=yes ;; + + -no-recursion | --no-recursion | --no-recursio | --no-recursi \ + | --no-recurs | --no-recur | --no-recu | --no-rec | --no-re | --no-r) + no_recursion=yes ;; + + -oldincludedir | --oldincludedir | --oldincludedi | --oldincluded \ + | --oldinclude | --oldinclud | --oldinclu | --oldincl | --oldinc \ + | --oldin | --oldi | --old | --ol | --o) + ac_prev=oldincludedir ;; + -oldincludedir=* | --oldincludedir=* | --oldincludedi=* | --oldincluded=* \ + | --oldinclude=* | --oldinclud=* | --oldinclu=* | --oldincl=* | --oldinc=* \ + | --oldin=* | --oldi=* | --old=* | --ol=* | --o=*) + oldincludedir="$ac_optarg" ;; + + -prefix | --prefix | --prefi | --pref | --pre | --pr | --p) + ac_prev=prefix ;; + -prefix=* | --prefix=* | --prefi=* | --pref=* | --pre=* | --pr=* | --p=*) + prefix="$ac_optarg" ;; + + -program-prefix | --program-prefix | --program-prefi | --program-pref \ + | --program-pre | --program-pr | --program-p) + ac_prev=program_prefix ;; + -program-prefix=* | --program-prefix=* | --program-prefi=* \ + | --program-pref=* | --program-pre=* | --program-pr=* | --program-p=*) + program_prefix="$ac_optarg" ;; + + -program-suffix | --program-suffix | --program-suffi | --program-suff \ + | --program-suf | --program-su | --program-s) + ac_prev=program_suffix ;; + -program-suffix=* | --program-suffix=* | --program-suffi=* \ + | --program-suff=* | --program-suf=* | --program-su=* | --program-s=*) + program_suffix="$ac_optarg" ;; + + -program-transform-name | --program-transform-name \ + | --program-transform-nam | --program-transform-na \ + | --program-transform-n | --program-transform- \ + | --program-transform | --program-transfor \ + | --program-transfo | --program-transf \ + | --program-trans | --program-tran \ + | --progr-tra | --program-tr | --program-t) + ac_prev=program_transform_name ;; + -program-transform-name=* | --program-transform-name=* \ + | --program-transform-nam=* | --program-transform-na=* \ + | --program-transform-n=* | --program-transform-=* \ + | --program-transform=* | --program-transfor=* \ + | --program-transfo=* | --program-transf=* \ + | --program-trans=* | --program-tran=* \ + | --progr-tra=* | --program-tr=* | --program-t=*) + program_transform_name="$ac_optarg" ;; + + -q | -quiet | --quiet | --quie | --qui | --qu | --q \ + | -silent | --silent | --silen | --sile | --sil) + silent=yes ;; + + -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb) + ac_prev=sbindir ;; + -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \ + | --sbi=* | --sb=*) + sbindir="$ac_optarg" ;; + + -sharedstatedir | --sharedstatedir | --sharedstatedi \ + | --sharedstated | --sharedstate | --sharedstat | --sharedsta \ + | --sharedst | --shareds | --shared | --share | --shar \ + | --sha | --sh) + ac_prev=sharedstatedir ;; + -sharedstatedir=* | --sharedstatedir=* | --sharedstatedi=* \ + | --sharedstated=* | --sharedstate=* | --sharedstat=* | --sharedsta=* \ + | --sharedst=* | --shareds=* | --shared=* | --share=* | --shar=* \ + | --sha=* | --sh=*) + sharedstatedir="$ac_optarg" ;; + + -site | --site | --sit) + ac_prev=site ;; + -site=* | --site=* | --sit=*) + site="$ac_optarg" ;; + + -srcdir | --srcdir | --srcdi | --srcd | --src | --sr) + ac_prev=srcdir ;; + -srcdir=* | --srcdir=* | --srcdi=* | --srcd=* | --src=* | --sr=*) + srcdir="$ac_optarg" ;; + + -sysconfdir | --sysconfdir | --sysconfdi | --sysconfd | --sysconf \ + | --syscon | --sysco | --sysc | --sys | --sy) + ac_prev=sysconfdir ;; + -sysconfdir=* | --sysconfdir=* | --sysconfdi=* | --sysconfd=* | --sysconf=* \ + | --syscon=* | --sysco=* | --sysc=* | --sys=* | --sy=*) + sysconfdir="$ac_optarg" ;; + + -target | --target | --targe | --targ | --tar | --ta | --t) + ac_prev=target ;; + -target=* | --target=* | --targe=* | --targ=* | --tar=* | --ta=* | --t=*) + target="$ac_optarg" ;; + + -v | -verbose | --verbose | --verbos | --verbo | --verb) + verbose=yes ;; + + -version | --version | --versio | --versi | --vers) + echo "configure generated by autoconf version 2.13" + exit 0 ;; + + -with-* | --with-*) + ac_package=`echo $ac_option|sed -e 's/-*with-//' -e 's/=.*//'` + # Reject names that are not valid shell variable names. + if test -n "`echo $ac_package| sed 's/[-_a-zA-Z0-9]//g'`"; then + { echo "configure: error: $ac_package: invalid package name" 1>&2; exit 1; } + fi + ac_package=`echo $ac_package| sed 's/-/_/g'` + case "$ac_option" in + *=*) ;; + *) ac_optarg=yes ;; + esac + eval "with_${ac_package}='$ac_optarg'" ;; + + -without-* | --without-*) + ac_package=`echo $ac_option|sed -e 's/-*without-//'` + # Reject names that are not valid shell variable names. + if test -n "`echo $ac_package| sed 's/[-a-zA-Z0-9_]//g'`"; then + { echo "configure: error: $ac_package: invalid package name" 1>&2; exit 1; } + fi + ac_package=`echo $ac_package| sed 's/-/_/g'` + eval "with_${ac_package}=no" ;; + + --x) + # Obsolete; use --with-x. + with_x=yes ;; + + -x-includes | --x-includes | --x-include | --x-includ | --x-inclu \ + | --x-incl | --x-inc | --x-in | --x-i) + ac_prev=x_includes ;; + -x-includes=* | --x-includes=* | --x-include=* | --x-includ=* | --x-inclu=* \ + | --x-incl=* | --x-inc=* | --x-in=* | --x-i=*) + x_includes="$ac_optarg" ;; + + -x-libraries | --x-libraries | --x-librarie | --x-librari \ + | --x-librar | --x-libra | --x-libr | --x-lib | --x-li | --x-l) + ac_prev=x_libraries ;; + -x-libraries=* | --x-libraries=* | --x-librarie=* | --x-librari=* \ + | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*) + x_libraries="$ac_optarg" ;; + + -*) { echo "configure: error: $ac_option: invalid option; use --help to show usage" 1>&2; exit 1; } + ;; + + *) + if test -n "`echo $ac_option| sed 's/[-a-z0-9.]//g'`"; then + echo "configure: warning: $ac_option: invalid host type" 1>&2 + fi + if test "x$nonopt" != xNONE; then + { echo "configure: error: can only configure for one host and one target at a time" 1>&2; exit 1; } + fi + nonopt="$ac_option" + ;; + + esac +done + +if test -n "$ac_prev"; then + { echo "configure: error: missing argument to --`echo $ac_prev | sed 's/_/-/g'`" 1>&2; exit 1; } +fi + +trap 'rm -fr conftest* confdefs* core core.* *.core $ac_clean_files; exit 1' 1 2 15 + +# File descriptor usage: +# 0 standard input +# 1 file creation +# 2 errors and warnings +# 3 some systems may open it to /dev/tty +# 4 used on the Kubota Titan +# 6 checking for... messages and results +# 5 compiler messages saved in config.log +if test "$silent" = yes; then + exec 6>/dev/null +else + exec 6>&1 +fi +exec 5>./config.log + +echo "\ +This file contains any messages produced by compilers while +running configure, to aid debugging if configure makes a mistake. +" 1>&5 + +# Strip out --no-create and --no-recursion so they do not pile up. +# Also quote any args containing shell metacharacters. +ac_configure_args= +for ac_arg +do + case "$ac_arg" in + -no-create | --no-create | --no-creat | --no-crea | --no-cre \ + | --no-cr | --no-c) ;; + -no-recursion | --no-recursion | --no-recursio | --no-recursi \ + | --no-recurs | --no-recur | --no-recu | --no-rec | --no-re | --no-r) ;; + *" "*|*" "*|*[\[\]\~\#\$\^\&\*\(\)\{\}\\\|\;\<\>\?]*) + ac_configure_args="$ac_configure_args '$ac_arg'" ;; + *) ac_configure_args="$ac_configure_args $ac_arg" ;; + esac +done + +# NLS nuisances. +# Only set these to C if already set. These must not be set unconditionally +# because not all systems understand e.g. LANG=C (notably SCO). +# Fixing LC_MESSAGES prevents Solaris sh from translating var values in `set'! +# Non-C LC_CTYPE values break the ctype check. +if test "${LANG+set}" = set; then LANG=C; export LANG; fi +if test "${LC_ALL+set}" = set; then LC_ALL=C; export LC_ALL; fi +if test "${LC_MESSAGES+set}" = set; then LC_MESSAGES=C; export LC_MESSAGES; fi +if test "${LC_CTYPE+set}" = set; then LC_CTYPE=C; export LC_CTYPE; fi + +# confdefs.h avoids OS command line length limits that DEFS can exceed. +rm -rf conftest* confdefs.h +# AIX cpp loses on an empty file, so make sure it contains at least a newline. +echo > confdefs.h + +# A filename unique to this package, relative to the directory that +# configure is in, which we can look for to find out if srcdir is correct. +ac_unique_file=rdr/InStream.h + +# Find the source files, if location was not specified. +if test -z "$srcdir"; then + ac_srcdir_defaulted=yes + # Try the directory containing this script, then its parent. + ac_prog=$0 + ac_confdir=`echo $ac_prog|sed 's%/[^/][^/]*$%%'` + test "x$ac_confdir" = "x$ac_prog" && ac_confdir=. + srcdir=$ac_confdir + if test ! -r $srcdir/$ac_unique_file; then + srcdir=.. + fi +else + ac_srcdir_defaulted=no +fi +if test ! -r $srcdir/$ac_unique_file; then + if test "$ac_srcdir_defaulted" = yes; then + { echo "configure: error: can not find sources in $ac_confdir or .." 1>&2; exit 1; } + else + { echo "configure: error: can not find sources in $srcdir" 1>&2; exit 1; } + fi +fi +srcdir=`echo "${srcdir}" | sed 's%\([^/]\)/*$%\1%'` + +# Prefer explicitly selected file to automatically selected ones. +if test -z "$CONFIG_SITE"; then + if test "x$prefix" != xNONE; then + CONFIG_SITE="$prefix/share/config.site $prefix/etc/config.site" + else + CONFIG_SITE="$ac_default_prefix/share/config.site $ac_default_prefix/etc/config.site" + fi +fi +for ac_site_file in $CONFIG_SITE; do + if test -r "$ac_site_file"; then + echo "loading site script $ac_site_file" + . "$ac_site_file" + fi +done + +if test -r "$cache_file"; then + echo "loading cache $cache_file" + . $cache_file +else + echo "creating cache $cache_file" + > $cache_file +fi + +ac_ext=c +# CFLAGS is not in ac_cpp because -g, -O, etc. are not valid cpp options. +ac_cpp='$CPP $CPPFLAGS' +ac_compile='${CC-cc} -c $CFLAGS $CPPFLAGS conftest.$ac_ext 1>&5' +ac_link='${CC-cc} -o conftest${ac_exeext} $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS 1>&5' +cross_compiling=$ac_cv_prog_cc_cross + +ac_exeext= +ac_objext=o +if (echo "testing\c"; echo 1,2,3) | grep c >/dev/null; then + # Stardent Vistra SVR4 grep lacks -e, says ghazi@caip.rutgers.edu. + if (echo -n testing; echo 1,2,3) | sed s/-n/xn/ | grep xn >/dev/null; then + ac_n= ac_c=' +' ac_t=' ' + else + ac_n=-n ac_c= ac_t= + fi +else + ac_n= ac_c='\c' ac_t= +fi + + + +ac_cv_prog_cc_g=no +ac_cv_prog_cxx_g=no + +# Extract the first word of "gcc", so it can be a program name with args. +set dummy gcc; ac_word=$2 +echo $ac_n "checking for $ac_word""... $ac_c" 1>&6 +echo "configure:537: checking for $ac_word" >&5 +if eval "test \"`echo '$''{'ac_cv_prog_CC'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. +else + IFS="${IFS= }"; ac_save_ifs="$IFS"; IFS=":" + ac_dummy="$PATH" + for ac_dir in $ac_dummy; do + test -z "$ac_dir" && ac_dir=. + if test -f $ac_dir/$ac_word; then + ac_cv_prog_CC="gcc" + break + fi + done + IFS="$ac_save_ifs" +fi +fi +CC="$ac_cv_prog_CC" +if test -n "$CC"; then + echo "$ac_t""$CC" 1>&6 +else + echo "$ac_t""no" 1>&6 +fi + +if test -z "$CC"; then + # Extract the first word of "cc", so it can be a program name with args. +set dummy cc; ac_word=$2 +echo $ac_n "checking for $ac_word""... $ac_c" 1>&6 +echo "configure:567: checking for $ac_word" >&5 +if eval "test \"`echo '$''{'ac_cv_prog_CC'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. +else + IFS="${IFS= }"; ac_save_ifs="$IFS"; IFS=":" + ac_prog_rejected=no + ac_dummy="$PATH" + for ac_dir in $ac_dummy; do + test -z "$ac_dir" && ac_dir=. + if test -f $ac_dir/$ac_word; then + if test "$ac_dir/$ac_word" = "/usr/ucb/cc"; then + ac_prog_rejected=yes + continue + fi + ac_cv_prog_CC="cc" + break + fi + done + IFS="$ac_save_ifs" +if test $ac_prog_rejected = yes; then + # We found a bogon in the path, so make sure we never use it. + set dummy $ac_cv_prog_CC + shift + if test $# -gt 0; then + # We chose a different compiler from the bogus one. + # However, it has the same basename, so the bogon will be chosen + # first if we set CC to just the basename; use the full file name. + shift + set dummy "$ac_dir/$ac_word" "$@" + shift + ac_cv_prog_CC="$@" + fi +fi +fi +fi +CC="$ac_cv_prog_CC" +if test -n "$CC"; then + echo "$ac_t""$CC" 1>&6 +else + echo "$ac_t""no" 1>&6 +fi + + if test -z "$CC"; then + case "`uname -s`" in + *win32* | *WIN32*) + # Extract the first word of "cl", so it can be a program name with args. +set dummy cl; ac_word=$2 +echo $ac_n "checking for $ac_word""... $ac_c" 1>&6 +echo "configure:618: checking for $ac_word" >&5 +if eval "test \"`echo '$''{'ac_cv_prog_CC'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. +else + IFS="${IFS= }"; ac_save_ifs="$IFS"; IFS=":" + ac_dummy="$PATH" + for ac_dir in $ac_dummy; do + test -z "$ac_dir" && ac_dir=. + if test -f $ac_dir/$ac_word; then + ac_cv_prog_CC="cl" + break + fi + done + IFS="$ac_save_ifs" +fi +fi +CC="$ac_cv_prog_CC" +if test -n "$CC"; then + echo "$ac_t""$CC" 1>&6 +else + echo "$ac_t""no" 1>&6 +fi + ;; + esac + fi + test -z "$CC" && { echo "configure: error: no acceptable cc found in \$PATH" 1>&2; exit 1; } +fi + +echo $ac_n "checking whether the C compiler ($CC $CFLAGS $LDFLAGS) works""... $ac_c" 1>&6 +echo "configure:650: checking whether the C compiler ($CC $CFLAGS $LDFLAGS) works" >&5 + +ac_ext=c +# CFLAGS is not in ac_cpp because -g, -O, etc. are not valid cpp options. +ac_cpp='$CPP $CPPFLAGS' +ac_compile='${CC-cc} -c $CFLAGS $CPPFLAGS conftest.$ac_ext 1>&5' +ac_link='${CC-cc} -o conftest${ac_exeext} $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS 1>&5' +cross_compiling=$ac_cv_prog_cc_cross + +cat > conftest.$ac_ext << EOF + +#line 661 "configure" +#include "confdefs.h" + +main(){return(0);} +EOF +if { (eval echo configure:666: \"$ac_link\") 1>&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + ac_cv_prog_cc_works=yes + # If we can't run a trivial program, we are probably using a cross compiler. + if (./conftest; exit) 2>/dev/null; then + ac_cv_prog_cc_cross=no + else + ac_cv_prog_cc_cross=yes + fi +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + ac_cv_prog_cc_works=no +fi +rm -fr conftest* +ac_ext=c +# CFLAGS is not in ac_cpp because -g, -O, etc. are not valid cpp options. +ac_cpp='$CPP $CPPFLAGS' +ac_compile='${CC-cc} -c $CFLAGS $CPPFLAGS conftest.$ac_ext 1>&5' +ac_link='${CC-cc} -o conftest${ac_exeext} $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS 1>&5' +cross_compiling=$ac_cv_prog_cc_cross + +echo "$ac_t""$ac_cv_prog_cc_works" 1>&6 +if test $ac_cv_prog_cc_works = no; then + { echo "configure: error: installation or configuration problem: C compiler cannot create executables." 1>&2; exit 1; } +fi +echo $ac_n "checking whether the C compiler ($CC $CFLAGS $LDFLAGS) is a cross-compiler""... $ac_c" 1>&6 +echo "configure:692: checking whether the C compiler ($CC $CFLAGS $LDFLAGS) is a cross-compiler" >&5 +echo "$ac_t""$ac_cv_prog_cc_cross" 1>&6 +cross_compiling=$ac_cv_prog_cc_cross + +echo $ac_n "checking whether we are using GNU C""... $ac_c" 1>&6 +echo "configure:697: checking whether we are using GNU C" >&5 +if eval "test \"`echo '$''{'ac_cv_prog_gcc'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + cat > conftest.c <&5; (eval $ac_try) 2>&5; }; } | egrep yes >/dev/null 2>&1; then + ac_cv_prog_gcc=yes +else + ac_cv_prog_gcc=no +fi +fi + +echo "$ac_t""$ac_cv_prog_gcc" 1>&6 + +if test $ac_cv_prog_gcc = yes; then + GCC=yes +else + GCC= +fi + +ac_test_CFLAGS="${CFLAGS+set}" +ac_save_CFLAGS="$CFLAGS" +CFLAGS= +echo $ac_n "checking whether ${CC-cc} accepts -g""... $ac_c" 1>&6 +echo "configure:725: checking whether ${CC-cc} accepts -g" >&5 +if eval "test \"`echo '$''{'ac_cv_prog_cc_g'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + echo 'void f(){}' > conftest.c +if test -z "`${CC-cc} -g -c conftest.c 2>&1`"; then + ac_cv_prog_cc_g=yes +else + ac_cv_prog_cc_g=no +fi +rm -f conftest* + +fi + +echo "$ac_t""$ac_cv_prog_cc_g" 1>&6 +if test "$ac_test_CFLAGS" = set; then + CFLAGS="$ac_save_CFLAGS" +elif test $ac_cv_prog_cc_g = yes; then + if test "$GCC" = yes; then + CFLAGS="-g -O2" + else + CFLAGS="-g" + fi +else + if test "$GCC" = yes; then + CFLAGS="-O2" + else + CFLAGS= + fi +fi + +for ac_prog in $CCC c++ g++ gcc CC cxx cc++ cl +do +# Extract the first word of "$ac_prog", so it can be a program name with args. +set dummy $ac_prog; ac_word=$2 +echo $ac_n "checking for $ac_word""... $ac_c" 1>&6 +echo "configure:761: checking for $ac_word" >&5 +if eval "test \"`echo '$''{'ac_cv_prog_CXX'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + if test -n "$CXX"; then + ac_cv_prog_CXX="$CXX" # Let the user override the test. +else + IFS="${IFS= }"; ac_save_ifs="$IFS"; IFS=":" + ac_dummy="$PATH" + for ac_dir in $ac_dummy; do + test -z "$ac_dir" && ac_dir=. + if test -f $ac_dir/$ac_word; then + ac_cv_prog_CXX="$ac_prog" + break + fi + done + IFS="$ac_save_ifs" +fi +fi +CXX="$ac_cv_prog_CXX" +if test -n "$CXX"; then + echo "$ac_t""$CXX" 1>&6 +else + echo "$ac_t""no" 1>&6 +fi + +test -n "$CXX" && break +done +test -n "$CXX" || CXX="gcc" + + +echo $ac_n "checking whether the C++ compiler ($CXX $CXXFLAGS $LDFLAGS) works""... $ac_c" 1>&6 +echo "configure:793: checking whether the C++ compiler ($CXX $CXXFLAGS $LDFLAGS) works" >&5 + +ac_ext=C +# CXXFLAGS is not in ac_cpp because -g, -O, etc. are not valid cpp options. +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='${CXX-g++} -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext 1>&5' +ac_link='${CXX-g++} -o conftest${ac_exeext} $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS 1>&5' +cross_compiling=$ac_cv_prog_cxx_cross + +cat > conftest.$ac_ext << EOF + +#line 804 "configure" +#include "confdefs.h" + +int main(){return(0);} +EOF +if { (eval echo configure:809: \"$ac_link\") 1>&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + ac_cv_prog_cxx_works=yes + # If we can't run a trivial program, we are probably using a cross compiler. + if (./conftest; exit) 2>/dev/null; then + ac_cv_prog_cxx_cross=no + else + ac_cv_prog_cxx_cross=yes + fi +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + ac_cv_prog_cxx_works=no +fi +rm -fr conftest* +ac_ext=c +# CFLAGS is not in ac_cpp because -g, -O, etc. are not valid cpp options. +ac_cpp='$CPP $CPPFLAGS' +ac_compile='${CC-cc} -c $CFLAGS $CPPFLAGS conftest.$ac_ext 1>&5' +ac_link='${CC-cc} -o conftest${ac_exeext} $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS 1>&5' +cross_compiling=$ac_cv_prog_cc_cross + +echo "$ac_t""$ac_cv_prog_cxx_works" 1>&6 +if test $ac_cv_prog_cxx_works = no; then + { echo "configure: error: installation or configuration problem: C++ compiler cannot create executables." 1>&2; exit 1; } +fi +echo $ac_n "checking whether the C++ compiler ($CXX $CXXFLAGS $LDFLAGS) is a cross-compiler""... $ac_c" 1>&6 +echo "configure:835: checking whether the C++ compiler ($CXX $CXXFLAGS $LDFLAGS) is a cross-compiler" >&5 +echo "$ac_t""$ac_cv_prog_cxx_cross" 1>&6 +cross_compiling=$ac_cv_prog_cxx_cross + +echo $ac_n "checking whether we are using GNU C++""... $ac_c" 1>&6 +echo "configure:840: checking whether we are using GNU C++" >&5 +if eval "test \"`echo '$''{'ac_cv_prog_gxx'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + cat > conftest.C <&5; (eval $ac_try) 2>&5; }; } | egrep yes >/dev/null 2>&1; then + ac_cv_prog_gxx=yes +else + ac_cv_prog_gxx=no +fi +fi + +echo "$ac_t""$ac_cv_prog_gxx" 1>&6 + +if test $ac_cv_prog_gxx = yes; then + GXX=yes +else + GXX= +fi + +ac_test_CXXFLAGS="${CXXFLAGS+set}" +ac_save_CXXFLAGS="$CXXFLAGS" +CXXFLAGS= +echo $ac_n "checking whether ${CXX-g++} accepts -g""... $ac_c" 1>&6 +echo "configure:868: checking whether ${CXX-g++} accepts -g" >&5 +if eval "test \"`echo '$''{'ac_cv_prog_cxx_g'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + echo 'void f(){}' > conftest.cc +if test -z "`${CXX-g++} -g -c conftest.cc 2>&1`"; then + ac_cv_prog_cxx_g=yes +else + ac_cv_prog_cxx_g=no +fi +rm -f conftest* + +fi + +echo "$ac_t""$ac_cv_prog_cxx_g" 1>&6 +if test "$ac_test_CXXFLAGS" = set; then + CXXFLAGS="$ac_save_CXXFLAGS" +elif test $ac_cv_prog_cxx_g = yes; then + if test "$GXX" = yes; then + CXXFLAGS="-g -O2" + else + CXXFLAGS="-g" + fi +else + if test "$GXX" = yes; then + CXXFLAGS="-O2" + else + CXXFLAGS= + fi +fi + +for ac_declaration in \ + ''\ + '#include ' \ + 'extern "C" void std::exit (int) throw (); using std::exit;' \ + 'extern "C" void std::exit (int); using std::exit;' \ + 'extern "C" void exit (int) throw ();' \ + 'extern "C" void exit (int);' \ + 'void exit (int);' +do + cat > conftest.$ac_ext < +$ac_declaration +int main() { +exit (42); +; return 0; } +EOF +if { (eval echo configure:917: \"$ac_compile\") 1>&5; (eval $ac_compile) 2>&5; }; then + : +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + continue +fi +rm -f conftest* + cat > conftest.$ac_ext <&5; (eval $ac_compile) 2>&5; }; then + rm -rf conftest* + break +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 +fi +rm -f conftest* +done +if test -n "$ac_declaration"; then + echo '#ifdef __cplusplus' >>confdefs.h + echo $ac_declaration >>confdefs.h + echo '#endif' >>confdefs.h +fi + + +# Extract the first word of "ranlib", so it can be a program name with args. +set dummy ranlib; ac_word=$2 +echo $ac_n "checking for $ac_word""... $ac_c" 1>&6 +echo "configure:953: checking for $ac_word" >&5 +if eval "test \"`echo '$''{'ac_cv_prog_RANLIB'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + if test -n "$RANLIB"; then + ac_cv_prog_RANLIB="$RANLIB" # Let the user override the test. +else + IFS="${IFS= }"; ac_save_ifs="$IFS"; IFS=":" + ac_dummy="$PATH" + for ac_dir in $ac_dummy; do + test -z "$ac_dir" && ac_dir=. + if test -f $ac_dir/$ac_word; then + ac_cv_prog_RANLIB="ranlib" + break + fi + done + IFS="$ac_save_ifs" + test -z "$ac_cv_prog_RANLIB" && ac_cv_prog_RANLIB=":" +fi +fi +RANLIB="$ac_cv_prog_RANLIB" +if test -n "$RANLIB"; then + echo "$ac_t""$RANLIB" 1>&6 +else + echo "$ac_t""no" 1>&6 +fi + +echo $ac_n "checking whether ${MAKE-make} sets \${MAKE}""... $ac_c" 1>&6 +echo "configure:981: checking whether ${MAKE-make} sets \${MAKE}" >&5 +set dummy ${MAKE-make}; ac_make=`echo "$2" | sed 'y%./+-%__p_%'` +if eval "test \"`echo '$''{'ac_cv_prog_make_${ac_make}_set'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + cat > conftestmake <<\EOF +all: + @echo 'ac_maketemp="${MAKE}"' +EOF +# GNU make sometimes prints "make[1]: Entering...", which would confuse us. +eval `${MAKE-make} -f conftestmake 2>/dev/null | grep temp=` +if test -n "$ac_maketemp"; then + eval ac_cv_prog_make_${ac_make}_set=yes +else + eval ac_cv_prog_make_${ac_make}_set=no +fi +rm -f conftestmake +fi +if eval "test \"`echo '$ac_cv_prog_make_'${ac_make}_set`\" = yes"; then + echo "$ac_t""yes" 1>&6 + SET_MAKE= +else + echo "$ac_t""no" 1>&6 + SET_MAKE="MAKE=${MAKE-make}" +fi + +ac_ext=C +# CXXFLAGS is not in ac_cpp because -g, -O, etc. are not valid cpp options. +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='${CXX-g++} -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext 1>&5' +ac_link='${CXX-g++} -o conftest${ac_exeext} $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS 1>&5' +cross_compiling=$ac_cv_prog_cxx_cross + + +case "`(uname -sr) 2>/dev/null`" in +"SunOS 5"*) + SOLARIS=yes + USE_MITSHM=yes + ;; +"Linux"*) + LINUX=yes + USE_MITSHM=yes + ;; +esac + +if test "$USE_MITSHM" = yes; then + MITSHM_CPPFLAGS="-DMITSHM" +fi + + +if test "$GCC" = yes; then + CFLAGS="$CFLAGS -Wall" + if test "$SOLARIS" = yes; then + CFLAGS="$CFLAGS -Wno-unknown-pragmas -Wno-implicit-int" + fi +fi +if test "$GXX" = yes; then + CXXFLAGS="$CXXFLAGS -Wall" + if test "$SOLARIS" = yes; then + CXXFLAGS="$CXXFLAGS -Wno-unknown-pragmas -Wno-implicit-int -fpermissive" + fi +fi + +echo $ac_n "checking how to run the C++ preprocessor""... $ac_c" 1>&6 +echo "configure:1045: checking how to run the C++ preprocessor" >&5 +if test -z "$CXXCPP"; then +if eval "test \"`echo '$''{'ac_cv_prog_CXXCPP'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + ac_ext=C +# CXXFLAGS is not in ac_cpp because -g, -O, etc. are not valid cpp options. +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='${CXX-g++} -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext 1>&5' +ac_link='${CXX-g++} -o conftest${ac_exeext} $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS 1>&5' +cross_compiling=$ac_cv_prog_cxx_cross + CXXCPP="${CXX-g++} -E" + cat > conftest.$ac_ext < +EOF +ac_try="$ac_cpp conftest.$ac_ext >/dev/null 2>conftest.out" +{ (eval echo configure:1063: \"$ac_try\") 1>&5; (eval $ac_try) 2>&5; } +ac_err=`grep -v '^ *+' conftest.out | grep -v "^conftest.${ac_ext}\$"` +if test -z "$ac_err"; then + : +else + echo "$ac_err" >&5 + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + CXXCPP=/lib/cpp +fi +rm -f conftest* + ac_cv_prog_CXXCPP="$CXXCPP" +ac_ext=C +# CXXFLAGS is not in ac_cpp because -g, -O, etc. are not valid cpp options. +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='${CXX-g++} -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext 1>&5' +ac_link='${CXX-g++} -o conftest${ac_exeext} $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS 1>&5' +cross_compiling=$ac_cv_prog_cxx_cross +fi +fi +CXXCPP="$ac_cv_prog_CXXCPP" +echo "$ac_t""$CXXCPP" 1>&6 + +# If we find X, set shell vars x_includes and x_libraries to the +# paths, otherwise set no_x=yes. +# Uses ac_ vars as temps to allow command line to override cache and checks. +# --without-x overrides everything else, but does not touch the cache. +echo $ac_n "checking for X""... $ac_c" 1>&6 +echo "configure:1092: checking for X" >&5 + +# Check whether --with-x or --without-x was given. +if test "${with_x+set}" = set; then + withval="$with_x" + : +fi + +# $have_x is `yes', `no', `disabled', or empty when we do not yet know. +if test "x$with_x" = xno; then + # The user explicitly disabled X. + have_x=disabled +else + if test "x$x_includes" != xNONE && test "x$x_libraries" != xNONE; then + # Both variables are already set. + have_x=yes + else +if eval "test \"`echo '$''{'ac_cv_have_x'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + # One or both of the vars are not set, and there is no cached value. +ac_x_includes=NO ac_x_libraries=NO +rm -fr conftestdir +if mkdir conftestdir; then + cd conftestdir + # Make sure to not put "make" in the Imakefile rules, since we grep it out. + cat > Imakefile <<'EOF' +acfindx: + @echo 'ac_im_incroot="${INCROOT}"; ac_im_usrlibdir="${USRLIBDIR}"; ac_im_libdir="${LIBDIR}"' +EOF + if (xmkmf) >/dev/null 2>/dev/null && test -f Makefile; then + # GNU make sometimes prints "make[1]: Entering...", which would confuse us. + eval `${MAKE-make} acfindx 2>/dev/null | grep -v make` + # Open Windows xmkmf reportedly sets LIBDIR instead of USRLIBDIR. + for ac_extension in a so sl; do + if test ! -f $ac_im_usrlibdir/libX11.$ac_extension && + test -f $ac_im_libdir/libX11.$ac_extension; then + ac_im_usrlibdir=$ac_im_libdir; break + fi + done + # Screen out bogus values from the imake configuration. They are + # bogus both because they are the default anyway, and because + # using them would break gcc on systems where it needs fixed includes. + case "$ac_im_incroot" in + /usr/include) ;; + *) test -f "$ac_im_incroot/X11/Xos.h" && ac_x_includes="$ac_im_incroot" ;; + esac + case "$ac_im_usrlibdir" in + /usr/lib | /lib) ;; + *) test -d "$ac_im_usrlibdir" && ac_x_libraries="$ac_im_usrlibdir" ;; + esac + fi + cd .. + rm -fr conftestdir +fi + +if test "$ac_x_includes" = NO; then + # Guess where to find include files, by looking for this one X11 .h file. + test -z "$x_direct_test_include" && x_direct_test_include=X11/Intrinsic.h + + # First, try using that file with no special directory specified. +cat > conftest.$ac_ext < +EOF +ac_try="$ac_cpp conftest.$ac_ext >/dev/null 2>conftest.out" +{ (eval echo configure:1159: \"$ac_try\") 1>&5; (eval $ac_try) 2>&5; } +ac_err=`grep -v '^ *+' conftest.out | grep -v "^conftest.${ac_ext}\$"` +if test -z "$ac_err"; then + rm -rf conftest* + # We can compile using X headers with no special include directory. +ac_x_includes= +else + echo "$ac_err" >&5 + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + # Look for the header file in a standard set of common directories. +# Check X11 before X11Rn because it is often a symlink to the current release. + for ac_dir in \ + /usr/X11/include \ + /usr/X11R6/include \ + /usr/X11R5/include \ + /usr/X11R4/include \ + \ + /usr/include/X11 \ + /usr/include/X11R6 \ + /usr/include/X11R5 \ + /usr/include/X11R4 \ + \ + /usr/local/X11/include \ + /usr/local/X11R6/include \ + /usr/local/X11R5/include \ + /usr/local/X11R4/include \ + \ + /usr/local/include/X11 \ + /usr/local/include/X11R6 \ + /usr/local/include/X11R5 \ + /usr/local/include/X11R4 \ + \ + /usr/X386/include \ + /usr/x386/include \ + /usr/XFree86/include/X11 \ + \ + /usr/include \ + /usr/local/include \ + /usr/unsupported/include \ + /usr/athena/include \ + /usr/local/x11r5/include \ + /usr/lpp/Xamples/include \ + \ + /usr/openwin/include \ + /usr/openwin/share/include \ + ; \ + do + if test -r "$ac_dir/$x_direct_test_include"; then + ac_x_includes=$ac_dir + break + fi + done +fi +rm -f conftest* +fi # $ac_x_includes = NO + +if test "$ac_x_libraries" = NO; then + # Check for the libraries. + + test -z "$x_direct_test_library" && x_direct_test_library=Xt + test -z "$x_direct_test_function" && x_direct_test_function=XtMalloc + + # See if we find them without any special options. + # Don't add to $LIBS permanently. + ac_save_LIBS="$LIBS" + LIBS="-l$x_direct_test_library $LIBS" +cat > conftest.$ac_ext <&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + LIBS="$ac_save_LIBS" +# We can link X programs with no special library path. +ac_x_libraries= +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + LIBS="$ac_save_LIBS" +# First see if replacing the include by lib works. +# Check X11 before X11Rn because it is often a symlink to the current release. +for ac_dir in `echo "$ac_x_includes" | sed s/include/lib/` \ + /usr/X11/lib \ + /usr/X11R6/lib \ + /usr/X11R5/lib \ + /usr/X11R4/lib \ + \ + /usr/lib/X11 \ + /usr/lib/X11R6 \ + /usr/lib/X11R5 \ + /usr/lib/X11R4 \ + \ + /usr/local/X11/lib \ + /usr/local/X11R6/lib \ + /usr/local/X11R5/lib \ + /usr/local/X11R4/lib \ + \ + /usr/local/lib/X11 \ + /usr/local/lib/X11R6 \ + /usr/local/lib/X11R5 \ + /usr/local/lib/X11R4 \ + \ + /usr/X386/lib \ + /usr/x386/lib \ + /usr/XFree86/lib/X11 \ + \ + /usr/lib \ + /usr/local/lib \ + /usr/unsupported/lib \ + /usr/athena/lib \ + /usr/local/x11r5/lib \ + /usr/lpp/Xamples/lib \ + /lib/usr/lib/X11 \ + \ + /usr/openwin/lib \ + /usr/openwin/share/lib \ + ; \ +do + for ac_extension in a so sl; do + if test -r $ac_dir/lib${x_direct_test_library}.$ac_extension; then + ac_x_libraries=$ac_dir + break 2 + fi + done +done +fi +rm -f conftest* +fi # $ac_x_libraries = NO + +if test "$ac_x_includes" = NO || test "$ac_x_libraries" = NO; then + # Didn't find X anywhere. Cache the known absence of X. + ac_cv_have_x="have_x=no" +else + # Record where we found X for the cache. + ac_cv_have_x="have_x=yes \ + ac_x_includes=$ac_x_includes ac_x_libraries=$ac_x_libraries" +fi +fi + fi + eval "$ac_cv_have_x" +fi # $with_x != no + +if test "$have_x" != yes; then + echo "$ac_t""$have_x" 1>&6 + no_x=yes +else + # If each of the values was on the command line, it overrides each guess. + test "x$x_includes" = xNONE && x_includes=$ac_x_includes + test "x$x_libraries" = xNONE && x_libraries=$ac_x_libraries + # Update the cache value to reflect the command line values. + ac_cv_have_x="have_x=yes \ + ac_x_includes=$x_includes ac_x_libraries=$x_libraries" + echo "$ac_t""libraries $x_libraries, headers $x_includes" 1>&6 +fi + +if test "$no_x" = yes; then + # Not all programs may use this symbol, but it does not hurt to define it. + cat >> confdefs.h <<\EOF +#define X_DISPLAY_MISSING 1 +EOF + + X_CFLAGS= X_PRE_LIBS= X_LIBS= X_EXTRA_LIBS= +else + if test -n "$x_includes"; then + X_CFLAGS="$X_CFLAGS -I$x_includes" + fi + + # It would also be nice to do this for all -L options, not just this one. + if test -n "$x_libraries"; then + X_LIBS="$X_LIBS -L$x_libraries" + # For Solaris; some versions of Sun CC require a space after -R and + # others require no space. Words are not sufficient . . . . + case "`(uname -sr) 2>/dev/null`" in + "SunOS 5"*) + echo $ac_n "checking whether -R must be followed by a space""... $ac_c" 1>&6 +echo "configure:1341: checking whether -R must be followed by a space" >&5 + ac_xsave_LIBS="$LIBS"; LIBS="$LIBS -R$x_libraries" + cat > conftest.$ac_ext <&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + ac_R_nospace=yes +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + ac_R_nospace=no +fi +rm -f conftest* + if test $ac_R_nospace = yes; then + echo "$ac_t""no" 1>&6 + X_LIBS="$X_LIBS -R$x_libraries" + else + LIBS="$ac_xsave_LIBS -R $x_libraries" + cat > conftest.$ac_ext <&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + ac_R_space=yes +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + ac_R_space=no +fi +rm -f conftest* + if test $ac_R_space = yes; then + echo "$ac_t""yes" 1>&6 + X_LIBS="$X_LIBS -R $x_libraries" + else + echo "$ac_t""neither works" 1>&6 + fi + fi + LIBS="$ac_xsave_LIBS" + esac + fi + + # Check for system-dependent libraries X programs must link with. + # Do this before checking for the system-independent R6 libraries + # (-lICE), since we may need -lsocket or whatever for X linking. + + if test "$ISC" = yes; then + X_EXTRA_LIBS="$X_EXTRA_LIBS -lnsl_s -linet" + else + # Martyn.Johnson@cl.cam.ac.uk says this is needed for Ultrix, if the X + # libraries were built with DECnet support. And karl@cs.umb.edu says + # the Alpha needs dnet_stub (dnet does not exist). + echo $ac_n "checking for dnet_ntoa in -ldnet""... $ac_c" 1>&6 +echo "configure:1406: checking for dnet_ntoa in -ldnet" >&5 +ac_lib_var=`echo dnet'_'dnet_ntoa | sed 'y%./+-%__p_%'` +if eval "test \"`echo '$''{'ac_cv_lib_$ac_lib_var'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + ac_save_LIBS="$LIBS" +LIBS="-ldnet $LIBS" +cat > conftest.$ac_ext <&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=yes" +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=no" +fi +rm -f conftest* +LIBS="$ac_save_LIBS" + +fi +if eval "test \"`echo '$ac_cv_lib_'$ac_lib_var`\" = yes"; then + echo "$ac_t""yes" 1>&6 + X_EXTRA_LIBS="$X_EXTRA_LIBS -ldnet" +else + echo "$ac_t""no" 1>&6 +fi + + if test $ac_cv_lib_dnet_dnet_ntoa = no; then + echo $ac_n "checking for dnet_ntoa in -ldnet_stub""... $ac_c" 1>&6 +echo "configure:1450: checking for dnet_ntoa in -ldnet_stub" >&5 +ac_lib_var=`echo dnet_stub'_'dnet_ntoa | sed 'y%./+-%__p_%'` +if eval "test \"`echo '$''{'ac_cv_lib_$ac_lib_var'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + ac_save_LIBS="$LIBS" +LIBS="-ldnet_stub $LIBS" +cat > conftest.$ac_ext <&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=yes" +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=no" +fi +rm -f conftest* +LIBS="$ac_save_LIBS" + +fi +if eval "test \"`echo '$ac_cv_lib_'$ac_lib_var`\" = yes"; then + echo "$ac_t""yes" 1>&6 + X_EXTRA_LIBS="$X_EXTRA_LIBS -ldnet_stub" +else + echo "$ac_t""no" 1>&6 +fi + + fi + + # msh@cis.ufl.edu says -lnsl (and -lsocket) are needed for his 386/AT, + # to get the SysV transport functions. + # chad@anasazi.com says the Pyramis MIS-ES running DC/OSx (SVR4) + # needs -lnsl. + # The nsl library prevents programs from opening the X display + # on Irix 5.2, according to dickey@clark.net. + echo $ac_n "checking for gethostbyname""... $ac_c" 1>&6 +echo "configure:1501: checking for gethostbyname" >&5 +if eval "test \"`echo '$''{'ac_cv_func_gethostbyname'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + cat > conftest.$ac_ext < +/* Override any gcc2 internal prototype to avoid an error. */ +#ifdef __cplusplus +extern "C" +#endif +/* We use char because int might match the return type of a gcc2 + builtin and then its argument prototype would still apply. */ +char gethostbyname(); + +int main() { + +/* The GNU C library defines this for functions which it implements + to always fail with ENOSYS. Some functions are actually named + something starting with __ and the normal name is an alias. */ +#if defined (__stub_gethostbyname) || defined (__stub___gethostbyname) +choke me +#else +gethostbyname(); +#endif + +; return 0; } +EOF +if { (eval echo configure:1532: \"$ac_link\") 1>&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + eval "ac_cv_func_gethostbyname=yes" +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + eval "ac_cv_func_gethostbyname=no" +fi +rm -f conftest* +fi + +if eval "test \"`echo '$ac_cv_func_'gethostbyname`\" = yes"; then + echo "$ac_t""yes" 1>&6 + : +else + echo "$ac_t""no" 1>&6 +fi + + if test $ac_cv_func_gethostbyname = no; then + echo $ac_n "checking for gethostbyname in -lnsl""... $ac_c" 1>&6 +echo "configure:1553: checking for gethostbyname in -lnsl" >&5 +ac_lib_var=`echo nsl'_'gethostbyname | sed 'y%./+-%__p_%'` +if eval "test \"`echo '$''{'ac_cv_lib_$ac_lib_var'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + ac_save_LIBS="$LIBS" +LIBS="-lnsl $LIBS" +cat > conftest.$ac_ext <&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=yes" +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=no" +fi +rm -f conftest* +LIBS="$ac_save_LIBS" + +fi +if eval "test \"`echo '$ac_cv_lib_'$ac_lib_var`\" = yes"; then + echo "$ac_t""yes" 1>&6 + X_EXTRA_LIBS="$X_EXTRA_LIBS -lnsl" +else + echo "$ac_t""no" 1>&6 +fi + + fi + + # lieder@skyler.mavd.honeywell.com says without -lsocket, + # socket/setsockopt and other routines are undefined under SCO ODT + # 2.0. But -lsocket is broken on IRIX 5.2 (and is not necessary + # on later versions), says simon@lia.di.epfl.ch: it contains + # gethostby* variants that don't use the nameserver (or something). + # -lsocket must be given before -lnsl if both are needed. + # We assume that if connect needs -lnsl, so does gethostbyname. + echo $ac_n "checking for connect""... $ac_c" 1>&6 +echo "configure:1605: checking for connect" >&5 +if eval "test \"`echo '$''{'ac_cv_func_connect'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + cat > conftest.$ac_ext < +/* Override any gcc2 internal prototype to avoid an error. */ +#ifdef __cplusplus +extern "C" +#endif +/* We use char because int might match the return type of a gcc2 + builtin and then its argument prototype would still apply. */ +char connect(); + +int main() { + +/* The GNU C library defines this for functions which it implements + to always fail with ENOSYS. Some functions are actually named + something starting with __ and the normal name is an alias. */ +#if defined (__stub_connect) || defined (__stub___connect) +choke me +#else +connect(); +#endif + +; return 0; } +EOF +if { (eval echo configure:1636: \"$ac_link\") 1>&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + eval "ac_cv_func_connect=yes" +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + eval "ac_cv_func_connect=no" +fi +rm -f conftest* +fi + +if eval "test \"`echo '$ac_cv_func_'connect`\" = yes"; then + echo "$ac_t""yes" 1>&6 + : +else + echo "$ac_t""no" 1>&6 +fi + + if test $ac_cv_func_connect = no; then + echo $ac_n "checking for connect in -lsocket""... $ac_c" 1>&6 +echo "configure:1657: checking for connect in -lsocket" >&5 +ac_lib_var=`echo socket'_'connect | sed 'y%./+-%__p_%'` +if eval "test \"`echo '$''{'ac_cv_lib_$ac_lib_var'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + ac_save_LIBS="$LIBS" +LIBS="-lsocket $X_EXTRA_LIBS $LIBS" +cat > conftest.$ac_ext <&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=yes" +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=no" +fi +rm -f conftest* +LIBS="$ac_save_LIBS" + +fi +if eval "test \"`echo '$ac_cv_lib_'$ac_lib_var`\" = yes"; then + echo "$ac_t""yes" 1>&6 + X_EXTRA_LIBS="-lsocket $X_EXTRA_LIBS" +else + echo "$ac_t""no" 1>&6 +fi + + fi + + # gomez@mi.uni-erlangen.de says -lposix is necessary on A/UX. + echo $ac_n "checking for remove""... $ac_c" 1>&6 +echo "configure:1703: checking for remove" >&5 +if eval "test \"`echo '$''{'ac_cv_func_remove'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + cat > conftest.$ac_ext < +/* Override any gcc2 internal prototype to avoid an error. */ +#ifdef __cplusplus +extern "C" +#endif +/* We use char because int might match the return type of a gcc2 + builtin and then its argument prototype would still apply. */ +char remove(); + +int main() { + +/* The GNU C library defines this for functions which it implements + to always fail with ENOSYS. Some functions are actually named + something starting with __ and the normal name is an alias. */ +#if defined (__stub_remove) || defined (__stub___remove) +choke me +#else +remove(); +#endif + +; return 0; } +EOF +if { (eval echo configure:1734: \"$ac_link\") 1>&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + eval "ac_cv_func_remove=yes" +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + eval "ac_cv_func_remove=no" +fi +rm -f conftest* +fi + +if eval "test \"`echo '$ac_cv_func_'remove`\" = yes"; then + echo "$ac_t""yes" 1>&6 + : +else + echo "$ac_t""no" 1>&6 +fi + + if test $ac_cv_func_remove = no; then + echo $ac_n "checking for remove in -lposix""... $ac_c" 1>&6 +echo "configure:1755: checking for remove in -lposix" >&5 +ac_lib_var=`echo posix'_'remove | sed 'y%./+-%__p_%'` +if eval "test \"`echo '$''{'ac_cv_lib_$ac_lib_var'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + ac_save_LIBS="$LIBS" +LIBS="-lposix $LIBS" +cat > conftest.$ac_ext <&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=yes" +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=no" +fi +rm -f conftest* +LIBS="$ac_save_LIBS" + +fi +if eval "test \"`echo '$ac_cv_lib_'$ac_lib_var`\" = yes"; then + echo "$ac_t""yes" 1>&6 + X_EXTRA_LIBS="$X_EXTRA_LIBS -lposix" +else + echo "$ac_t""no" 1>&6 +fi + + fi + + # BSDI BSD/OS 2.1 needs -lipc for XOpenDisplay. + echo $ac_n "checking for shmat""... $ac_c" 1>&6 +echo "configure:1801: checking for shmat" >&5 +if eval "test \"`echo '$''{'ac_cv_func_shmat'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + cat > conftest.$ac_ext < +/* Override any gcc2 internal prototype to avoid an error. */ +#ifdef __cplusplus +extern "C" +#endif +/* We use char because int might match the return type of a gcc2 + builtin and then its argument prototype would still apply. */ +char shmat(); + +int main() { + +/* The GNU C library defines this for functions which it implements + to always fail with ENOSYS. Some functions are actually named + something starting with __ and the normal name is an alias. */ +#if defined (__stub_shmat) || defined (__stub___shmat) +choke me +#else +shmat(); +#endif + +; return 0; } +EOF +if { (eval echo configure:1832: \"$ac_link\") 1>&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + eval "ac_cv_func_shmat=yes" +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + eval "ac_cv_func_shmat=no" +fi +rm -f conftest* +fi + +if eval "test \"`echo '$ac_cv_func_'shmat`\" = yes"; then + echo "$ac_t""yes" 1>&6 + : +else + echo "$ac_t""no" 1>&6 +fi + + if test $ac_cv_func_shmat = no; then + echo $ac_n "checking for shmat in -lipc""... $ac_c" 1>&6 +echo "configure:1853: checking for shmat in -lipc" >&5 +ac_lib_var=`echo ipc'_'shmat | sed 'y%./+-%__p_%'` +if eval "test \"`echo '$''{'ac_cv_lib_$ac_lib_var'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + ac_save_LIBS="$LIBS" +LIBS="-lipc $LIBS" +cat > conftest.$ac_ext <&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=yes" +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=no" +fi +rm -f conftest* +LIBS="$ac_save_LIBS" + +fi +if eval "test \"`echo '$ac_cv_lib_'$ac_lib_var`\" = yes"; then + echo "$ac_t""yes" 1>&6 + X_EXTRA_LIBS="$X_EXTRA_LIBS -lipc" +else + echo "$ac_t""no" 1>&6 +fi + + fi + fi + + # Check for libraries that X11R6 Xt/Xaw programs need. + ac_save_LDFLAGS="$LDFLAGS" + test -n "$x_libraries" && LDFLAGS="$LDFLAGS -L$x_libraries" + # SM needs ICE to (dynamically) link under SunOS 4.x (so we have to + # check for ICE first), but we must link in the order -lSM -lICE or + # we get undefined symbols. So assume we have SM if we have ICE. + # These have to be linked with before -lX11, unlike the other + # libraries we check for below, so use a different variable. + # --interran@uluru.Stanford.EDU, kb@cs.umb.edu. + echo $ac_n "checking for IceConnectionNumber in -lICE""... $ac_c" 1>&6 +echo "configure:1908: checking for IceConnectionNumber in -lICE" >&5 +ac_lib_var=`echo ICE'_'IceConnectionNumber | sed 'y%./+-%__p_%'` +if eval "test \"`echo '$''{'ac_cv_lib_$ac_lib_var'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + ac_save_LIBS="$LIBS" +LIBS="-lICE $X_EXTRA_LIBS $LIBS" +cat > conftest.$ac_ext <&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=yes" +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + eval "ac_cv_lib_$ac_lib_var=no" +fi +rm -f conftest* +LIBS="$ac_save_LIBS" + +fi +if eval "test \"`echo '$ac_cv_lib_'$ac_lib_var`\" = yes"; then + echo "$ac_t""yes" 1>&6 + X_PRE_LIBS="$X_PRE_LIBS -lSM -lICE" +else + echo "$ac_t""no" 1>&6 +fi + + LDFLAGS="$ac_save_LDFLAGS" + +fi + + +# Check whether --with-installed-zlib or --without-installed-zlib was given. +if test "${with_installed_zlib+set}" = set; then + withval="$with_installed_zlib" + : +fi + + +if test "$with_installed_zlib" = yes; then + echo "using installed zlib" + ZLIB_LIB=-lz +else + ZLIB_DIR=zlib + ZLIB_INCLUDE='-I$(top_srcdir)/zlib' + ZLIB_LIB='$(top_srcdir)/zlib/libz.a' + echo "configuring zlib..." + (cd zlib; ./configure) + echo "...done configuring zlib" +fi + + + + + +if test "$SOLARIS" = yes; then + VSNPRINTF_DEFINE= +else + echo $ac_n "checking for vsnprintf""... $ac_c" 1>&6 +echo "configure:1982: checking for vsnprintf" >&5 +if eval "test \"`echo '$''{'ac_cv_func_vsnprintf'+set}'`\" = set"; then + echo $ac_n "(cached) $ac_c" 1>&6 +else + cat > conftest.$ac_ext < +/* Override any gcc2 internal prototype to avoid an error. */ +#ifdef __cplusplus +extern "C" +#endif +/* We use char because int might match the return type of a gcc2 + builtin and then its argument prototype would still apply. */ +char vsnprintf(); + +int main() { + +/* The GNU C library defines this for functions which it implements + to always fail with ENOSYS. Some functions are actually named + something starting with __ and the normal name is an alias. */ +#if defined (__stub_vsnprintf) || defined (__stub___vsnprintf) +choke me +#else +vsnprintf(); +#endif + +; return 0; } +EOF +if { (eval echo configure:2013: \"$ac_link\") 1>&5; (eval $ac_link) 2>&5; } && test -s conftest${ac_exeext}; then + rm -rf conftest* + eval "ac_cv_func_vsnprintf=yes" +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + eval "ac_cv_func_vsnprintf=no" +fi +rm -f conftest* +fi + +if eval "test \"`echo '$ac_cv_func_'vsnprintf`\" = yes"; then + echo "$ac_t""yes" 1>&6 + VSNPRINTF_DEFINE='-DHAVE_VSNPRINTF' +else + echo "$ac_t""no" 1>&6 +VSNPRINTF_DEFINE= +fi + +fi + + +echo $ac_n "checking for socklen_t""... $ac_c" 1>&6 +echo "configure:2037: checking for socklen_t" >&5 +cat > conftest.$ac_ext < + #include +int main() { +socklen_t x; +accept(0, 0, &x); +; return 0; } +EOF +if { (eval echo configure:2048: \"$ac_compile\") 1>&5; (eval $ac_compile) 2>&5; }; then + rm -rf conftest* + echo "$ac_t""yes" 1>&6 +SOCKLEN_T_DEFINE='-DVNC_SOCKLEN_T=socklen_t' +else + echo "configure: failed program was:" >&5 + cat conftest.$ac_ext >&5 + rm -rf conftest* + echo "$ac_t""using int" 1>&6 +SOCKLEN_T_DEFINE='-DVNC_SOCKLEN_T=int' +fi +rm -f conftest* + + +BOILERPLATE=boilerplate.mk + +if (sh -c "make --version" 2>/dev/null | grep GNU 2>&1 >/dev/null); then + if sh -c "vncmkdepend" >/dev/null 2>&1; then + BOILERPLATE="$BOILERPLATE:depend.mk" + fi +fi + +trap '' 1 2 15 +cat > confcache <<\EOF +# This file is a shell script that caches the results of configure +# tests run on this system so they can be shared between configure +# scripts and configure runs. It is not useful on other systems. +# If it contains results you don't want to keep, you may remove or edit it. +# +# By default, configure uses ./config.cache as the cache file, +# creating it if it does not exist already. You can give configure +# the --cache-file=FILE option to use a different cache file; that is +# what configure does when it calls configure scripts in +# subdirectories, so they share the cache. +# Giving --cache-file=/dev/null disables caching, for debugging configure. +# config.status only pays attention to the cache file if you give it the +# --recheck option to rerun configure. +# +EOF +# The following way of writing the cache mishandles newlines in values, +# but we know of no workaround that is simple, portable, and efficient. +# So, don't put newlines in cache variables' values. +# Ultrix sh set writes to stderr and can't be redirected directly, +# and sets the high bit in the cache file unless we assign to the vars. +(set) 2>&1 | + case `(ac_space=' '; set | grep ac_space) 2>&1` in + *ac_space=\ *) + # `set' does not quote correctly, so add quotes (double-quote substitution + # turns \\\\ into \\, and sed turns \\ into \). + sed -n \ + -e "s/'/'\\\\''/g" \ + -e "s/^\\([a-zA-Z0-9_]*_cv_[a-zA-Z0-9_]*\\)=\\(.*\\)/\\1=\${\\1='\\2'}/p" + ;; + *) + # `set' quotes correctly as required by POSIX, so do not add quotes. + sed -n -e 's/^\([a-zA-Z0-9_]*_cv_[a-zA-Z0-9_]*\)=\(.*\)/\1=${\1=\2}/p' + ;; + esac >> confcache +if cmp -s $cache_file confcache; then + : +else + if test -w $cache_file; then + echo "updating cache $cache_file" + cat confcache > $cache_file + else + echo "not updating unwritable cache $cache_file" + fi +fi +rm -f confcache + +trap 'rm -fr conftest* confdefs* core core.* *.core $ac_clean_files; exit 1' 1 2 15 + +test "x$prefix" = xNONE && prefix=$ac_default_prefix +# Let make expand exec_prefix. +test "x$exec_prefix" = xNONE && exec_prefix='${prefix}' + +# Any assignment to VPATH causes Sun make to only execute +# the first set of double-colon rules, so remove it if not needed. +# If there is a colon in the path, we need to keep it. +if test "x$srcdir" = x.; then + ac_vpsub='/^[ ]*VPATH[ ]*=[^:]*$/d' +fi + +trap 'rm -f $CONFIG_STATUS conftest*; exit 1' 1 2 15 + +# Transform confdefs.h into DEFS. +# Protect against shell expansion while executing Makefile rules. +# Protect against Makefile macro expansion. +cat > conftest.defs <<\EOF +s%#define \([A-Za-z_][A-Za-z0-9_]*\) *\(.*\)%-D\1=\2%g +s%[ `~#$^&*(){}\\|;'"<>?]%\\&%g +s%\[%\\&%g +s%\]%\\&%g +s%\$%$$%g +EOF +DEFS=`sed -f conftest.defs confdefs.h | tr '\012' ' '` +rm -f conftest.defs + + +# Without the "./", some shells look in PATH for config.status. +: ${CONFIG_STATUS=./config.status} + +echo creating $CONFIG_STATUS +rm -f $CONFIG_STATUS +cat > $CONFIG_STATUS </dev/null | sed 1q`: +# +# $0 $ac_configure_args +# +# Compiler output produced by configure, useful for debugging +# configure, is in ./config.log if it exists. + +ac_cs_usage="Usage: $CONFIG_STATUS [--recheck] [--version] [--help]" +for ac_option +do + case "\$ac_option" in + -recheck | --recheck | --rechec | --reche | --rech | --rec | --re | --r) + echo "running \${CONFIG_SHELL-/bin/sh} $0 $ac_configure_args --no-create --no-recursion" + exec \${CONFIG_SHELL-/bin/sh} $0 $ac_configure_args --no-create --no-recursion ;; + -version | --version | --versio | --versi | --vers | --ver | --ve | --v) + echo "$CONFIG_STATUS generated by autoconf version 2.13" + exit 0 ;; + -help | --help | --hel | --he | --h) + echo "\$ac_cs_usage"; exit 0 ;; + *) echo "\$ac_cs_usage"; exit 1 ;; + esac +done + +ac_given_srcdir=$srcdir + +trap 'rm -fr `echo "Makefile:Makefile.in:$BOILERPLATE \ + rdr/Makefile:rdr/Makefile.in:$BOILERPLATE \ + network/Makefile:network/Makefile.in:$BOILERPLATE \ + Xregion/Makefile:Xregion/Makefile.in:$BOILERPLATE \ + rfb/Makefile:rfb/Makefile.in:$BOILERPLATE \ +" | sed "s/:[^ ]*//g"` conftest*; exit 1' 1 2 15 +EOF +cat >> $CONFIG_STATUS < conftest.subs <<\\CEOF +$ac_vpsub +$extrasub +s%@SHELL@%$SHELL%g +s%@CFLAGS@%$CFLAGS%g +s%@CPPFLAGS@%$CPPFLAGS%g +s%@CXXFLAGS@%$CXXFLAGS%g +s%@FFLAGS@%$FFLAGS%g +s%@DEFS@%$DEFS%g +s%@LDFLAGS@%$LDFLAGS%g +s%@LIBS@%$LIBS%g +s%@exec_prefix@%$exec_prefix%g +s%@prefix@%$prefix%g +s%@program_transform_name@%$program_transform_name%g +s%@bindir@%$bindir%g +s%@sbindir@%$sbindir%g +s%@libexecdir@%$libexecdir%g +s%@datadir@%$datadir%g +s%@sysconfdir@%$sysconfdir%g +s%@sharedstatedir@%$sharedstatedir%g +s%@localstatedir@%$localstatedir%g +s%@libdir@%$libdir%g +s%@includedir@%$includedir%g +s%@oldincludedir@%$oldincludedir%g +s%@infodir@%$infodir%g +s%@mandir@%$mandir%g +s%@CC@%$CC%g +s%@CXX@%$CXX%g +s%@RANLIB@%$RANLIB%g +s%@SET_MAKE@%$SET_MAKE%g +s%@MITSHM_CPPFLAGS@%$MITSHM_CPPFLAGS%g +s%@CXXCPP@%$CXXCPP%g +s%@X_CFLAGS@%$X_CFLAGS%g +s%@X_PRE_LIBS@%$X_PRE_LIBS%g +s%@X_LIBS@%$X_LIBS%g +s%@X_EXTRA_LIBS@%$X_EXTRA_LIBS%g +s%@ZLIB_DIR@%$ZLIB_DIR%g +s%@ZLIB_INCLUDE@%$ZLIB_INCLUDE%g +s%@ZLIB_LIB@%$ZLIB_LIB%g +s%@VSNPRINTF_DEFINE@%$VSNPRINTF_DEFINE%g +s%@SOCKLEN_T_DEFINE@%$SOCKLEN_T_DEFINE%g + +CEOF +EOF + +cat >> $CONFIG_STATUS <<\EOF + +# Split the substitutions into bite-sized pieces for seds with +# small command number limits, like on Digital OSF/1 and HP-UX. +ac_max_sed_cmds=90 # Maximum number of lines to put in a sed script. +ac_file=1 # Number of current file. +ac_beg=1 # First line for current file. +ac_end=$ac_max_sed_cmds # Line after last line for current file. +ac_more_lines=: +ac_sed_cmds="" +while $ac_more_lines; do + if test $ac_beg -gt 1; then + sed "1,${ac_beg}d; ${ac_end}q" conftest.subs > conftest.s$ac_file + else + sed "${ac_end}q" conftest.subs > conftest.s$ac_file + fi + if test ! -s conftest.s$ac_file; then + ac_more_lines=false + rm -f conftest.s$ac_file + else + if test -z "$ac_sed_cmds"; then + ac_sed_cmds="sed -f conftest.s$ac_file" + else + ac_sed_cmds="$ac_sed_cmds | sed -f conftest.s$ac_file" + fi + ac_file=`expr $ac_file + 1` + ac_beg=$ac_end + ac_end=`expr $ac_end + $ac_max_sed_cmds` + fi +done +if test -z "$ac_sed_cmds"; then + ac_sed_cmds=cat +fi +EOF + +cat >> $CONFIG_STATUS <> $CONFIG_STATUS <<\EOF +for ac_file in .. $CONFIG_FILES; do if test "x$ac_file" != x..; then + # Support "outfile[:infile[:infile...]]", defaulting infile="outfile.in". + case "$ac_file" in + *:*) ac_file_in=`echo "$ac_file"|sed 's%[^:]*:%%'` + ac_file=`echo "$ac_file"|sed 's%:.*%%'` ;; + *) ac_file_in="${ac_file}.in" ;; + esac + + # Adjust a relative srcdir, top_srcdir, and INSTALL for subdirectories. + + # Remove last slash and all that follows it. Not all systems have dirname. + ac_dir=`echo $ac_file|sed 's%/[^/][^/]*$%%'` + if test "$ac_dir" != "$ac_file" && test "$ac_dir" != .; then + # The file is in a subdirectory. + test ! -d "$ac_dir" && mkdir "$ac_dir" + ac_dir_suffix="/`echo $ac_dir|sed 's%^\./%%'`" + # A "../" for each directory in $ac_dir_suffix. + ac_dots=`echo $ac_dir_suffix|sed 's%/[^/]*%../%g'` + else + ac_dir_suffix= ac_dots= + fi + + case "$ac_given_srcdir" in + .) srcdir=. + if test -z "$ac_dots"; then top_srcdir=. + else top_srcdir=`echo $ac_dots|sed 's%/$%%'`; fi ;; + /*) srcdir="$ac_given_srcdir$ac_dir_suffix"; top_srcdir="$ac_given_srcdir" ;; + *) # Relative path. + srcdir="$ac_dots$ac_given_srcdir$ac_dir_suffix" + top_srcdir="$ac_dots$ac_given_srcdir" ;; + esac + + + echo creating "$ac_file" + rm -f "$ac_file" + configure_input="Generated automatically from `echo $ac_file_in|sed 's%.*/%%'` by configure." + case "$ac_file" in + *Makefile*) ac_comsub="1i\\ +# $configure_input" ;; + *) ac_comsub= ;; + esac + + ac_file_inputs=`echo $ac_file_in|sed -e "s%^%$ac_given_srcdir/%" -e "s%:% $ac_given_srcdir/%g"` + sed -e "$ac_comsub +s%@configure_input@%$configure_input%g +s%@srcdir@%$srcdir%g +s%@top_srcdir@%$top_srcdir%g +" $ac_file_inputs | (eval "$ac_sed_cmds") > $ac_file +fi; done +rm -f conftest.s* + +EOF +cat >> $CONFIG_STATUS <> $CONFIG_STATUS <<\EOF + +exit 0 +EOF +chmod +x $CONFIG_STATUS +rm -fr confdefs* $ac_clean_files +test "$no_create" = yes || ${CONFIG_SHELL-/bin/sh} $CONFIG_STATUS || exit 1 + diff --git a/src/vnc/common/configure.in b/src/vnc/common/configure.in new file mode 100644 index 00000000..cc1897a3 --- /dev/null +++ b/src/vnc/common/configure.in @@ -0,0 +1,99 @@ +dnl Process this file with autoconf to produce a configure script. +AC_INIT(rdr/InStream.h) + +dnl dirty hack to prevent use of -g in CFLAGS and CXXFLAGS +ac_cv_prog_cc_g=no +ac_cv_prog_cxx_g=no + +dnl Checks for programs. +AC_PROG_CC +AC_PROG_CXX +AC_PROG_RANLIB +AC_PROG_MAKE_SET +AC_LANG_CPLUSPLUS + +case "`(uname -sr) 2>/dev/null`" in +"SunOS 5"*) + SOLARIS=yes + USE_MITSHM=yes + ;; +"Linux"*) + LINUX=yes + USE_MITSHM=yes + ;; +esac + +if test "$USE_MITSHM" = yes; then + MITSHM_CPPFLAGS="-DMITSHM" +fi +AC_SUBST(MITSHM_CPPFLAGS) + +if test "$GCC" = yes; then + CFLAGS="$CFLAGS -Wall" + if test "$SOLARIS" = yes; then + CFLAGS="$CFLAGS -Wno-unknown-pragmas -Wno-implicit-int" + fi +fi +if test "$GXX" = yes; then + CXXFLAGS="$CXXFLAGS -Wall" + if test "$SOLARIS" = yes; then + CXXFLAGS="$CXXFLAGS -Wno-unknown-pragmas -Wno-implicit-int -fpermissive" + fi +fi + +AC_PATH_XTRA + +AC_ARG_WITH(installed-zlib, +[ --with-installed-zlib use the version of zlib which is installed on the + system instead of the one distributed with VNC]) + +if test "$with_installed_zlib" = yes; then + echo "using installed zlib" + ZLIB_LIB=-lz +else + ZLIB_DIR=zlib + ZLIB_INCLUDE='-I$(top_srcdir)/zlib' + ZLIB_LIB='$(top_srcdir)/zlib/libz.a' + echo "configuring zlib..." + (cd zlib; ./configure) + echo "...done configuring zlib" +fi + +AC_SUBST(ZLIB_DIR) +AC_SUBST(ZLIB_INCLUDE) +AC_SUBST(ZLIB_LIB) + +if test "$SOLARIS" = yes; then + dnl Solaris 2.5 lacks vsnprintf, so use our own version & support 2.5 + VSNPRINTF_DEFINE= +else + AC_CHECK_FUNC(vsnprintf,VSNPRINTF_DEFINE='-DHAVE_VSNPRINTF',VSNPRINTF_DEFINE=) +fi +AC_SUBST(VSNPRINTF_DEFINE) + +AC_MSG_CHECKING(for socklen_t) +AC_TRY_COMPILE( +[#include + #include ], +[socklen_t x; +accept(0, 0, &x);], +AC_MSG_RESULT(yes) +SOCKLEN_T_DEFINE='-DVNC_SOCKLEN_T=socklen_t', +AC_MSG_RESULT(using int) +SOCKLEN_T_DEFINE='-DVNC_SOCKLEN_T=int') +AC_SUBST(SOCKLEN_T_DEFINE) + +BOILERPLATE=boilerplate.mk + +if (sh -c "make --version" 2>/dev/null | grep GNU 2>&1 >/dev/null); then + if sh -c "vncmkdepend" >/dev/null 2>&1; then + BOILERPLATE="$BOILERPLATE:depend.mk" + fi +fi + +AC_OUTPUT(Makefile:Makefile.in:$BOILERPLATE \ + rdr/Makefile:rdr/Makefile.in:$BOILERPLATE \ + network/Makefile:network/Makefile.in:$BOILERPLATE \ + Xregion/Makefile:Xregion/Makefile.in:$BOILERPLATE \ + rfb/Makefile:rfb/Makefile.in:$BOILERPLATE \ +) diff --git a/src/vnc/common/depend.mk b/src/vnc/common/depend.mk new file mode 100644 index 00000000..51d4cd63 --- /dev/null +++ b/src/vnc/common/depend.mk @@ -0,0 +1,76 @@ +# +# C / C++ header dependency stuff +# +# Needs GNU make and vncmkdepend, a hacked version of makedepend + +.SUFFIXES: .d + +CMAKEDEPEND = vncmkdepend +CXXMAKEDEPEND = vncmkdepend + +# +# The recommended method of doing dependency analysis in the GNU make manual +# turns out to be painfully slow. This method is similar but it's +# substantially faster and retains the desirable property that the user doesn't +# need to manually invoke a "make depend" step. +# +# As with the method described in the manual, we generate a separate dependency +# (.d) file for each source file. The .d file records the header files that +# each C or C++ source file includes. Any source file recorded in SRCS or +# CXXSRCS will cause us to try and include the corresponding .d file and GNU +# make then treats each .d file as a target to be remade. +# +# Unlike the manual's method, the rule we provide for making the .d file is +# actually a fake. All it does is record in a temporary file that the .d file +# needs to be remade. But as well as all the .d files, we also try to include +# a file called "depend.phony". This file never exists, but it causes GNU make +# to try and make the target "depend.phony". The rule for depend.phony then +# looks at the temporary files generated by the .d rules and then invokes the +# "omkdepend" program on all of the source files in one go. +# + +# +# We use simple assignment here to remove any of the depend.tmp files +# at the time make parses this bit. +# + +dummyvariable := $(shell $(RM) cdepend.tmp cxxdepend.tmp) + +# +# Now the "fake" rules for generating .d files. +# + +%.d: %.c + @echo "$<" >> cdepend.tmp + +%.d: %.cxx + @echo "$<" >> cxxdepend.tmp + +# +# The depend.phony rule which actually runs omkdepend. +# + +depend.phony: + @if [ -f cdepend.tmp ]; then \ + echo $(CMAKEDEPEND) $(ALL_CPPFLAGS) `cat cdepend.tmp`; \ + $(CMAKEDEPEND) $(ALL_CPPFLAGS) `cat cdepend.tmp`; \ + rm -f cdepend.tmp; \ + fi; \ + if [ -f cxxdepend.tmp ]; then \ + echo $(CXXMAKEDEPEND) $(ALL_CPPFLAGS) `cat cxxdepend.tmp`; \ + $(CXXMAKEDEPEND) $(ALL_CPPFLAGS) `cat cxxdepend.tmp`; \ + rm -f cxxdepend.tmp; \ + fi + +# +# Now include the .d files and the "depend.phony" file which never exists. +# For some reason GNU make evaluates the targets in reverse order, so we need +# to include depend.phony first. The "-" tells make not to complain that it +# can't find the file. +# + +-include depend.phony + +ifdef SRCS +-include $(patsubst %.c,%.d,$(patsubst %.cxx,%.d,$(SRCS))) +endif diff --git a/src/vnc/common/javabin/index.vnc b/src/vnc/common/javabin/index.vnc new file mode 100644 index 00000000..aecb6131 --- /dev/null +++ b/src/vnc/common/javabin/index.vnc @@ -0,0 +1,13 @@ + + + +VNC viewer for Java + + + + + + + + diff --git a/src/vnc/common/javabin/logo150x150.gif b/src/vnc/common/javabin/logo150x150.gif new file mode 100644 index 0000000000000000000000000000000000000000..f1699ba5894157998459c8455541206c04091d6c GIT binary patch literal 3584 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 literal 0 HcmV?d00001 diff --git a/src/vnc/common/javabin/vncviewer.jar b/src/vnc/common/javabin/vncviewer.jar new file mode 100644 index 0000000000000000000000000000000000000000..9916d39822c9b5b8b47d368230c6651a97de78be GIT binary patch literal 104689 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%0DM98ZTsjuj(FUsgy0^Hv!D7?mBORa8>sHd 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. + +set(inc + Socket.h + TcpSocket.h +) + +set(src + ${inc} + TcpSocket.cxx +) + +if (WIN32) + list(APPEND src ${inc}) +endif() + +set(inc + .. +) + +include_directories(${inc}) +add_library(vnc_network STATIC ${src}) diff --git a/src/vnc/common/network/Makefile.in b/src/vnc/common/network/Makefile.in new file mode 100644 index 00000000..8aed303a --- /dev/null +++ b/src/vnc/common/network/Makefile.in @@ -0,0 +1,17 @@ + +SRCS = TcpSocket.cxx + +OBJS = $(SRCS:.cxx=.o) + +DIR_CPPFLAGS = -I$(top_srcdir) @SOCKLEN_T_DEFINE@ + +library = libnetwork.a + +all:: $(library) + +$(library): $(OBJS) + rm -f $(library) + $(AR) $(library) $(OBJS) + $(RANLIB) $(library) + +# followed by boilerplate.mk diff --git a/src/vnc/common/network/Socket.h b/src/vnc/common/network/Socket.h new file mode 100644 index 00000000..7713d2cd --- /dev/null +++ b/src/vnc/common/network/Socket.h @@ -0,0 +1,147 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// -=- Socket.h - abstract base-class for any kind of network stream/socket + +#ifndef __NETWORK_SOCKET_H__ +#define __NETWORK_SOCKET_H__ + +#include +#include +#include +#include + +namespace network { + + class Socket { + public: + Socket(int fd) + : instream(new rdr::FdInStream(fd)), + outstream(new rdr::FdOutStream(fd)), + ownStreams(true), isShutdown_(false), + queryConnection(false) {} + virtual ~Socket() { + if (ownStreams) { + delete instream; + delete outstream; + } + } + rdr::FdInStream &inStream() {return *instream;} + rdr::FdOutStream &outStream() {return *outstream;} + int getFd() {return outstream->getFd();} + + // if shutdown() is overridden then the override MUST call on to here + virtual void shutdown() {isShutdown_ = true;} + bool isShutdown() const {return isShutdown_;} + + // information about this end of the socket + virtual char* getMyAddress() = 0; // a string e.g. "192.168.0.1" + virtual int getMyPort() = 0; + virtual char* getMyEndpoint() = 0; //

:: + + // information about the remote end of the socket + virtual char* getPeerAddress() = 0; // a string e.g. "192.168.0.1" + virtual int getPeerPort() = 0; + virtual char* getPeerEndpoint() = 0; //
:: + + // Is the remote end on the same machine? + virtual bool sameMachine() = 0; + + // Was there a "?" in the ConnectionFilter used to accept this Socket? + void setRequiresQuery() {queryConnection = true;} + bool requiresQuery() const {return queryConnection;} + + protected: + Socket() : instream(0), outstream(0), ownStreams(false), + isShutdown_(false), queryConnection(false) {} + Socket(rdr::FdInStream* i, rdr::FdOutStream* o, bool own) + : instream(i), outstream(o), ownStreams(own), + isShutdown_(false), queryConnection(false) {} + rdr::FdInStream* instream; + rdr::FdOutStream* outstream; + bool ownStreams; + bool isShutdown_; + bool queryConnection; + }; + + class ConnectionFilter { + public: + virtual bool verifyConnection(Socket* s) = 0; + }; + + class SocketListener { + public: + SocketListener() : fd(0), filter(0) {} + virtual ~SocketListener() {} + + // shutdown() stops the socket from accepting further connections + virtual void shutdown() = 0; + + // accept() returns a new Socket object if there is a connection + // attempt in progress AND if the connection passes the filter + // if one is installed. Otherwise, returns 0. + virtual Socket* accept() = 0; + + // setFilter() applies the specified filter to all new connections + void setFilter(ConnectionFilter* f) {filter = f;} + int getFd() {return fd;} + protected: + int fd; + ConnectionFilter* filter; + }; + + struct SocketException : public rdr::SystemException { + SocketException(const char* text, int err_) : rdr::SystemException(text, err_) {} + }; + + class SocketServer { + public: + virtual ~SocketServer() {} + + // addSocket() tells the server to serve the Socket. The caller + // retains ownership of the Socket - the only way for the server + // to discard a Socket is by calling shutdown() on it. + // outgoing is set to true if the socket was created by connecting out + // to another host, or false if the socket was created by accept()ing + // an incoming connection. + virtual void addSocket(network::Socket* sock, bool outgoing=false) = 0; + + // removeSocket() tells the server to stop serving the Socket. The + // caller retains ownership of the Socket - the server must NOT + // delete the Socket! This call is used mainly to cause per-Socket + // resources to be freed. + virtual void removeSocket(network::Socket* sock) = 0; + + // processSocketEvent() tells the server there is a Socket read event. + // The implementation can indicate that the Socket is no longer active + // by calling shutdown() on it. The caller will then call removeSocket() + // soon after processSocketEvent returns, to allow any pre-Socket + // resources to be tidied up. + virtual void processSocketEvent(network::Socket* sock) = 0; + + // checkTimeouts() allows the server to check socket timeouts, etc. The + // return value is the number of milliseconds to wait before + // checkTimeouts() should be called again. If this number is zero then + // there is no timeout and checkTimeouts() should be called the next time + // an event occurs. + virtual int checkTimeouts() = 0; + }; + +} + +#endif // __NETWORK_SOCKET_H__ diff --git a/src/vnc/common/network/TcpSocket.cxx b/src/vnc/common/network/TcpSocket.cxx new file mode 100644 index 00000000..058621b1 --- /dev/null +++ b/src/vnc/common/network/TcpSocket.cxx @@ -0,0 +1,467 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifdef WIN32 +//#include +#include +#define errorNumber WSAGetLastError() +#define snprintf _snprintf +#else +#define errorNumber errno +#define closesocket close +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#include +#include +#include + +#ifndef VNC_SOCKLEN_T +#define VNC_SOCKLEN_T int +#endif + +#ifndef INADDR_NONE +#define INADDR_NONE ((unsigned long)-1) +#endif + +using namespace network; +using namespace rdr; + +static rfb::LogWriter vlog("TcpSocket"); + + +// -=- Socket initialisation +static bool socketsInitialised = false; +static void initSockets() { + if (socketsInitialised) + return; +#ifdef WIN32 + WORD requiredVersion = MAKEWORD(2,0); + WSADATA initResult; + + if (WSAStartup(requiredVersion, &initResult) != 0) + throw SocketException("unable to initialise Winsock2", errorNumber); +#else + signal(SIGPIPE, SIG_IGN); +#endif + socketsInitialised = true; +} + + +// -=- TcpSocket + +TcpSocket::TcpSocket(int sock, bool close) + : Socket(new FdInStream(sock), new FdOutStream(sock), true), closeFd(close) +{ +} + +TcpSocket::TcpSocket(const char *host, int port) + : closeFd(true) +{ + int sock; + + // - Create a socket + initSockets(); + if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) + throw SocketException("unable to create socket", errorNumber); + +#ifndef WIN32 + // - By default, close the socket on exec() + fcntl(sock, F_SETFD, FD_CLOEXEC); +#endif + + // - Connect it to something + + // Try processing the host as an IP address + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr(host); + addr.sin_port = htons(port); + if ((int)addr.sin_addr.s_addr == -1) { + // Host was not an IP address - try resolving as DNS name + struct hostent *hostinfo; + hostinfo = gethostbyname(host); + if (hostinfo && hostinfo->h_addr) { + addr.sin_addr.s_addr = ((struct in_addr *)hostinfo->h_addr)->s_addr; + } else { + int e = errorNumber; + closesocket(sock); + throw SocketException("unable to resolve host by name", e); + } + } + + // Attempt to connect to the remote host + if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + int e = errorNumber; + closesocket(sock); + throw SocketException("unable to connect to host", e); + } + + // Disable Nagle's algorithm, to reduce latency + enableNagles(sock, false); + + // Create the input and output streams + instream = new FdInStream(sock); + outstream = new FdOutStream(sock); + ownStreams = true; +} + +TcpSocket::~TcpSocket() { + if (closeFd) + closesocket(getFd()); +} + +char* TcpSocket::getMyAddress() { + struct sockaddr_in info; + struct in_addr addr; + VNC_SOCKLEN_T info_size = sizeof(info); + + getsockname(getFd(), (struct sockaddr *)&info, &info_size); + memcpy(&addr, &info.sin_addr, sizeof(addr)); + + char* name = inet_ntoa(addr); + if (name) { + return rfb::strDup(name); + } else { + return rfb::strDup(""); + } +} + +int TcpSocket::getMyPort() { + return getSockPort(getFd()); +} + +char* TcpSocket::getMyEndpoint() { + rfb::CharArray address; address.buf = getMyAddress(); + int port = getMyPort(); + + int buflen = strlen(address.buf) + 32; + char* buffer = new char[buflen]; + sprintf(buffer, "%s::%d", address.buf, port); + return buffer; +} + +char* TcpSocket::getPeerAddress() { + struct sockaddr_in info; + struct in_addr addr; + VNC_SOCKLEN_T info_size = sizeof(info); + + getpeername(getFd(), (struct sockaddr *)&info, &info_size); + memcpy(&addr, &info.sin_addr, sizeof(addr)); + + char* name = inet_ntoa(addr); + if (name) { + return rfb::strDup(name); + } else { + return rfb::strDup(""); + } +} + +int TcpSocket::getPeerPort() { + struct sockaddr_in info; + VNC_SOCKLEN_T info_size = sizeof(info); + + getpeername(getFd(), (struct sockaddr *)&info, &info_size); + return ntohs(info.sin_port); +} + +char* TcpSocket::getPeerEndpoint() { + rfb::CharArray address; address.buf = getPeerAddress(); + int port = getPeerPort(); + + int buflen = strlen(address.buf) + 32; + char* buffer = new char[buflen]; + sprintf(buffer, "%s::%d", address.buf, port); + return buffer; +} + +bool TcpSocket::sameMachine() { + struct sockaddr_in peeraddr, myaddr; + VNC_SOCKLEN_T addrlen = sizeof(struct sockaddr_in); + + getpeername(getFd(), (struct sockaddr *)&peeraddr, &addrlen); + getsockname(getFd(), (struct sockaddr *)&myaddr, &addrlen); + + return (peeraddr.sin_addr.s_addr == myaddr.sin_addr.s_addr); +} + +void TcpSocket::shutdown() +{ + Socket::shutdown(); + ::shutdown(getFd(), 2); +} + +bool TcpSocket::enableNagles(int sock, bool enable) { + int one = enable ? 0 : 1; + if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, + (char *)&one, sizeof(one)) < 0) { + int e = errorNumber; + vlog.error("unable to setsockopt TCP_NODELAY: %d", e); + return false; + } + return true; +} + +bool TcpSocket::isSocket(int sock) +{ + struct sockaddr_in info; + VNC_SOCKLEN_T info_size = sizeof(info); + return getsockname(sock, (struct sockaddr *)&info, &info_size) >= 0; +} + +bool TcpSocket::isConnected(int sock) +{ + struct sockaddr_in info; + VNC_SOCKLEN_T info_size = sizeof(info); + return getpeername(sock, (struct sockaddr *)&info, &info_size) >= 0; +} + +int TcpSocket::getSockPort(int sock) +{ + struct sockaddr_in info; + VNC_SOCKLEN_T info_size = sizeof(info); + if (getsockname(sock, (struct sockaddr *)&info, &info_size) < 0) + return 0; + return ntohs(info.sin_port); +} + + +TcpListener::TcpListener(int port, bool localhostOnly, int sock, bool close_) + : closeFd(close_) +{ + if (sock != -1) { + fd = sock; + return; + } + + initSockets(); + if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) + throw SocketException("unable to create listening socket", errorNumber); + +#ifndef WIN32 + // - By default, close the socket on exec() + fcntl(fd, F_SETFD, FD_CLOEXEC); + + int one = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, + (const char *)&one, sizeof(one)) < 0) { + int e = errorNumber; + closesocket(fd); + throw SocketException("unable to create listening socket", e); + } +#endif + + // - Bind it to the desired port + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (localhostOnly) + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + else + addr.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + int e = errorNumber; + closesocket(fd); + throw SocketException("unable to bind listening socket", e); + } + + // - Set it to be a listening socket + if (listen(fd, 5) < 0) { + int e = errorNumber; + closesocket(fd); + throw SocketException("unable to set socket to listening mode", e); + } +} + +TcpListener::~TcpListener() { + if (closeFd) closesocket(fd); +} + +void TcpListener::shutdown() +{ +#ifdef WIN32 + closesocket(getFd()); +#else + ::shutdown(getFd(), 2); +#endif +} + + +Socket* +TcpListener::accept() { + int new_sock = -1; + + // Accept an incoming connection + if ((new_sock = ::accept(fd, 0, 0)) < 0) + throw SocketException("unable to accept new connection", errorNumber); + +#ifndef WIN32 + // - By default, close the socket on exec() + fcntl(new_sock, F_SETFD, FD_CLOEXEC); +#endif + + // Disable Nagle's algorithm, to reduce latency + TcpSocket::enableNagles(new_sock, false); + + // Create the socket object & check connection is allowed + TcpSocket* s = new TcpSocket(new_sock); + if (filter && !filter->verifyConnection(s)) { + delete s; + return 0; + } + return s; +} + +void TcpListener::getMyAddresses(std::list* result) { + const hostent* addrs = gethostbyname(0); + if (addrs == 0) + throw rdr::SystemException("gethostbyname", errorNumber); + if (addrs->h_addrtype != AF_INET) + throw rdr::Exception("getMyAddresses: bad family"); + for (int i=0; addrs->h_addr_list[i] != 0; i++) { + const char* addrC = inet_ntoa(*((struct in_addr*)addrs->h_addr_list[i])); + char* addr = new char[strlen(addrC)+1]; + strcpy(addr, addrC); + result->push_back(addr); + } +} + +int TcpListener::getMyPort() { + return TcpSocket::getSockPort(getFd()); +} + + +TcpFilter::TcpFilter(const char* spec) { + rfb::CharArray tmp; + tmp.buf = rfb::strDup(spec); + while (tmp.buf) { + rfb::CharArray first; + rfb::strSplit(tmp.buf, ',', &first.buf, &tmp.buf); + if (strlen(first.buf)) + filter.push_back(parsePattern(first.buf)); + } +} + +TcpFilter::~TcpFilter() { +} + + +static bool +patternMatchIP(const TcpFilter::Pattern& pattern, const char* value) { + unsigned long address = inet_addr(value); + if (address == INADDR_NONE) return false; + return ((pattern.address & pattern.mask) == (address & pattern.mask)); +} + +bool +TcpFilter::verifyConnection(Socket* s) { + rfb::CharArray name; + + name.buf = s->getPeerAddress(); + std::list::iterator i; + for (i=filter.begin(); i!=filter.end(); i++) { + if (patternMatchIP(*i, name.buf)) { + switch ((*i).action) { + case Accept: + vlog.debug("ACCEPT %s", name.buf); + return true; + case Query: + vlog.debug("QUERY %s", name.buf); + s->setRequiresQuery(); + return true; + case Reject: + vlog.debug("REJECT %s", name.buf); + return false; + } + } + } + + vlog.debug("[REJECT] %s", name.buf); + return false; +} + + +TcpFilter::Pattern TcpFilter::parsePattern(const char* p) { + TcpFilter::Pattern pattern; + + bool expandMask = false; + rfb::CharArray addr, mask; + + if (rfb::strSplit(&p[1], '/', &addr.buf, &mask.buf)) { + if (rfb::strContains(mask.buf, '.')) { + pattern.mask = inet_addr(mask.buf); + } else { + pattern.mask = atoi(mask.buf); + expandMask = true; + } + } else { + pattern.mask = 32; + expandMask = true; + } + if (expandMask) { + unsigned long expanded = 0; + // *** check endianness! + for (int i=0; i<(int)pattern.mask; i++) + expanded |= 1<<(31-i); + pattern.mask = htonl(expanded); + } + + pattern.address = inet_addr(addr.buf) & pattern.mask; + if ((pattern.address == INADDR_NONE) || + (pattern.address == 0)) pattern.mask = 0; + + switch(p[0]) { + case '+': pattern.action = TcpFilter::Accept; break; + case '-': pattern.action = TcpFilter::Reject; break; + case '?': pattern.action = TcpFilter::Query; break; + }; + + return pattern; +} + +char* TcpFilter::patternToStr(const TcpFilter::Pattern& p) { + in_addr tmp; + rfb::CharArray addr, mask; + tmp.s_addr = p.address; + addr.buf = rfb::strDup(inet_ntoa(tmp)); + tmp.s_addr = p.mask; + mask.buf = rfb::strDup(inet_ntoa(tmp)); + char* result = new char[strlen(addr.buf)+1+strlen(mask.buf)+1+1]; + switch (p.action) { + case Accept: result[0] = '+'; break; + case Reject: result[0] = '-'; break; + case Query: result[0] = '?'; break; + }; + result[1] = 0; + strcat(result, addr.buf); + strcat(result, "/"); + strcat(result, mask.buf); + return result; +} diff --git a/src/vnc/common/network/TcpSocket.h b/src/vnc/common/network/TcpSocket.h new file mode 100644 index 00000000..8b19c7ef --- /dev/null +++ b/src/vnc/common/network/TcpSocket.h @@ -0,0 +1,99 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// -=- TcpSocket.h - base-class for TCP stream sockets. +// This header also defines the TcpListener class, used +// to listen for incoming socket connections over TCP +// +// NB: Any file descriptors created by the TcpSocket or +// TcpListener classes are close-on-exec if the OS supports +// it. TcpSockets initialised with a caller-supplied fd +// are NOT set to close-on-exec. + +#ifndef __NETWORK_TCP_SOCKET_H__ +#define __NETWORK_TCP_SOCKET_H__ + +#include + +#include + +namespace network { + + class TcpSocket : public Socket { + public: + TcpSocket(int sock, bool close=true); + TcpSocket(const char *name, int port); + virtual ~TcpSocket(); + + virtual char* getMyAddress(); + virtual int getMyPort(); + virtual char* getMyEndpoint(); + + virtual char* getPeerAddress(); + virtual int getPeerPort(); + virtual char* getPeerEndpoint(); + virtual bool sameMachine(); + + virtual void shutdown(); + + static bool enableNagles(int sock, bool enable); + static bool isSocket(int sock); + static bool isConnected(int sock); + static int getSockPort(int sock); + private: + bool closeFd; + }; + + class TcpListener : public SocketListener { + public: + TcpListener(int port, bool localhostOnly=false, int sock=-1, + bool close=true); + virtual ~TcpListener(); + + virtual void shutdown(); + virtual Socket* accept(); + + void getMyAddresses(std::list* addrs); + int getMyPort(); + + private: + bool closeFd; + }; + + class TcpFilter : public ConnectionFilter { + public: + TcpFilter(const char* filter); + virtual ~TcpFilter(); + + virtual bool verifyConnection(Socket* s); + + typedef enum {Accept, Reject, Query} Action; + struct Pattern { + Action action; + unsigned long address; + unsigned long mask; + }; + static Pattern parsePattern(const char* s); + static char* patternToStr(const Pattern& p); + protected: + std::list filter; + }; + +} + +#endif // __NETWORK_TCP_SOCKET_H__ diff --git a/src/vnc/common/rdr/CMakeLists.txt b/src/vnc/common/rdr/CMakeLists.txt new file mode 100644 index 00000000..5bd1f7ad --- /dev/null +++ b/src/vnc/common/rdr/CMakeLists.txt @@ -0,0 +1,58 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2012 Nick Bolton +# +# 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 COPYING 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 . + +set(inc + Exception.h + FdInStream.h + FdOutStream.h + FixedMemOutStream.h + HexInStream.h + HexOutStream.h + InStream.h + MemInStream.h + MemOutStream.h + OutStream.h + RandomStream.h + SubstitutingInStream.h + types.h + ZlibInStream.h + ZlibOutStream.h +) + +set(src + Exception.cxx + FdInStream.cxx + FdOutStream.cxx + HexInStream.cxx + HexOutStream.cxx + InStream.cxx + RandomStream.cxx + ZlibInStream.cxx + ZlibOutStream.cxx +) + +if (WIN32) + list(APPEND src ${inc}) +endif() + +set(inc + .. + ../zlib +) + +include_directories(${inc}) +add_library(vnc_rdr STATIC ${src}) +target_link_libraries(vnc_rdr + vnc_zlib) diff --git a/src/vnc/common/rdr/Exception.cxx b/src/vnc/common/rdr/Exception.cxx new file mode 100644 index 00000000..1f1ce66c --- /dev/null +++ b/src/vnc/common/rdr/Exception.cxx @@ -0,0 +1,73 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#include +#ifdef _WIN32 +#include +#include +//#include +#endif + +using namespace rdr; + +SystemException::SystemException(const char* s, int err_) + : Exception(s), err(err_) +{ + strncat(str_, ": ", len-1-strlen(str_)); +#ifdef _WIN32 + // Windows error messages are crap, so use unix ones for common errors. + const char* msg = 0; + switch (err) { + case WSAECONNREFUSED: msg = "Connection refused"; break; + case WSAETIMEDOUT: msg = "Connection timed out"; break; + case WSAECONNRESET: msg = "Connection reset by peer"; break; + case WSAECONNABORTED: msg = "Connection aborted"; break; + } + if (msg) { + strncat(str_, msg, len-1-strlen(str_)); + } else { +#ifdef UNICODE + WCHAR* tmsg = new WCHAR[len-strlen(str_)]; + FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + 0, err, 0, tmsg, len-1-strlen(str_), 0); + WideCharToMultiByte(CP_ACP, 0, tmsg, wcslen(tmsg)+1, + str_+strlen(str_), len-strlen(str_), 0, 0); + delete [] tmsg; +#else + char* currStr = str_+strlen(str_); + FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + 0, err, 0, currStr, len-1-strlen(str_), 0); +#endif + int l = strlen(str_); + if ((l >= 2) && (str_[l-2] == '\r') && (str_[l-1] == '\n')) + str_[l-2] = 0; + } + +#else + strncat(str_, strerror(err), len-1-strlen(str_)); +#endif + strncat(str_, " (", len-1-strlen(str_)); + char buf[20]; +#ifdef WIN32 + if (err < 0) + sprintf(buf, "%x", err); + else +#endif + sprintf(buf,"%d",err); + strncat(str_, buf, len-1-strlen(str_)); + strncat(str_, ")", len-1-strlen(str_)); +} diff --git a/src/vnc/common/rdr/Exception.h b/src/vnc/common/rdr/Exception.h new file mode 100644 index 00000000..1b92f777 --- /dev/null +++ b/src/vnc/common/rdr/Exception.h @@ -0,0 +1,58 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifndef __RDR_EXCEPTION_H__ +#define __RDR_EXCEPTION_H__ + +#include +#include + +namespace rdr { + + struct Exception { + enum { len = 256 }; + char str_[len]; + Exception(const char* s=0) { + str_[0] = 0; + if (s) + strncat(str_, s, len-1); + else + strcat(str_, "Exception"); + } + virtual const char* str() const { return str_; } + }; + + struct SystemException : public Exception { + int err; + SystemException(const char* s, int err_); + }; + + struct TimedOut : public Exception { + TimedOut(const char* s="Timed out") : Exception(s) {} + }; + + struct EndOfStream : public Exception { + EndOfStream(const char* s="End of stream") : Exception(s) {} + }; + + struct FrameException : public Exception { + FrameException(const char* s="Frame exception") : Exception(s) {} + }; +} + +#endif diff --git a/src/vnc/common/rdr/FdInStream.cxx b/src/vnc/common/rdr/FdInStream.cxx new file mode 100644 index 00000000..6496ed5b --- /dev/null +++ b/src/vnc/common/rdr/FdInStream.cxx @@ -0,0 +1,283 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#include +#include +#ifdef _WIN32 +#include +#ifndef _WIN32_WCE +#include +#endif +#define read(s,b,l) recv(s,(char*)b,l,0) +#define close closesocket +#undef errno +#define errno WSAGetLastError() +#undef EINTR +#define EINTR WSAEINTR +#else +#include +#include +#include +#include +#endif + +// XXX should use autoconf HAVE_SYS_SELECT_H +#ifdef _AIX +#include +#endif + +#include +#include + +#pragma warning(disable: 4244) + +using namespace rdr; + +enum { DEFAULT_BUF_SIZE = 8192, + MIN_BULK_SIZE = 1024 }; + +FdInStream::FdInStream(int fd_, int timeoutms_, int bufSize_, + bool closeWhenDone_) + : fd(fd_), closeWhenDone(closeWhenDone_), + timeoutms(timeoutms_), blockCallback(0), + timing(false), timeWaitedIn100us(5), timedKbits(0), + bufSize(bufSize_ ? bufSize_ : DEFAULT_BUF_SIZE), offset(0) +{ + ptr = end = start = new U8[bufSize]; +} + +FdInStream::FdInStream(int fd_, FdInStreamBlockCallback* blockCallback_, + int bufSize_) + : fd(fd_), timeoutms(0), blockCallback(blockCallback_), + timing(false), timeWaitedIn100us(5), timedKbits(0), + bufSize(bufSize_ ? bufSize_ : DEFAULT_BUF_SIZE), offset(0) +{ + ptr = end = start = new U8[bufSize]; +} + +FdInStream::~FdInStream() +{ + delete [] start; + if (closeWhenDone) close(fd); +} + + +void FdInStream::setTimeout(int timeoutms_) { + timeoutms = timeoutms_; +} + +void FdInStream::setBlockCallback(FdInStreamBlockCallback* blockCallback_) +{ + blockCallback = blockCallback_; + timeoutms = 0; +} + +int FdInStream::pos() +{ + return offset + ptr - start; +} + +void FdInStream::readBytes(void* data, int length) +{ + if (length < MIN_BULK_SIZE) { + InStream::readBytes(data, length); + return; + } + + U8* dataPtr = (U8*)data; + + int n = end - ptr; + if (n > length) n = length; + + memcpy(dataPtr, ptr, n); + dataPtr += n; + length -= n; + ptr += n; + + while (length > 0) { + n = readWithTimeoutOrCallback(dataPtr, length); + dataPtr += n; + length -= n; + offset += n; + } +} + + +int FdInStream::overrun(int itemSize, int nItems, bool wait) +{ + if (itemSize > bufSize) + throw Exception("FdInStream overrun: max itemSize exceeded"); + + if (end - ptr != 0) + memmove(start, ptr, end - ptr); + + offset += ptr - start; + end -= ptr - start; + ptr = start; + + while (end < start + itemSize) { + int n = readWithTimeoutOrCallback((U8*)end, start + bufSize - end, wait); + if (n == 0) return 0; + end += n; + } + + if (itemSize * nItems > end - ptr) + nItems = (end - ptr) / itemSize; + + return nItems; +} + +#ifdef _WIN32 +static void gettimeofday(struct timeval* tv, void*) +{ + LARGE_INTEGER counts, countsPerSec; + static double usecPerCount = 0.0; + + if (QueryPerformanceCounter(&counts)) { + if (usecPerCount == 0.0) { + QueryPerformanceFrequency(&countsPerSec); + usecPerCount = 1000000.0 / countsPerSec.QuadPart; + } + + LONGLONG usecs = (LONGLONG)(counts.QuadPart * usecPerCount); + tv->tv_usec = (long)(usecs % 1000000); + tv->tv_sec = (long)(usecs / 1000000); + + } else { +#ifndef _WIN32_WCE + struct timeb tb; + ftime(&tb); + tv->tv_sec = tb.time; + tv->tv_usec = tb.millitm * 1000; +#else + throw SystemException("QueryPerformanceCounter", GetLastError()); +#endif + } +} +#endif + +// +// readWithTimeoutOrCallback() reads up to the given length in bytes from the +// file descriptor into a buffer. If the wait argument is false, then zero is +// returned if no bytes can be read without blocking. Otherwise if a +// blockCallback is set, it will be called (repeatedly) instead of blocking. +// If alternatively there is a timeout set and that timeout expires, it throws +// a TimedOut exception. Otherwise it returns the number of bytes read. It +// never attempts to read() unless select() indicates that the fd is readable - +// this means it can be used on an fd which has been set non-blocking. It also +// has to cope with the annoying possibility of both select() and read() +// returning EINTR. +// + +int FdInStream::readWithTimeoutOrCallback(void* buf, int len, bool wait) +{ + struct timeval before, after; + if (timing) + gettimeofday(&before, 0); + + int n; + while (true) { + do { + fd_set fds; + struct timeval tv; + struct timeval* tvp = &tv; + + if (!wait) { + tv.tv_sec = tv.tv_usec = 0; + } else if (timeoutms != -1) { + tv.tv_sec = timeoutms / 1000; + tv.tv_usec = (timeoutms % 1000) * 1000; + } else { + tvp = 0; + } + + FD_ZERO(&fds); + FD_SET(fd, &fds); + n = select(fd+1, &fds, 0, 0, tvp); + } while (n < 0 && errno == EINTR); + + if (n > 0) break; + if (n < 0) throw SystemException("select",errno); + if (!wait) return 0; + if (!blockCallback) throw TimedOut(); + + blockCallback->blockCallback(); + } + + do { + n = ::read(fd, buf, len); + } while (n < 0 && errno == EINTR); + + if (n < 0) throw SystemException("read",errno); + if (n == 0) throw EndOfStream(); + + if (timing) { + gettimeofday(&after, 0); +// fprintf(stderr,"%d.%06d\n",(after.tv_sec - before.tv_sec), +// (after.tv_usec - before.tv_usec)); + int newTimeWaited = ((after.tv_sec - before.tv_sec) * 10000 + + (after.tv_usec - before.tv_usec) / 100); + int newKbits = n * 8 / 1000; + +// if (newTimeWaited == 0) { +// fprintf(stderr,"new kbps infinite t %d k %d\n", +// newTimeWaited, newKbits); +// } else { +// fprintf(stderr,"new kbps %d t %d k %d\n", +// newKbits * 10000 / newTimeWaited, newTimeWaited, newKbits); +// } + + // limit rate to between 10kbit/s and 40Mbit/s + + if (newTimeWaited > newKbits*1000) newTimeWaited = newKbits*1000; + if (newTimeWaited < newKbits/4) newTimeWaited = newKbits/4; + + timeWaitedIn100us += newTimeWaited; + timedKbits += newKbits; + } + + return n; +} + +void FdInStream::startTiming() +{ + timing = true; + + // Carry over up to 1s worth of previous rate for smoothing. + + if (timeWaitedIn100us > 10000) { + timedKbits = timedKbits * 10000 / timeWaitedIn100us; + timeWaitedIn100us = 10000; + } +} + +void FdInStream::stopTiming() +{ + timing = false; + if (timeWaitedIn100us < timedKbits/2) + timeWaitedIn100us = timedKbits/2; // upper limit 20Mbit/s +} + +unsigned int FdInStream::kbitsPerSecond() +{ + // The following calculation will overflow 32-bit arithmetic if we have + // received more than about 50Mbytes (400Mbits) since we started timing, so + // it should be OK for a single RFB update. + + return timedKbits * 10000 / timeWaitedIn100us; +} diff --git a/src/vnc/common/rdr/FdInStream.h b/src/vnc/common/rdr/FdInStream.h new file mode 100644 index 00000000..5d9598c8 --- /dev/null +++ b/src/vnc/common/rdr/FdInStream.h @@ -0,0 +1,77 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// +// FdInStream streams from a file descriptor. +// + +#ifndef __RDR_FDINSTREAM_H__ +#define __RDR_FDINSTREAM_H__ + +#include + +namespace rdr { + + class FdInStreamBlockCallback { + public: + virtual void blockCallback() = 0; + }; + + class FdInStream : public InStream { + + public: + + FdInStream(int fd, int timeoutms=-1, int bufSize=0, + bool closeWhenDone_=false); + FdInStream(int fd, FdInStreamBlockCallback* blockCallback, int bufSize=0); + virtual ~FdInStream(); + + void setTimeout(int timeoutms); + void setBlockCallback(FdInStreamBlockCallback* blockCallback); + int getFd() { return fd; } + int pos(); + void readBytes(void* data, int length); + + void startTiming(); + void stopTiming(); + unsigned int kbitsPerSecond(); + unsigned int timeWaited() { return timeWaitedIn100us; } + + protected: + int overrun(int itemSize, int nItems, bool wait); + + private: + int readWithTimeoutOrCallback(void* buf, int len, bool wait=true); + + int fd; + bool closeWhenDone; + int timeoutms; + FdInStreamBlockCallback* blockCallback; + + bool timing; + unsigned int timeWaitedIn100us; + unsigned int timedKbits; + + int bufSize; + int offset; + U8* start; + }; + +} // end of namespace rdr + +#endif diff --git a/src/vnc/common/rdr/FdOutStream.cxx b/src/vnc/common/rdr/FdOutStream.cxx new file mode 100644 index 00000000..bfe1d36a --- /dev/null +++ b/src/vnc/common/rdr/FdOutStream.cxx @@ -0,0 +1,174 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#include +#include +#ifdef _WIN32 +#include +#define write(s,b,l) send(s,(const char*)b,l,0) +#define EWOULDBLOCK WSAEWOULDBLOCK +#undef errno +#define errno WSAGetLastError() +#undef EINTR +#define EINTR WSAEINTR +#else +#include +#include +#include +#include +#endif + +#include +#include + + +using namespace rdr; + +enum { DEFAULT_BUF_SIZE = 16384, + MIN_BULK_SIZE = 1024 }; + +FdOutStream::FdOutStream(int fd_, int timeoutms_, int bufSize_) + : fd(fd_), timeoutms(timeoutms_), + bufSize(bufSize_ ? bufSize_ : DEFAULT_BUF_SIZE), offset(0) +{ + ptr = start = new U8[bufSize]; + end = start + bufSize; +} + +FdOutStream::~FdOutStream() +{ + try { + flush(); + } catch (Exception&) { + } + delete [] start; +} + +void FdOutStream::setTimeout(int timeoutms_) { + timeoutms = timeoutms_; +} + +void FdOutStream::writeBytes(const void* data, int length) +{ + if (length < MIN_BULK_SIZE) { + OutStream::writeBytes(data, length); + return; + } + + const U8* dataPtr = (const U8*)data; + + flush(); + + while (length > 0) { + int n = writeWithTimeout(dataPtr, length); + length -= n; + dataPtr += n; + offset += n; + } +} + +int FdOutStream::length() +{ + return offset + ptr - start; +} + +void FdOutStream::flush() +{ + U8* sentUpTo = start; + while (sentUpTo < ptr) { + int n = writeWithTimeout((const void*) sentUpTo, ptr - sentUpTo); + sentUpTo += n; + offset += n; + } + + ptr = start; +} + + +int FdOutStream::overrun(int itemSize, int nItems) +{ + if (itemSize > bufSize) + throw Exception("FdOutStream overrun: max itemSize exceeded"); + + flush(); + + if (itemSize * nItems > end - ptr) + nItems = (end - ptr) / itemSize; + + return nItems; +} + +// +// writeWithTimeout() writes up to the given length in bytes from the given +// buffer to the file descriptor. If there is a timeout set and that timeout +// expires, it throws a TimedOut exception. Otherwise it returns the number of +// bytes written. It never attempts to write() unless select() indicates that +// the fd is writable - this means it can be used on an fd which has been set +// non-blocking. It also has to cope with the annoying possibility of both +// select() and write() returning EINTR. +// + +int FdOutStream::writeWithTimeout(const void* data, int length) +{ + int n; + + do { + + do { + fd_set fds; + struct timeval tv; + struct timeval* tvp = &tv; + + if (timeoutms != -1) { + tv.tv_sec = timeoutms / 1000; + tv.tv_usec = (timeoutms % 1000) * 1000; + } else { + tvp = 0; + } + + FD_ZERO(&fds); + FD_SET(fd, &fds); +#ifdef _WIN32_WCE + // NB: This fixes a broken Winsock2 select() behaviour. select() + // never returns for non-blocking sockets, unless they're already + // ready to be written to... + u_long zero = 0; ioctlsocket(fd, FIONBIO, &zero); +#endif + n = select(fd+1, 0, &fds, 0, tvp); +#ifdef _WIN32_WCE + u_long one = 0; ioctlsocket(fd, FIONBIO, &one); +#endif + } while (n < 0 && errno == EINTR); + + if (n < 0) throw SystemException("select",errno); + + if (n == 0) throw TimedOut(); + + do { + n = ::write(fd, data, length); + } while (n < 0 && (errno == EINTR)); + + // NB: This outer loop simply fixes a broken Winsock2 EWOULDBLOCK + // condition, found only under Win98 (first edition), with slow + // network connections. Should in fact never ever happen... + } while (n < 0 && (errno == EWOULDBLOCK)); + + if (n < 0) throw SystemException("write",errno); + + return n; +} diff --git a/src/vnc/common/rdr/FdOutStream.h b/src/vnc/common/rdr/FdOutStream.h new file mode 100644 index 00000000..f75917cd --- /dev/null +++ b/src/vnc/common/rdr/FdOutStream.h @@ -0,0 +1,56 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// +// FdOutStream streams to a file descriptor. +// + +#ifndef __RDR_FDOUTSTREAM_H__ +#define __RDR_FDOUTSTREAM_H__ + +#include + +namespace rdr { + + class FdOutStream : public OutStream { + + public: + + FdOutStream(int fd, int timeoutms=-1, int bufSize=0); + virtual ~FdOutStream(); + + void setTimeout(int timeoutms); + int getFd() { return fd; } + + void flush(); + int length(); + void writeBytes(const void* data, int length); + + private: + int overrun(int itemSize, int nItems); + int writeWithTimeout(const void* data, int length); + int fd; + int timeoutms; + int bufSize; + int offset; + U8* start; + }; + +} + +#endif diff --git a/src/vnc/common/rdr/FixedMemOutStream.h b/src/vnc/common/rdr/FixedMemOutStream.h new file mode 100644 index 00000000..e4ec52cb --- /dev/null +++ b/src/vnc/common/rdr/FixedMemOutStream.h @@ -0,0 +1,52 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// +// A FixedMemOutStream writes to a buffer of a fixed length. +// + +#ifndef __RDR_FIXEDMEMOUTSTREAM_H__ +#define __RDR_FIXEDMEMOUTSTREAM_H__ + +#include +#include + +namespace rdr { + + class FixedMemOutStream : public OutStream { + + public: + + FixedMemOutStream(void* buf, int len) { + ptr = start = (U8*)buf; + end = start + len; + } + + int length() { return ptr - start; } + void reposition(int pos) { ptr = start + pos; } + const void* data() { return (const void*)start; } + + private: + + int overrun(int itemSize, int nItems) { throw EndOfStream(); } + U8* start; + }; + +} + +#endif diff --git a/src/vnc/common/rdr/HexInStream.cxx b/src/vnc/common/rdr/HexInStream.cxx new file mode 100644 index 00000000..80f8a796 --- /dev/null +++ b/src/vnc/common/rdr/HexInStream.cxx @@ -0,0 +1,117 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#include +#include + +#include +#include + +using namespace rdr; + +const int DEFAULT_BUF_LEN = 16384; + +static inline int min(int a, int b) {return a= '0') && (c <= '9')) + *v = (*v << 4) + (c - '0'); + else if ((c >= 'a') && (c <= 'f')) + *v = (*v << 4) + (c - 'a' + 10); + else + return false; + return true; +} + +bool HexInStream::hexStrToBin(const char* s, char** data, int* length) { + int l=strlen(s); + if ((l % 2) == 0) { + delete [] *data; + *data = 0; *length = 0; + if (l == 0) + return true; + *data = new char[l/2]; + *length = l/2; + for(int i=0;i bufSize) + throw Exception("HexInStream overrun: max itemSize exceeded"); + + if (end - ptr != 0) + memmove(start, ptr, end - ptr); + + end -= ptr - start; + offset += ptr - start; + ptr = start; + + while (end < ptr + itemSize) { + int n = in_stream.check(2, 1, wait); + if (n == 0) return 0; + const U8* iptr = in_stream.getptr(); + const U8* eptr = in_stream.getend(); + int length = min((eptr - iptr)/2, start + bufSize - end); + + U8* optr = (U8*) end; + for (int i=0; i end - ptr) + nItems = (end - ptr) / itemSize; + + return nItems; +} diff --git a/src/vnc/common/rdr/HexInStream.h b/src/vnc/common/rdr/HexInStream.h new file mode 100644 index 00000000..6bfb8433 --- /dev/null +++ b/src/vnc/common/rdr/HexInStream.h @@ -0,0 +1,50 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifndef __RDR_HEX_INSTREAM_H__ +#define __RDR_HEX_INSTREAM_H__ + +#include + +namespace rdr { + + class HexInStream : public InStream { + public: + + HexInStream(InStream& is, int bufSize=0); + virtual ~HexInStream(); + + int pos(); + + static bool readHexAndShift(char c, int* v); + static bool hexStrToBin(const char* s, char** data, int* length); + + protected: + int overrun(int itemSize, int nItems, bool wait); + + private: + int bufSize; + U8* start; + int offset; + + InStream& in_stream; + }; + +} // end of namespace rdr + +#endif diff --git a/src/vnc/common/rdr/HexOutStream.cxx b/src/vnc/common/rdr/HexOutStream.cxx new file mode 100644 index 00000000..9b0b6c4d --- /dev/null +++ b/src/vnc/common/rdr/HexOutStream.cxx @@ -0,0 +1,110 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#include +#include + +using namespace rdr; + +const int DEFAULT_BUF_LEN = 16384; + +static inline int min(int a, int b) {return a=0) && (i<=9)) + return '0'+i; + else if ((i>=10) && (i<=15)) + return 'a'+(i-10); + else + throw rdr::Exception("intToHex failed"); +} + +char* HexOutStream::binToHexStr(const char* data, int length) { + char* buffer = new char[length*2+1]; + for (int i=0; i> 4) & 15); + buffer[i*2+1] = intToHex((data[i] & 15)); + if (!buffer[i*2] || !buffer[i*2+1]) { + delete [] buffer; + return 0; + } + } + buffer[length*2] = 0; + return buffer; +} + + +void +HexOutStream::writeBuffer() { + U8* pos = start; + while (pos != ptr) { + out_stream.check(2); + U8* optr = out_stream.getptr(); + U8* oend = out_stream.getend(); + int length = min(ptr-pos, (oend-optr)/2); + + for (int i=0; i> 4) & 0xf); + optr[i*2+1] = intToHex(pos[i] & 0xf); + } + + out_stream.setptr(optr + length*2); + pos += length; + } + offset += ptr - start; + ptr = start; +} + +int HexOutStream::length() +{ + return offset + ptr - start; +} + +void +HexOutStream::flush() { + writeBuffer(); + out_stream.flush(); +} + +int +HexOutStream::overrun(int itemSize, int nItems) { + if (itemSize > bufSize) + throw Exception("HexOutStream overrun: max itemSize exceeded"); + + writeBuffer(); + + if (itemSize * nItems > end - ptr) + nItems = (end - ptr) / itemSize; + + return nItems; +} + diff --git a/src/vnc/common/rdr/HexOutStream.h b/src/vnc/common/rdr/HexOutStream.h new file mode 100644 index 00000000..10247e68 --- /dev/null +++ b/src/vnc/common/rdr/HexOutStream.h @@ -0,0 +1,51 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifndef __RDR_HEX_OUTSTREAM_H__ +#define __RDR_HEX_OUTSTREAM_H__ + +#include + +namespace rdr { + + class HexOutStream : public OutStream { + public: + + HexOutStream(OutStream& os, int buflen=0); + virtual ~HexOutStream(); + + void flush(); + int length(); + + static char intToHex(int i); + static char* binToHexStr(const char* data, int length); + + private: + void writeBuffer(); + int overrun(int itemSize, int nItems); + + OutStream& out_stream; + + U8* start; + int offset; + int bufSize; + }; + +} + +#endif diff --git a/src/vnc/common/rdr/InStream.cxx b/src/vnc/common/rdr/InStream.cxx new file mode 100644 index 00000000..a413b6c1 --- /dev/null +++ b/src/vnc/common/rdr/InStream.cxx @@ -0,0 +1,35 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#include +#include + +using namespace rdr; + +U32 InStream::maxStringLength = 65535; + +char* InStream::readString() +{ + U32 len = readU32(); + if (len > maxStringLength) + throw Exception("InStream max string length exceeded"); + char* str = new char[len+1]; + readBytes(str, len); + str[len] = 0; + return str; +} diff --git a/src/vnc/common/rdr/InStream.h b/src/vnc/common/rdr/InStream.h new file mode 100644 index 00000000..97648fb4 --- /dev/null +++ b/src/vnc/common/rdr/InStream.h @@ -0,0 +1,153 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// +// rdr::InStream marshalls data from a buffer stored in RDR (RFB Data +// Representation). +// + +#ifndef __RDR_INSTREAM_H__ +#define __RDR_INSTREAM_H__ + +#include +#include // for memcpy + +namespace rdr { + + class InStream { + + public: + + virtual ~InStream() {} + + // check() ensures there is buffer data for at least one item of size + // itemSize bytes. Returns the number of items in the buffer (up to a + // maximum of nItems). If wait is false, then instead of blocking to wait + // for the bytes, zero is returned if the bytes are not immediately + // available. + + inline int check(int itemSize, int nItems=1, bool wait=true) + { + if (ptr + itemSize * nItems > end) { + if (ptr + itemSize > end) + return overrun(itemSize, nItems, wait); + + nItems = (end - ptr) / itemSize; + } + return nItems; + } + + // checkNoWait() tries to make sure that the given number of bytes can + // be read without blocking. It returns true if this is the case, false + // otherwise. The length must be "small" (less than the buffer size). + + inline bool checkNoWait(int length) { return check(length, 1, false)!=0; } + + // readU/SN() methods read unsigned and signed N-bit integers. + + inline U8 readU8() { check(1); return *ptr++; } + inline U16 readU16() { check(2); int b0 = *ptr++; int b1 = *ptr++; + return b0 << 8 | b1; } + inline U32 readU32() { check(4); int b0 = *ptr++; int b1 = *ptr++; + int b2 = *ptr++; int b3 = *ptr++; + return b0 << 24 | b1 << 16 | b2 << 8 | b3; } + + inline S8 readS8() { return (S8) readU8(); } + inline S16 readS16() { return (S16)readU16(); } + inline S32 readS32() { return (S32)readU32(); } + + // readString() reads a string - a U32 length followed by the data. + // Returns a null-terminated string - the caller should delete[] it + // afterwards. + + char* readString(); + + // maxStringLength protects against allocating a huge buffer. Set it + // higher if you need longer strings. + + static U32 maxStringLength; + + inline void skip(int bytes) { + while (bytes > 0) { + int n = check(1, bytes); + ptr += n; + bytes -= n; + } + } + + // readBytes() reads an exact number of bytes. + + virtual void readBytes(void* data, int length) { + U8* dataPtr = (U8*)data; + U8* dataEnd = dataPtr + length; + while (dataPtr < dataEnd) { + int n = check(1, dataEnd - dataPtr); + memcpy(dataPtr, ptr, n); + ptr += n; + dataPtr += n; + } + } + + // readOpaqueN() reads a quantity without byte-swapping. + + inline U8 readOpaque8() { return readU8(); } + inline U16 readOpaque16() { check(2); U16 r; ((U8*)&r)[0] = *ptr++; + ((U8*)&r)[1] = *ptr++; return r; } + inline U32 readOpaque32() { check(4); U32 r; ((U8*)&r)[0] = *ptr++; + ((U8*)&r)[1] = *ptr++; ((U8*)&r)[2] = *ptr++; + ((U8*)&r)[3] = *ptr++; return r; } + inline U32 readOpaque24A() { check(3); U32 r=0; ((U8*)&r)[0] = *ptr++; + ((U8*)&r)[1] = *ptr++; ((U8*)&r)[2] = *ptr++; + return r; } + inline U32 readOpaque24B() { check(3); U32 r=0; ((U8*)&r)[1] = *ptr++; + ((U8*)&r)[2] = *ptr++; ((U8*)&r)[3] = *ptr++; + return r; } + + // pos() returns the position in the stream. + + virtual int pos() = 0; + + // getptr(), getend() and setptr() are "dirty" methods which allow you to + // manipulate the buffer directly. This is useful for a stream which is a + // wrapper around an underlying stream. + + inline const U8* getptr() const { return ptr; } + inline const U8* getend() const { return end; } + inline void setptr(const U8* p) { ptr = p; } + + private: + + // overrun() is implemented by a derived class to cope with buffer overrun. + // It ensures there are at least itemSize bytes of buffer data. Returns + // the number of items in the buffer (up to a maximum of nItems). itemSize + // is supposed to be "small" (a few bytes). If wait is false, then + // instead of blocking to wait for the bytes, zero is returned if the bytes + // are not immediately available. + + virtual int overrun(int itemSize, int nItems, bool wait=true) = 0; + + protected: + + InStream() {} + const U8* ptr; + const U8* end; + }; + +} + +#endif diff --git a/src/vnc/common/rdr/Makefile.in b/src/vnc/common/rdr/Makefile.in new file mode 100644 index 00000000..09fa5544 --- /dev/null +++ b/src/vnc/common/rdr/Makefile.in @@ -0,0 +1,19 @@ + +SRCS = Exception.cxx FdInStream.cxx FdOutStream.cxx InStream.cxx \ + RandomStream.cxx ZlibInStream.cxx ZlibOutStream.cxx \ + HexInStream.cxx HexOutStream.cxx + +OBJS = $(SRCS:.cxx=.o) + +DIR_CPPFLAGS = -I$(top_srcdir) @ZLIB_INCLUDE@ + +library = librdr.a + +all:: $(library) + +$(library): $(OBJS) + rm -f $(library) + $(AR) $(library) $(OBJS) + $(RANLIB) $(library) + +# followed by boilerplate.mk diff --git a/src/vnc/common/rdr/MemInStream.h b/src/vnc/common/rdr/MemInStream.h new file mode 100644 index 00000000..77ca3f3a --- /dev/null +++ b/src/vnc/common/rdr/MemInStream.h @@ -0,0 +1,63 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// +// rdr::MemInStream is an InStream which streams from a given memory buffer. +// If the deleteWhenDone parameter is true then the buffer will be delete[]d in +// the destructor. Note that it is delete[]d as a U8* - strictly speaking this +// means it ought to be new[]ed as a U8* as well, but on most platforms this +// doesn't matter. +// + +#ifndef __RDR_MEMINSTREAM_H__ +#define __RDR_MEMINSTREAM_H__ + +#include +#include + +namespace rdr { + + class MemInStream : public InStream { + + public: + + MemInStream(const void* data, int len, bool deleteWhenDone_=false) + : start((const U8*)data), deleteWhenDone(deleteWhenDone_) + { + ptr = start; + end = start + len; + } + + virtual ~MemInStream() { + if (deleteWhenDone) + delete [] (U8*)start; + } + + int pos() { return ptr - start; } + void reposition(int pos) { ptr = start + pos; } + + private: + + int overrun(int itemSize, int nItems, bool wait) { throw EndOfStream(); } + const U8* start; + bool deleteWhenDone; + }; + +} + +#endif diff --git a/src/vnc/common/rdr/MemOutStream.h b/src/vnc/common/rdr/MemOutStream.h new file mode 100644 index 00000000..ee3e9500 --- /dev/null +++ b/src/vnc/common/rdr/MemOutStream.h @@ -0,0 +1,83 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// +// A MemOutStream grows as needed when data is written to it. +// + +#ifndef __RDR_MEMOUTSTREAM_H__ +#define __RDR_MEMOUTSTREAM_H__ + +#include + +namespace rdr { + + class MemOutStream : public OutStream { + + public: + + MemOutStream(int len=1024) { + start = ptr = new U8[len]; + end = start + len; + } + + virtual ~MemOutStream() { + delete [] start; + } + + void writeBytes(const void* data, int length) { + check(length); + memcpy(ptr, data, length); + ptr += length; + } + + int length() { return ptr - start; } + void clear() { ptr = start; }; + void clearAndZero() { memset(start, 0, ptr-start); clear(); } + void reposition(int pos) { ptr = start + pos; } + + // data() returns a pointer to the buffer. + + const void* data() { return (const void*)start; } + + private: + + // overrun() either doubles the buffer or adds enough space for nItems of + // size itemSize bytes. + + int overrun(int itemSize, int nItems) { + int len = ptr - start + itemSize * nItems; + if (len < (end - start) * 2) + len = (end - start) * 2; + + U8* newStart = new U8[len]; + memcpy(newStart, start, ptr - start); + ptr = newStart + (ptr - start); + delete [] start; + start = newStart; + end = newStart + len; + + return nItems; + } + + U8* start; + }; + +} + +#endif diff --git a/src/vnc/common/rdr/OutStream.h b/src/vnc/common/rdr/OutStream.h new file mode 100644 index 00000000..35c699dc --- /dev/null +++ b/src/vnc/common/rdr/OutStream.h @@ -0,0 +1,152 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// +// rdr::OutStream marshalls data into a buffer stored in RDR (RFB Data +// Representation). +// + +#ifndef __RDR_OUTSTREAM_H__ +#define __RDR_OUTSTREAM_H__ + +#include +#include // for memcpy + +namespace rdr { + + class OutStream { + + protected: + + OutStream() {} + + public: + + virtual ~OutStream() {} + + // check() ensures there is buffer space for at least one item of size + // itemSize bytes. Returns the number of items which fit (up to a maximum + // of nItems). + + inline int check(int itemSize, int nItems=1) + { + if (ptr + itemSize * nItems > end) { + if (ptr + itemSize > end) + return overrun(itemSize, nItems); + + nItems = (end - ptr) / itemSize; + } + return nItems; + } + + // writeU/SN() methods write unsigned and signed N-bit integers. + + inline void writeU8( U8 u) { check(1); *ptr++ = u; } + inline void writeU16(U16 u) { check(2); *ptr++ = u >> 8; *ptr++ = (U8)u; } + inline void writeU32(U32 u) { check(4); *ptr++ = u >> 24; *ptr++ = u >> 16; + *ptr++ = u >> 8; *ptr++ = u; } + + inline void writeS8( S8 s) { writeU8((U8)s); } + inline void writeS16(S16 s) { writeU16((U16)s); } + inline void writeS32(S32 s) { writeU32((U32)s); } + + // writeString() writes a string - a U32 length followed by the data. The + // given string should be null-terminated (but the terminating null is not + // written to the stream). + + inline void writeString(const char* str) { + U32 len = strlen(str); + writeU32(len); + writeBytes(str, len); + } + + inline void pad(int bytes) { + while (bytes-- > 0) writeU8(0); + } + + inline void skip(int bytes) { + while (bytes > 0) { + int n = check(1, bytes); + ptr += n; + bytes -= n; + } + } + + // writeBytes() writes an exact number of bytes. + + virtual void writeBytes(const void* data, int length) { + const U8* dataPtr = (const U8*)data; + const U8* dataEnd = dataPtr + length; + while (dataPtr < dataEnd) { + int n = check(1, dataEnd - dataPtr); + memcpy(ptr, dataPtr, n); + ptr += n; + dataPtr += n; + } + } + + // writeOpaqueN() writes a quantity without byte-swapping. + + inline void writeOpaque8( U8 u) { writeU8(u); } + inline void writeOpaque16(U16 u) { check(2); *ptr++ = ((U8*)&u)[0]; + *ptr++ = ((U8*)&u)[1]; } + inline void writeOpaque32(U32 u) { check(4); *ptr++ = ((U8*)&u)[0]; + *ptr++ = ((U8*)&u)[1]; + *ptr++ = ((U8*)&u)[2]; + *ptr++ = ((U8*)&u)[3]; } + inline void writeOpaque24A(U32 u) { check(3); *ptr++ = ((U8*)&u)[0]; + *ptr++ = ((U8*)&u)[1]; + *ptr++ = ((U8*)&u)[2]; } + inline void writeOpaque24B(U32 u) { check(3); *ptr++ = ((U8*)&u)[1]; + *ptr++ = ((U8*)&u)[2]; + *ptr++ = ((U8*)&u)[3]; } + + // length() returns the length of the stream. + + virtual int length() = 0; + + // flush() requests that the stream be flushed. + + virtual void flush() {} + + // getptr(), getend() and setptr() are "dirty" methods which allow you to + // manipulate the buffer directly. This is useful for a stream which is a + // wrapper around an underlying stream. + + inline U8* getptr() { return ptr; } + inline U8* getend() { return end; } + inline void setptr(U8* p) { ptr = p; } + + private: + + // overrun() is implemented by a derived class to cope with buffer overrun. + // It ensures there are at least itemSize bytes of buffer space. Returns + // the number of items which fit (up to a maximum of nItems). itemSize is + // supposed to be "small" (a few bytes). + + virtual int overrun(int itemSize, int nItems) = 0; + + protected: + + U8* ptr; + U8* end; + }; + +} + +#endif diff --git a/src/vnc/common/rdr/RandomStream.cxx b/src/vnc/common/rdr/RandomStream.cxx new file mode 100644 index 00000000..7056c2cc --- /dev/null +++ b/src/vnc/common/rdr/RandomStream.cxx @@ -0,0 +1,130 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#include +#include +#include +#include +#ifndef WIN32 +#include +#include +#else +#define getpid() GetCurrentProcessId() +#ifndef RFB_HAVE_WINCRYPT +#pragma message(" NOTE: Not building WinCrypt-based RandomStream") +#endif +#endif + +using namespace rdr; + +const int DEFAULT_BUF_LEN = 256; + +unsigned int RandomStream::seed; + +RandomStream::RandomStream() + : offset(0) +{ + ptr = end = start = new U8[DEFAULT_BUF_LEN]; + +#ifdef RFB_HAVE_WINCRYPT + provider = 0; + if (!CryptAcquireContext(&provider, 0, 0, PROV_RSA_FULL, 0)) { + if (GetLastError() == NTE_BAD_KEYSET) { + if (!CryptAcquireContext(&provider, 0, 0, PROV_RSA_FULL, CRYPT_NEWKEYSET)) { + fprintf(stderr, "RandomStream: unable to create keyset\n"); + provider = 0; + } + } else { + fprintf(stderr, "RandomStream: unable to acquire context\n"); + provider = 0; + } + } + if (!provider) { +#else +#ifndef WIN32 + fp = fopen("/dev/urandom", "r"); + if (!fp) + fp = fopen("/dev/random", "r"); + if (!fp) { +#else + { +#endif +#endif + fprintf(stderr,"RandomStream: warning: no OS supplied random source - using rand()\n"); + seed += (unsigned int) time(0) + getpid() + getpid() * 987654 + rand(); + srand(seed); + } +} + +RandomStream::~RandomStream() { + delete [] start; + +#ifdef RFB_HAVE_WINCRYPT + if (provider) + CryptReleaseContext(provider, 0); +#endif +#ifndef WIN32 + if (fp) fclose(fp); +#endif +} + +int RandomStream::pos() { + return offset + ptr - start; +} + +int RandomStream::overrun(int itemSize, int nItems, bool wait) { + if (itemSize > DEFAULT_BUF_LEN) + throw Exception("RandomStream overrun: max itemSize exceeded"); + + if (end - ptr != 0) + memmove(start, ptr, end - ptr); + + end -= ptr - start; + offset += ptr - start; + ptr = start; + + int length = start + DEFAULT_BUF_LEN - end; + +#ifdef RFB_HAVE_WINCRYPT + if (provider) { + if (!CryptGenRandom(provider, length, (U8*)end)) + throw rdr::SystemException("unable to CryptGenRandom", GetLastError()); + end += length; + } else { +#else +#ifndef WIN32 + if (fp) { + int n = fread((U8*)end, length, 1, fp); + if (n != 1) + throw rdr::SystemException("reading /dev/urandom or /dev/random failed", + errno); + end += length; + } else { +#else + { +#endif +#endif + for (int i=0; i end - ptr) + nItems = (end - ptr) / itemSize; + + return nItems; +} diff --git a/src/vnc/common/rdr/RandomStream.h b/src/vnc/common/rdr/RandomStream.h new file mode 100644 index 00000000..c33360d7 --- /dev/null +++ b/src/vnc/common/rdr/RandomStream.h @@ -0,0 +1,63 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifndef __RDR_RANDOMSTREAM_H__ +#define __RDR_RANDOMSTREAM_H__ + +#include +#include + +#ifdef WIN32 +#include +#include +#ifdef WINCRYPT32API +#define RFB_HAVE_WINCRYPT +#endif +#endif + +namespace rdr { + + class RandomStream : public InStream { + + public: + + RandomStream(); + virtual ~RandomStream(); + + int pos(); + + protected: + int overrun(int itemSize, int nItems, bool wait); + + private: + U8* start; + int offset; + + static unsigned int seed; +#ifdef RFB_HAVE_WINCRYPT + HCRYPTPROV provider; +#endif +#ifndef WIN32 + FILE* fp; +#endif + + }; + +} // end of namespace rdr + +#endif diff --git a/src/vnc/common/rdr/SubstitutingInStream.h b/src/vnc/common/rdr/SubstitutingInStream.h new file mode 100644 index 00000000..325b01c5 --- /dev/null +++ b/src/vnc/common/rdr/SubstitutingInStream.h @@ -0,0 +1,102 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifndef __RDR_SUBSTITUTINGINSTREAM_H__ +#define __RDR_SUBSTITUTINGINSTREAM_H__ + +#include +#include + +namespace rdr { + + class Substitutor { + public: + virtual char* substitute(const char* varName) = 0; + }; + + class SubstitutingInStream : public InStream { + public: + SubstitutingInStream(InStream* underlying_, Substitutor* s, + int maxVarNameLen_) + : underlying(underlying_), dollar(0), substitutor(s), subst(0), + maxVarNameLen(maxVarNameLen_) + { + ptr = end = underlying->getptr(); + varName = new char[maxVarNameLen+1]; + } + ~SubstitutingInStream() { + delete underlying; + delete [] varName; + delete [] subst; + } + + int pos() { return underlying->pos(); } + + virtual int overrun(int itemSize, int nItems, bool wait=true) { + if (itemSize != 1) + throw new rdr::Exception("SubstitutingInStream: itemSize must be 1"); + + if (subst) { + delete [] subst; + subst = 0; + } else { + underlying->setptr(ptr); + } + + underlying->check(1); + ptr = underlying->getptr(); + end = underlying->getend(); + dollar = (const U8*)memchr(ptr, '$', end-ptr); + if (dollar) { + if (dollar == ptr) { + try { + int i = 0; + while (i < maxVarNameLen) { + varName[i++] = underlying->readS8(); + varName[i] = 0; + subst = substitutor->substitute(varName); + if (subst) { + ptr = (U8*)subst; + end = (U8*)subst + strlen(subst); + break; + } + } + } catch (EndOfStream&) { + } + + if (!subst) + dollar = (const U8*)memchr(ptr+1, '$', end-ptr-1); + } + if (!subst && dollar) end = dollar; + } + + if (itemSize * nItems > end - ptr) + nItems = (end - ptr) / itemSize; + + return nItems; + } + + InStream* underlying; + const U8* dollar; + Substitutor* substitutor; + char* varName; + char* subst; + int maxVarNameLen; + }; +} +#endif diff --git a/src/vnc/common/rdr/ZlibInStream.cxx b/src/vnc/common/rdr/ZlibInStream.cxx new file mode 100644 index 00000000..6f3a7d02 --- /dev/null +++ b/src/vnc/common/rdr/ZlibInStream.cxx @@ -0,0 +1,125 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#include +#include +#include + +using namespace rdr; + +enum { DEFAULT_BUF_SIZE = 16384 }; + +ZlibInStream::ZlibInStream(int bufSize_) + : underlying(0), bufSize(bufSize_ ? bufSize_ : DEFAULT_BUF_SIZE), offset(0), + bytesIn(0) +{ + zs = new z_stream; + zs->zalloc = Z_NULL; + zs->zfree = Z_NULL; + zs->opaque = Z_NULL; + zs->next_in = Z_NULL; + zs->avail_in = 0; + if (inflateInit(zs) != Z_OK) { + delete zs; + throw Exception("ZlibInStream: inflateInit failed"); + } + ptr = end = start = new U8[bufSize]; +} + +ZlibInStream::~ZlibInStream() +{ + delete [] start; + inflateEnd(zs); + delete zs; +} + +void ZlibInStream::setUnderlying(InStream* is, int bytesIn_) +{ + underlying = is; + bytesIn = bytesIn_; + ptr = end = start; +} + +int ZlibInStream::pos() +{ + return offset + ptr - start; +} + +void ZlibInStream::reset() +{ + ptr = end = start; + if (!underlying) return; + + while (bytesIn > 0) { + decompress(true); + end = start; // throw away any data + } + underlying = 0; +} + +int ZlibInStream::overrun(int itemSize, int nItems, bool wait) +{ + if (itemSize > bufSize) + throw Exception("ZlibInStream overrun: max itemSize exceeded"); + if (!underlying) + throw Exception("ZlibInStream overrun: no underlying stream"); + + if (end - ptr != 0) + memmove(start, ptr, end - ptr); + + offset += ptr - start; + end -= ptr - start; + ptr = start; + + while (end - ptr < itemSize) { + if (!decompress(wait)) + return 0; + } + + if (itemSize * nItems > end - ptr) + nItems = (end - ptr) / itemSize; + + return nItems; +} + +// decompress() calls the decompressor once. Note that this won't necessarily +// generate any output data - it may just consume some input data. Returns +// false if wait is false and we would block on the underlying stream. + +bool ZlibInStream::decompress(bool wait) +{ + zs->next_out = (U8*)end; + zs->avail_out = start + bufSize - end; + + int n = underlying->check(1, 1, wait); + if (n == 0) return false; + zs->next_in = (U8*)underlying->getptr(); + zs->avail_in = underlying->getend() - underlying->getptr(); + if ((int)zs->avail_in > bytesIn) + zs->avail_in = bytesIn; + + int rc = inflate(zs, Z_SYNC_FLUSH); + if (rc != Z_OK) { + throw Exception("ZlibInStream: inflate failed"); + } + + bytesIn -= zs->next_in - underlying->getptr(); + end = zs->next_out; + underlying->setptr(zs->next_in); + return true; +} diff --git a/src/vnc/common/rdr/ZlibInStream.h b/src/vnc/common/rdr/ZlibInStream.h new file mode 100644 index 00000000..c26b6d63 --- /dev/null +++ b/src/vnc/common/rdr/ZlibInStream.h @@ -0,0 +1,59 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// +// ZlibInStream streams from a compressed data stream ("underlying"), +// decompressing with zlib on the fly. +// + +#ifndef __RDR_ZLIBINSTREAM_H__ +#define __RDR_ZLIBINSTREAM_H__ + +#include + +struct z_stream_s; + +namespace rdr { + + class ZlibInStream : public InStream { + + public: + + ZlibInStream(int bufSize=0); + virtual ~ZlibInStream(); + + void setUnderlying(InStream* is, int bytesIn); + void reset(); + int pos(); + + private: + + int overrun(int itemSize, int nItems, bool wait); + bool decompress(bool wait); + + InStream* underlying; + int bufSize; + int offset; + z_stream_s* zs; + int bytesIn; + U8* start; + }; + +} // end of namespace rdr + +#endif diff --git a/src/vnc/common/rdr/ZlibOutStream.cxx b/src/vnc/common/rdr/ZlibOutStream.cxx new file mode 100644 index 00000000..181e3567 --- /dev/null +++ b/src/vnc/common/rdr/ZlibOutStream.cxx @@ -0,0 +1,140 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#include +#include +#include + +using namespace rdr; + +enum { DEFAULT_BUF_SIZE = 16384 }; + +ZlibOutStream::ZlibOutStream(OutStream* os, int bufSize_, int compressLevel) + : underlying(os), bufSize(bufSize_ ? bufSize_ : DEFAULT_BUF_SIZE), offset(0) +{ + zs = new z_stream; + zs->zalloc = Z_NULL; + zs->zfree = Z_NULL; + zs->opaque = Z_NULL; + if (deflateInit(zs, compressLevel) != Z_OK) { + delete zs; + throw Exception("ZlibOutStream: deflateInit failed"); + } + ptr = start = new U8[bufSize]; + end = start + bufSize; +} + +ZlibOutStream::~ZlibOutStream() +{ + try { + flush(); + } catch (Exception&) { + } + delete [] start; + deflateEnd(zs); + delete zs; +} + +void ZlibOutStream::setUnderlying(OutStream* os) +{ + underlying = os; +} + +int ZlibOutStream::length() +{ + return offset + ptr - start; +} + +void ZlibOutStream::flush() +{ + zs->next_in = start; + zs->avail_in = ptr - start; + +// fprintf(stderr,"zos flush: avail_in %d\n",zs->avail_in); + + while (zs->avail_in != 0) { + + do { + underlying->check(1); + zs->next_out = underlying->getptr(); + zs->avail_out = underlying->getend() - underlying->getptr(); + +// fprintf(stderr,"zos flush: calling deflate, avail_in %d, avail_out %d\n", +// zs->avail_in,zs->avail_out); + int rc = deflate(zs, Z_SYNC_FLUSH); + if (rc != Z_OK) throw Exception("ZlibOutStream: deflate failed"); + +// fprintf(stderr,"zos flush: after deflate: %d bytes\n", +// zs->next_out-underlying->getptr()); + + underlying->setptr(zs->next_out); + } while (zs->avail_out == 0); + } + + offset += ptr - start; + ptr = start; +} + +int ZlibOutStream::overrun(int itemSize, int nItems) +{ +// fprintf(stderr,"ZlibOutStream overrun\n"); + + if (itemSize > bufSize) + throw Exception("ZlibOutStream overrun: max itemSize exceeded"); + + while (end - ptr < itemSize) { + zs->next_in = start; + zs->avail_in = ptr - start; + + do { + underlying->check(1); + zs->next_out = underlying->getptr(); + zs->avail_out = underlying->getend() - underlying->getptr(); + +// fprintf(stderr,"zos overrun: calling deflate, avail_in %d, avail_out %d\n", +// zs->avail_in,zs->avail_out); + + int rc = deflate(zs, 0); + if (rc != Z_OK) throw Exception("ZlibOutStream: deflate failed"); + +// fprintf(stderr,"zos overrun: after deflate: %d bytes\n", +// zs->next_out-underlying->getptr()); + + underlying->setptr(zs->next_out); + } while (zs->avail_out == 0); + + // output buffer not full + + if (zs->avail_in == 0) { + offset += ptr - start; + ptr = start; + } else { + // but didn't consume all the data? try shifting what's left to the + // start of the buffer. + fprintf(stderr,"z out buf not full, but in data not consumed\n"); + memmove(start, zs->next_in, ptr - zs->next_in); + offset += zs->next_in - start; + ptr -= zs->next_in - start; + } + } + + if (itemSize * nItems > end - ptr) + nItems = (end - ptr) / itemSize; + + return nItems; +} diff --git a/src/vnc/common/rdr/ZlibOutStream.h b/src/vnc/common/rdr/ZlibOutStream.h new file mode 100644 index 00000000..7d737c14 --- /dev/null +++ b/src/vnc/common/rdr/ZlibOutStream.h @@ -0,0 +1,57 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// +// ZlibOutStream streams to a compressed data stream (underlying), compressing +// with zlib on the fly. +// + +#ifndef __RDR_ZLIBOUTSTREAM_H__ +#define __RDR_ZLIBOUTSTREAM_H__ + +#include + +struct z_stream_s; + +namespace rdr { + + class ZlibOutStream : public OutStream { + + public: + + ZlibOutStream(OutStream* os=0, int bufSize=0, int compressionLevel=-1); + virtual ~ZlibOutStream(); + + void setUnderlying(OutStream* os); + void flush(); + int length(); + + private: + + int overrun(int itemSize, int nItems); + + OutStream* underlying; + int bufSize; + int offset; + z_stream_s* zs; + U8* start; + }; + +} // end of namespace rdr + +#endif diff --git a/src/vnc/common/rdr/types.h b/src/vnc/common/rdr/types.h new file mode 100644 index 00000000..6421b137 --- /dev/null +++ b/src/vnc/common/rdr/types.h @@ -0,0 +1,66 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifndef __RDR_TYPES_H__ +#define __RDR_TYPES_H__ + +namespace rdr { + + typedef unsigned char U8; + typedef unsigned short U16; + typedef unsigned int U32; + typedef signed char S8; + typedef signed short S16; + typedef signed int S32; + + class U8Array { + public: + U8Array() : buf(0) {} + U8Array(U8* a) : buf(a) {} // note: assumes ownership + U8Array(int len) : buf(new U8[len]) {} + ~U8Array() { delete [] buf; } + + // Get the buffer pointer & clear it (i.e. caller takes ownership) + U8* takeBuf() { U8* tmp = buf; buf = 0; return tmp; } + + U8* buf; + }; + + class U16Array { + public: + U16Array() : buf(0) {} + U16Array(U16* a) : buf(a) {} // note: assumes ownership + U16Array(int len) : buf(new U16[len]) {} + ~U16Array() { delete [] buf; } + U16* takeBuf() { U16* tmp = buf; buf = 0; return tmp; } + U16* buf; + }; + + class U32Array { + public: + U32Array() : buf(0) {} + U32Array(U32* a) : buf(a) {} // note: assumes ownership + U32Array(int len) : buf(new U32[len]) {} + ~U32Array() { delete [] buf; } + U32* takeBuf() { U32* tmp = buf; buf = 0; return tmp; } + U32* buf; + }; + +} // end of namespace rdr + +#endif diff --git a/src/vnc/common/rfb/Blacklist.cxx b/src/vnc/common/rfb/Blacklist.cxx new file mode 100644 index 00000000..4590befe --- /dev/null +++ b/src/vnc/common/rfb/Blacklist.cxx @@ -0,0 +1,86 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#include +#include + +using namespace rfb; + +IntParameter Blacklist::threshold("BlacklistThreshold", + "The number of unauthenticated connection attempts allowed from any " + "individual host before that host is black-listed", + 5); +IntParameter Blacklist::initialTimeout("BlacklistTimeout", + "The initial timeout applied when a host is first black-listed. " + "The host cannot re-attempt a connection until the timeout expires.", + 10); + + +Blacklist::Blacklist() { +} + +Blacklist::~Blacklist() { + // Free the map keys + BlacklistMap::iterator i; + for (i=blm.begin(); i!=blm.end(); i++) { + strFree((char*)(*i).first); + } +} + +bool Blacklist::isBlackmarked(const char* name) { + BlacklistMap::iterator i = blm.find(name); + if (i == blm.end()) { + // Entry is not already black-marked. + // Create the entry unmarked, unblocked, + // with suitable defaults set. + BlacklistInfo bi; + bi.marks = 1; + bi.blockUntil = 0; + bi.blockTimeout = initialTimeout; + blm[strDup(name)] = bi; + i = blm.find(name); + } + + // Entry exists - has it reached the threshold yet? + if ((*i).second.marks >= threshold) { + // Yes - entry is blocked - has the timeout expired? + time_t now = time(0); + if (now >= (*i).second.blockUntil) { + // Timeout has expired. Reset timeout and allow + // a re-try. + (*i).second.blockUntil = now + (*i).second.blockTimeout; + (*i).second.blockTimeout = (*i).second.blockTimeout * 2; + return false; + } + // Blocked and timeout still in effect - reject! + return true; + } + + // We haven't reached the threshold yet. + // Increment the black-mark counter but allow + // the entry to pass. + (*i).second.marks++; + return false; +} + +void Blacklist::clearBlackmark(const char* name) { + BlacklistMap::iterator i = blm.find(name); + if (i != blm.end()) { + strFree((char*)(*i).first); + blm.erase(i); + } +} diff --git a/src/vnc/common/rfb/Blacklist.h b/src/vnc/common/rfb/Blacklist.h new file mode 100644 index 00000000..0eb38460 --- /dev/null +++ b/src/vnc/common/rfb/Blacklist.h @@ -0,0 +1,91 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// +// Blacklist.h - Handling of black-listed entities. +// Just keeps a table mapping strings to timing information, including +// how many times the entry has been black-listed and when to next +// put it on probation (e.g. allow a connection in from the host, and +// re-blacklist it if that fails). +// + +#ifndef __RFB_BLACKLIST_H__ +#define __RFB_BLACKLIST_H__ + +#include +#include +#include + +#include +#include + +namespace rfb { + + // + // -=- Blacklist handler + // + // Parameters include a threshold after which to blacklist the named + // host, and a timeout after which to re-consider them. + // + // Threshold means that isBlackmarked can be called that number of times + // before it will return true. + // + // Timeout means that after that many seconds, the next call to isBlackmarked + // will return false. At the same time, the timeout is doubled, so that the + // next calls will fail, until the timeout expires again or clearBlackmark is + // called. + // + // When clearBlackMark is called, the corresponding entry is completely + // removed, causing the next isBlackmarked call to return false. + + // KNOWN BUG: Client can keep making rejected requests, thus increasing + // their timeout. If client does this for 30 years, timeout may wrap round + // to a very small value again. + + // THIS CLASS IS NOT THREAD-SAFE! + + class Blacklist { + public: + Blacklist(); + ~Blacklist(); + + bool isBlackmarked(const char* name); + void clearBlackmark(const char* name); + + static IntParameter threshold; + static IntParameter initialTimeout; + + protected: + struct ltStr { + bool operator()(const char* s1, const char* s2) const { + return strcmp(s1, s2) < 0; + }; + }; + struct BlacklistInfo { + int marks; + time_t blockUntil; + unsigned int blockTimeout; + }; + typedef std::map BlacklistMap; + BlacklistMap blm; + }; + +} + +#endif + diff --git a/src/vnc/common/rfb/CConnection.cxx b/src/vnc/common/rfb/CConnection.cxx new file mode 100644 index 00000000..36778f02 --- /dev/null +++ b/src/vnc/common/rfb/CConnection.cxx @@ -0,0 +1,276 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace rfb; + +static LogWriter vlog("CConnection"); + +CConnection::CConnection() + : is(0), os(0), reader_(0), writer_(0), + shared(false), security(0), nSecTypes(0), clientSecTypeOrder(false), + state_(RFBSTATE_UNINITIALISED), useProtocol3_3(false) +{ +} + +CConnection::~CConnection() +{ + if (security) security->destroy(); + deleteReaderAndWriter(); +} + +void CConnection::deleteReaderAndWriter() +{ + delete reader_; + reader_ = 0; + delete writer_; + writer_ = 0; +} + +void CConnection::setStreams(rdr::InStream* is_, rdr::OutStream* os_) +{ + is = is_; + os = os_; +} + +void CConnection::addSecType(rdr::U8 secType) +{ + if (nSecTypes == maxSecTypes) + throw Exception("too many security types"); + secTypes[nSecTypes++] = secType; +} + +void CConnection::setClientSecTypeOrder(bool clientOrder) { + clientSecTypeOrder = clientOrder; +} + +void CConnection::initialiseProtocol() +{ + state_ = RFBSTATE_PROTOCOL_VERSION; +} + +void CConnection::processMsg() +{ + switch (state_) { + + case RFBSTATE_PROTOCOL_VERSION: processVersionMsg(); break; + case RFBSTATE_SECURITY_TYPES: processSecurityTypesMsg(); break; + case RFBSTATE_SECURITY: processSecurityMsg(); break; + case RFBSTATE_SECURITY_RESULT: processSecurityResultMsg(); break; + case RFBSTATE_INITIALISATION: processInitMsg(); break; + case RFBSTATE_NORMAL: reader_->readMsg(); break; + case RFBSTATE_UNINITIALISED: + throw Exception("CConnection::processMsg: not initialised yet?"); + default: + throw Exception("CConnection::processMsg: invalid state"); + } +} + +void CConnection::processVersionMsg() +{ + vlog.debug("reading protocol version"); + bool done; + if (!cp.readVersion(is, &done)) { + state_ = RFBSTATE_INVALID; + throw Exception("reading version failed: not an RFB server?"); + } + if (!done) return; + + vlog.info("Server supports RFB protocol version %d.%d", + cp.majorVersion, cp.minorVersion); + + // The only official RFB protocol versions are currently 3.3, 3.7 and 3.8 + if (cp.beforeVersion(3,3)) { + char msg[256]; + sprintf(msg,"Server gave unsupported RFB protocol version %d.%d", + cp.majorVersion, cp.minorVersion); + vlog.error(msg); + state_ = RFBSTATE_INVALID; + throw Exception(msg); + } else if (useProtocol3_3 || cp.beforeVersion(3,7)) { + cp.setVersion(3,3); + } else if (cp.afterVersion(3,8)) { + cp.setVersion(3,8); + } + + cp.writeVersion(os); + state_ = RFBSTATE_SECURITY_TYPES; + + vlog.info("Using RFB protocol version %d.%d", + cp.majorVersion, cp.minorVersion); +} + + +void CConnection::processSecurityTypesMsg() +{ + vlog.debug("processing security types message"); + + int secType = secTypeInvalid; + + if (cp.isVersion(3,3)) { + + // legacy 3.3 server may only offer "vnc authentication" or "none" + + secType = is->readU32(); + if (secType == secTypeInvalid) { + throwConnFailedException(); + + } else if (secType == secTypeNone || secType == secTypeVncAuth) { + int j; + for (j = 0; j < nSecTypes; j++) + if (secTypes[j] == secType) break; + if (j == nSecTypes) + secType = secTypeInvalid; + } else { + vlog.error("Unknown 3.3 security type %d", secType); + throw Exception("Unknown 3.3 security type"); + } + + } else { + + // >=3.7 server will offer us a list + + int nServerSecTypes = is->readU8(); + if (nServerSecTypes == 0) + throwConnFailedException(); + + int secTypePos = nSecTypes; + for (int i = 0; i < nServerSecTypes; i++) { + rdr::U8 serverSecType = is->readU8(); + vlog.debug("Server offers security type %s(%d)", + secTypeName(serverSecType),serverSecType); + + // If we haven't already chosen a secType, try this one + // If we are using the client's preference for types, + // we keep trying types, to find the one that matches and + // which appears first in the client's list of supported types. + if (secType == secTypeInvalid || clientSecTypeOrder) { + for (int j = 0; j < nSecTypes; j++) { + if (secTypes[j] == serverSecType && j < secTypePos) { + secType = secTypes[j]; + secTypePos = j; + break; + } + } + // NB: Continue reading the remaining server secTypes, but ignore them + } + } + + // Inform the server of our decision + if (secType != secTypeInvalid) { + os->writeU8(secType); + os->flush(); + vlog.debug("Choosing security type %s(%d)",secTypeName(secType),secType); + } + } + + if (secType == secTypeInvalid) { + state_ = RFBSTATE_INVALID; + vlog.error("No matching security types"); + throw Exception("No matching security types"); + } + + state_ = RFBSTATE_SECURITY; + security = getCSecurity(secType); + processSecurityMsg(); +} + +void CConnection::processSecurityMsg() +{ + vlog.debug("processing security message"); + if (security->processMsg(this)) { + state_ = RFBSTATE_SECURITY_RESULT; + processSecurityResultMsg(); + } +} + +void CConnection::processSecurityResultMsg() +{ + vlog.debug("processing security result message"); + int result; + if (cp.beforeVersion(3,8) && security->getType() == secTypeNone) { + result = secResultOK; + } else { + if (!is->checkNoWait(1)) return; + result = is->readU32(); + } + switch (result) { + case secResultOK: + securityCompleted(); + return; + case secResultFailed: + vlog.debug("auth failed"); + break; + case secResultTooMany: + vlog.debug("auth failed - too many tries"); + break; + default: + throw Exception("Unknown security result from server"); + } + CharArray reason; + if (cp.beforeVersion(3,8)) + reason.buf = strDup("Authentication failure"); + else + reason.buf = is->readString(); + state_ = RFBSTATE_INVALID; + throw AuthFailureException(reason.buf); +} + +void CConnection::processInitMsg() +{ + vlog.debug("reading server initialisation"); + reader_->readServerInit(); +} + +void CConnection::throwConnFailedException() +{ + state_ = RFBSTATE_INVALID; + CharArray reason; + reason.buf = is->readString(); + throw ConnFailedException(reason.buf); +} + +void CConnection::securityCompleted() +{ + state_ = RFBSTATE_INITIALISATION; + reader_ = new CMsgReaderV3(this, is); + writer_ = new CMsgWriterV3(&cp, os); + vlog.debug("Authentication success!"); + authSuccess(); + writer_->writeClientInit(shared); +} + +void CConnection::authSuccess() +{ +} + +void CConnection::serverInit() +{ + state_ = RFBSTATE_NORMAL; + vlog.debug("initialisation done"); +} diff --git a/src/vnc/common/rfb/CConnection.h b/src/vnc/common/rfb/CConnection.h new file mode 100644 index 00000000..79110eb9 --- /dev/null +++ b/src/vnc/common/rfb/CConnection.h @@ -0,0 +1,183 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +// +// CConnection - class on the client side representing a connection to a +// server. A derived class should override methods appropriately. +// + +#ifndef __RFB_CCONNECTION_H__ +#define __RFB_CCONNECTION_H__ + +#include +#include +#include +#include + +namespace rfb { + + class CMsgReader; + class CMsgWriter; + class CSecurity; + class IdentityVerifier; + + class CConnection : public CMsgHandler { + public: + + CConnection(); + virtual ~CConnection(); + + // Methods to initialise the connection + + // setServerName() is used to provide a unique(ish) name for the server to + // which we are connected. This might be the result of getPeerEndpoint on + // a TcpSocket, for example, or a host specified by DNS name & port. + // The serverName is used when verifying the Identity of a host (see RA2). + void setServerName(const char* name_) { serverName.replaceBuf(strDup(name_)); } + + // setStreams() sets the streams to be used for the connection. These must + // be set before initialiseProtocol() and processMsg() are called. The + // CSecurity object may call setStreams() again to provide alternative + // streams over which the RFB protocol is sent (i.e. encrypting/decrypting + // streams). Ownership of the streams remains with the caller + // (i.e. SConnection will not delete them). + void setStreams(rdr::InStream* is, rdr::OutStream* os); + + // addSecType() should be called once for each security type which the + // client supports. The order in which they're added is such that the + // first one is most preferred. + void addSecType(rdr::U8 secType); + + // setClientSecTypeOrder() determines whether the client should obey + // the server's security type preference, by picking the first server security + // type that the client supports, or whether it should pick the first type + // that the server supports, from the client-supported list of types. + void setClientSecTypeOrder(bool clientOrder); + + // setShared sets the value of the shared flag which will be sent to the + // server upon initialisation. + void setShared(bool s) { shared = s; } + + // setProtocol3_3 configures whether or not the CConnection should + // only ever support protocol version 3.3 + void setProtocol3_3(bool s) {useProtocol3_3 = s;} + + // initialiseProtocol() should be called once the streams and security + // types are set. Subsequently, processMsg() should be called whenever + // there is data to read on the InStream. + void initialiseProtocol(); + + // processMsg() should be called whenever there is either: + // - data available on the underlying network stream + // In this case, processMsg may return without processing an RFB message, + // if the available data does not result in an RFB message being ready + // to handle. e.g. if data is encrypted. + // NB: This makes it safe to call processMsg() in response to select() + // - data available on the CConnection's current InStream + // In this case, processMsg should always process the available RFB + // message before returning. + // NB: In either case, you must have called initialiseProtocol() first. + void processMsg(); + + + // Methods to be overridden in a derived class + + // getCSecurity() gets the CSecurity object for the given type. The type + // is guaranteed to be one of the secTypes passed in to addSecType(). The + // CSecurity object's destroy() method will be called by the CConnection + // from its destructor. + virtual CSecurity* getCSecurity(int secType)=0; + + // getCurrentCSecurity() gets the CSecurity instance used for this connection. + const CSecurity* getCurrentCSecurity() const {return security;} + + // getIdVerifier() returns the identity verifier associated with the connection. + // Ownership of the IdentityVerifier is retained by the CConnection instance. + virtual IdentityVerifier* getIdentityVerifier() {return 0;} + + // authSuccess() is called when authentication has succeeded. + virtual void authSuccess(); + + // serverInit() is called when the ServerInit message is received. The + // derived class must call on to CConnection::serverInit(). + virtual void serverInit(); + + + // Other methods + + // deleteReaderAndWriter() deletes the reader and writer associated with + // this connection. This may be useful if you want to delete the streams + // before deleting the SConnection to make sure that no attempt by the + // SConnection is made to read or write. + // XXX Do we really need this at all??? + void deleteReaderAndWriter(); + + CMsgReader* reader() { return reader_; } + CMsgWriter* writer() { return writer_; } + + rdr::InStream* getInStream() { return is; } + rdr::OutStream* getOutStream() { return os; } + + // Access method used by SSecurity implementations that can verify servers' + // Identities, to determine the unique(ish) name of the server. + const char* getServerName() const { return serverName.buf; } + + enum stateEnum { + RFBSTATE_UNINITIALISED, + RFBSTATE_PROTOCOL_VERSION, + RFBSTATE_SECURITY_TYPES, + RFBSTATE_SECURITY, + RFBSTATE_SECURITY_RESULT, + RFBSTATE_INITIALISATION, + RFBSTATE_NORMAL, + RFBSTATE_INVALID + }; + + stateEnum state() { return state_; } + + protected: + void setState(stateEnum s) { state_ = s; } + + private: + void processVersionMsg(); + void processSecurityTypesMsg(); + void processSecurityMsg(); + void processSecurityResultMsg(); + void processInitMsg(); + void throwAuthFailureException(); + void throwConnFailedException(); + void securityCompleted(); + + rdr::InStream* is; + rdr::OutStream* os; + CMsgReader* reader_; + CMsgWriter* writer_; + bool deleteStreamsWhenDone; + bool shared; + CSecurity* security; + enum { maxSecTypes = 8 }; + int nSecTypes; + rdr::U8 secTypes[maxSecTypes]; + bool clientSecTypeOrder; + stateEnum state_; + + CharArray serverName; + + bool useProtocol3_3; + }; +} +#endif diff --git a/src/vnc/common/rfb/CMakeLists.txt b/src/vnc/common/rfb/CMakeLists.txt new file mode 100644 index 00000000..a5c83839 --- /dev/null +++ b/src/vnc/common/rfb/CMakeLists.txt @@ -0,0 +1,162 @@ +# synergy -- mouse and keyboard sharing utility +# Copyright (C) 2012 Nick Bolton +# +# 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 COPYING 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 . + +set(inc + Blacklist.h + CConnection.h + CMsgHandler.h + CMsgReader.h + CMsgReaderV3.h + CMsgWriter.h + CMsgWriterV3.h + ColourCube.h + ColourMap.h + ComparingUpdateTracker.h + Configuration.h + ConnParams.h + CSecurity.h + CSecurityNone.h + CSecurityVncAuth.h + Cursor.h + d3des.h + Decoder.h + Encoder.h + encodings.h + Exception.h + hextileConstants.h + hextileDecode.h + HextileDecoder.h + hextileEncode.h + HextileEncoder.h + Hostname.h + HTTPServer.h + ImageGetter.h + InputHandler.h + KeyRemapper.h + keysymdef.h + Logger.h + Logger_file.h + Logger_stdio.h + LogWriter.h + msgTypes.h + Password.h + Pixel.h + PixelBuffer.h + PixelFormat.h + RawDecoder.h + RawEncoder.h + Rect.h + Region.h + rreDecode.h + RREDecoder.h + rreEncode.h + RREEncoder.h + SConnection.h + SDesktop.h + secTypes.h + ServerCore.h + SMsgHandler.h + SMsgReader.h + SMsgReaderV3.h + SMsgWriter.h + SMsgWriterV3.h + SSecurity.h + SSecurityFactoryStandard.h + SSecurityNone.h + SSecurityVncAuth.h + Threading.h + Timer.h + TransImageGetter.h + transInitTempl.h + transTempl.h + TrueColourMap.h + UpdateTracker.h + UserPasswdGetter.h + util.h + VNCSConnectionST.h + VNCServer.h + VNCServerST.h + zrleDecode.h + ZRLEDecoder.h + zrleEncode.h + ZRLEEncoder.h +) + +set(src + d3des.c + Blacklist.cxx + CConnection.cxx + CMsgHandler.cxx + CMsgReader.cxx + CMsgReaderV3.cxx + CMsgWriter.cxx + CMsgWriterV3.cxx + ComparingUpdateTracker.cxx + Configuration.cxx + ConnParams.cxx + CSecurityVncAuth.cxx + Cursor.cxx + Decoder.cxx + Encoder.cxx + encodings.cxx + HextileDecoder.cxx + HextileEncoder.cxx + HTTPServer.cxx + KeyRemapper.cxx + Logger.cxx + Logger_file.cxx + Logger_stdio.cxx + LogWriter.cxx + Password.cxx + PixelBuffer.cxx + PixelFormat.cxx + RawDecoder.cxx + RawEncoder.cxx + Region.cxx + RREDecoder.cxx + RREEncoder.cxx + SConnection.cxx + secTypes.cxx + ServerCore.cxx + SMsgHandler.cxx + SMsgReader.cxx + SMsgReaderV3.cxx + SMsgWriter.cxx + SMsgWriterV3.cxx + SSecurityFactoryStandard.cxx + SSecurityVncAuth.cxx + Timer.cxx + TransImageGetter.cxx + UpdateTracker.cxx + util.cxx + VNCSConnectionST.cxx + VNCServerST.cxx + ZRLEDecoder.cxx + ZRLEEncoder.cxx +) + +if (WIN32) + list(APPEND src ${inc}) +endif() + +set(inc + .. + ../../win +) + +include_directories(${inc}) +add_library(vnc_rfb STATIC ${src}) +target_link_libraries(vnc_rfb + vnc_Xregion) diff --git a/src/vnc/common/rfb/CMsgHandler.cxx b/src/vnc/common/rfb/CMsgHandler.cxx new file mode 100644 index 00000000..05dbf70f --- /dev/null +++ b/src/vnc/common/rfb/CMsgHandler.cxx @@ -0,0 +1,99 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#include +#include + +using namespace rfb; + +CMsgHandler::CMsgHandler() +{ +} + +CMsgHandler::~CMsgHandler() +{ +} + +void CMsgHandler::setDesktopSize(int width, int height) +{ + cp.width = width; + cp.height = height; +} + +void CMsgHandler::setCursor(int w, int h, const Point& hotspot, void* data, void* mask) +{ +} + +void CMsgHandler::setPixelFormat(const PixelFormat& pf) +{ + cp.setPF(pf); +} + +void CMsgHandler::setName(const char* name) +{ + cp.setName(name); +} + +void CMsgHandler::serverInit() +{ + throw Exception("CMsgHandler::serverInit called"); +} + +void CMsgHandler::framebufferUpdateStart() +{ +} + +void CMsgHandler::framebufferUpdateEnd() +{ +} + +void CMsgHandler::beginRect(const Rect& r, unsigned int encoding) +{ +} + +void CMsgHandler::endRect(const Rect& r, unsigned int encoding) +{ +} + + +void CMsgHandler::setColourMapEntries(int firstColour, int nColours, + rdr::U16* rgbs) +{ + throw Exception("CMsgHandler::setColourMapEntries called"); +} + +void CMsgHandler::bell() +{ +} + +void CMsgHandler::serverCutText(const char* str, int len) +{ +} + +void CMsgHandler::fillRect(const Rect& r, Pixel pix) +{ +} + +void CMsgHandler::imageRect(const Rect& r, void* pixels) +{ +} + +void CMsgHandler::copyRect(const Rect& r, int srcX, int srcY) +{ +} + + diff --git a/src/vnc/common/rfb/CMsgHandler.h b/src/vnc/common/rfb/CMsgHandler.h new file mode 100644 index 00000000..a9cafbfe --- /dev/null +++ b/src/vnc/common/rfb/CMsgHandler.h @@ -0,0 +1,69 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +// +// CMsgHandler - class to handle incoming messages on the client side. +// + +#ifndef __RFB_CMSGHANDLER_H__ +#define __RFB_CMSGHANDLER_H__ + +#include +#include +#include +#include + +namespace rdr { class InStream; } + +namespace rfb { + + class CMsgHandler { + public: + CMsgHandler(); + virtual ~CMsgHandler(); + + // The following methods are called as corresponding messages are read. A + // derived class should override these methods as desired. Note that for + // the setDesktopSize(), setPixelFormat() and setName() methods, a derived + // class should call on to CMsgHandler's methods to set the members of cp + // appropriately. + + virtual void setDesktopSize(int w, int h); + virtual void setCursor(int width, int height, const Point& hotspot, + void* data, void* mask); + virtual void setPixelFormat(const PixelFormat& pf); + virtual void setName(const char* name); + virtual void serverInit(); + + virtual void framebufferUpdateStart(); + virtual void framebufferUpdateEnd(); + virtual void beginRect(const Rect& r, unsigned int encoding); + virtual void endRect(const Rect& r, unsigned int encoding); + + virtual void setColourMapEntries(int firstColour, int nColours, + rdr::U16* rgbs); + virtual void bell(); + virtual void serverCutText(const char* str, int len); + + virtual void fillRect(const Rect& r, Pixel pix); + virtual void imageRect(const Rect& r, void* pixels); + virtual void copyRect(const Rect& r, int srcX, int srcY); + + ConnParams cp; + }; +} +#endif diff --git a/src/vnc/common/rfb/CMsgReader.cxx b/src/vnc/common/rfb/CMsgReader.cxx new file mode 100644 index 00000000..49110070 --- /dev/null +++ b/src/vnc/common/rfb/CMsgReader.cxx @@ -0,0 +1,160 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#include +#include +#include +#include +#include +#include + +using namespace rfb; + +CMsgReader::CMsgReader(CMsgHandler* handler_, rdr::InStream* is_) + : imageBufIdealSize(0), handler(handler_), is(is_), + imageBuf(0), imageBufSize(0) +{ + for (unsigned int i = 0; i <= encodingMax; i++) { + decoders[i] = 0; + } +} + +CMsgReader::~CMsgReader() +{ + for (unsigned int i = 0; i <= encodingMax; i++) { + delete decoders[i]; + } + delete [] imageBuf; +} + +void CMsgReader::readSetColourMapEntries() +{ + is->skip(1); + int firstColour = is->readU16(); + int nColours = is->readU16(); + rdr::U16Array rgbs(nColours * 3); + for (int i = 0; i < nColours * 3; i++) + rgbs.buf[i] = is->readU16(); + handler->setColourMapEntries(firstColour, nColours, rgbs.buf); +} + +void CMsgReader::readBell() +{ + handler->bell(); +} + +void CMsgReader::readServerCutText() +{ + is->skip(3); + int len = is->readU32(); + if (len > 256*1024) { + is->skip(len); + fprintf(stderr,"cut text too long (%d bytes) - ignoring\n",len); + return; + } + CharArray ca(len+1); + ca.buf[len] = 0; + is->readBytes(ca.buf, len); + handler->serverCutText(ca.buf, len); +} + +void CMsgReader::readFramebufferUpdateStart() +{ + handler->framebufferUpdateStart(); +} + +void CMsgReader::readFramebufferUpdateEnd() +{ + handler->framebufferUpdateEnd(); +} + +void CMsgReader::readRect(const Rect& r, unsigned int encoding) +{ + if ((r.br.x > handler->cp.width) || (r.br.y > handler->cp.height)) { + fprintf(stderr, "Rect too big: %dx%d at %d,%d exceeds %dx%d\n", + r.width(), r.height(), r.tl.x, r.tl.y, + handler->cp.width, handler->cp.height); + throw Exception("Rect too big"); + } + + if (r.is_empty()) + fprintf(stderr, "Warning: zero size rect\n"); + + handler->beginRect(r, encoding); + + if (encoding == encodingCopyRect) { + readCopyRect(r); + } else { + if (encoding > encodingMax) + throw Exception("Unknown rect encoding"); + if (!decoders[encoding]) { + decoders[encoding] = Decoder::createDecoder(encoding, this); + if (!decoders[encoding]) { + fprintf(stderr, "Unknown rect encoding %d\n", encoding); + throw Exception("Unknown rect encoding"); + } + } + decoders[encoding]->readRect(r, handler); + } + + handler->endRect(r, encoding); +} + +void CMsgReader::readCopyRect(const Rect& r) +{ + int srcX = is->readU16(); + int srcY = is->readU16(); + handler->copyRect(r, srcX, srcY); +} + +void CMsgReader::readSetCursor(int width, int height, const Point& hotspot) +{ + int data_len = width * height * (handler->cp.pf().bpp/8); + int mask_len = ((width+7)/8) * height; + rdr::U8Array data(data_len); + rdr::U8Array mask(mask_len); + + is->readBytes(data.buf, data_len); + is->readBytes(mask.buf, mask_len); + + handler->setCursor(width, height, hotspot, data.buf, mask.buf); +} + +rdr::U8* CMsgReader::getImageBuf(int required, int requested, int* nPixels) +{ + int requiredBytes = required * (handler->cp.pf().bpp / 8); + int requestedBytes = requested * (handler->cp.pf().bpp / 8); + int size = requestedBytes; + if (size > imageBufIdealSize) size = imageBufIdealSize; + + if (size < requiredBytes) + size = requiredBytes; + + if (imageBufSize < size) { + imageBufSize = size; + delete [] imageBuf; + imageBuf = new rdr::U8[imageBufSize]; + } + if (nPixels) + *nPixels = imageBufSize / (handler->cp.pf().bpp / 8); + return imageBuf; +} + +int CMsgReader::bpp() +{ + return handler->cp.pf().bpp; +} diff --git a/src/vnc/common/rfb/CMsgReader.h b/src/vnc/common/rfb/CMsgReader.h new file mode 100644 index 00000000..7a611fc8 --- /dev/null +++ b/src/vnc/common/rfb/CMsgReader.h @@ -0,0 +1,73 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +// +// CMsgReader - class for reading RFB messages on the server side +// (i.e. messages from client to server). +// + +#ifndef __RFB_CMSGREADER_H__ +#define __RFB_CMSGREADER_H__ + +#include +#include +#include + +namespace rdr { class InStream; } + +namespace rfb { + class CMsgHandler; + struct Rect; + + class CMsgReader { + public: + virtual ~CMsgReader(); + + virtual void readServerInit()=0; + + // readMsg() reads a message, calling the handler as appropriate. + virtual void readMsg()=0; + + rdr::InStream* getInStream() { return is; } + rdr::U8* getImageBuf(int required, int requested=0, int* nPixels=0); + int bpp(); + + int imageBufIdealSize; + + protected: + virtual void readSetColourMapEntries(); + virtual void readBell(); + virtual void readServerCutText(); + + virtual void readFramebufferUpdateStart(); + virtual void readFramebufferUpdateEnd(); + virtual void readRect(const Rect& r, unsigned int encoding); + + virtual void readCopyRect(const Rect& r); + + virtual void readSetCursor(int width, int height, const Point& hotspot); + + CMsgReader(CMsgHandler* handler, rdr::InStream* is); + + CMsgHandler* handler; + rdr::InStream* is; + Decoder* decoders[encodingMax+1]; + rdr::U8* imageBuf; + int imageBufSize; + }; +} +#endif diff --git a/src/vnc/common/rfb/CMsgReaderV3.cxx b/src/vnc/common/rfb/CMsgReaderV3.cxx new file mode 100644 index 00000000..2f2c1655 --- /dev/null +++ b/src/vnc/common/rfb/CMsgReaderV3.cxx @@ -0,0 +1,95 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#include +#include +#include +#include +#include +#include +#include + +using namespace rfb; + +CMsgReaderV3::CMsgReaderV3(CMsgHandler* handler, rdr::InStream* is) + : CMsgReader(handler, is), nUpdateRectsLeft(0) +{ +} + +CMsgReaderV3::~CMsgReaderV3() +{ +} + +void CMsgReaderV3::readServerInit() +{ + int width = is->readU16(); + int height = is->readU16(); + handler->setDesktopSize(width, height); + PixelFormat pf; + pf.read(is); + handler->setPixelFormat(pf); + CharArray name(is->readString()); + handler->setName(name.buf); + handler->serverInit(); +} + +void CMsgReaderV3::readMsg() +{ + if (nUpdateRectsLeft == 0) { + + int type = is->readU8(); + switch (type) { + case msgTypeFramebufferUpdate: readFramebufferUpdate(); break; + case msgTypeSetColourMapEntries: readSetColourMapEntries(); break; + case msgTypeBell: readBell(); break; + case msgTypeServerCutText: readServerCutText(); break; + default: + fprintf(stderr, "unknown message type %d\n", type); + throw Exception("unknown message type"); + } + + } else { + + int x = is->readU16(); + int y = is->readU16(); + int w = is->readU16(); + int h = is->readU16(); + unsigned int encoding = is->readU32(); + + switch (encoding) { + case pseudoEncodingDesktopSize: + handler->setDesktopSize(w, h); + break; + case pseudoEncodingCursor: + readSetCursor(w, h, Point(x,y)); + break; + default: + readRect(Rect(x, y, x+w, y+h), encoding); + break; + }; + + nUpdateRectsLeft--; + if (nUpdateRectsLeft == 0) handler->framebufferUpdateEnd(); + } +} + +void CMsgReaderV3::readFramebufferUpdate() +{ + is->skip(1); + nUpdateRectsLeft = is->readU16(); + handler->framebufferUpdateStart(); +} diff --git a/src/vnc/common/rfb/CMsgReaderV3.h b/src/vnc/common/rfb/CMsgReaderV3.h new file mode 100644 index 00000000..689bb650 --- /dev/null +++ b/src/vnc/common/rfb/CMsgReaderV3.h @@ -0,0 +1,35 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#ifndef __RFB_CMSGREADERV3_H__ +#define __RFB_CMSGREADERV3_H__ + +#include + +namespace rfb { + class CMsgReaderV3 : public CMsgReader { + public: + CMsgReaderV3(CMsgHandler* handler, rdr::InStream* is); + virtual ~CMsgReaderV3(); + virtual void readServerInit(); + virtual void readMsg(); + private: + void readFramebufferUpdate(); + int nUpdateRectsLeft; + }; +} +#endif diff --git a/src/vnc/common/rfb/CMsgWriter.cxx b/src/vnc/common/rfb/CMsgWriter.cxx new file mode 100644 index 00000000..42ae09ec --- /dev/null +++ b/src/vnc/common/rfb/CMsgWriter.cxx @@ -0,0 +1,126 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace rfb; + +CMsgWriter::CMsgWriter(ConnParams* cp_, rdr::OutStream* os_) + : cp(cp_), os(os_) +{ +} + +CMsgWriter::~CMsgWriter() +{ +} + +void CMsgWriter::writeSetPixelFormat(const PixelFormat& pf) +{ + startMsg(msgTypeSetPixelFormat); + os->pad(3); + pf.write(os); + endMsg(); +} + +void CMsgWriter::writeSetEncodings(int nEncodings, rdr::U32* encodings) +{ + startMsg(msgTypeSetEncodings); + os->skip(1); + os->writeU16(nEncodings); + for (int i = 0; i < nEncodings; i++) + os->writeU32(encodings[i]); + endMsg(); +} + +// Ask for encodings based on which decoders are supported. Assumes higher +// encoding numbers are more desirable. + +void CMsgWriter::writeSetEncodings(int preferredEncoding, bool useCopyRect) +{ + int nEncodings = 0; + rdr::U32 encodings[encodingMax+2]; + if (cp->supportsLocalCursor) + encodings[nEncodings++] = pseudoEncodingCursor; + if (cp->supportsDesktopResize) + encodings[nEncodings++] = pseudoEncodingDesktopSize; + if (Decoder::supported(preferredEncoding)) { + encodings[nEncodings++] = preferredEncoding; + } + if (useCopyRect) { + encodings[nEncodings++] = encodingCopyRect; + } + for (int i = encodingMax; i >= 0; i--) { + if (i != preferredEncoding && Decoder::supported(i)) { + encodings[nEncodings++] = i; + } + } + writeSetEncodings(nEncodings, encodings); +} + +void CMsgWriter::writeFramebufferUpdateRequest(const Rect& r, bool incremental) +{ + startMsg(msgTypeFramebufferUpdateRequest); + os->writeU8(incremental); + os->writeU16(r.tl.x); + os->writeU16(r.tl.y); + os->writeU16(r.width()); + os->writeU16(r.height()); + endMsg(); +} + + +void CMsgWriter::keyEvent(rdr::U32 key, bool down) +{ + startMsg(msgTypeKeyEvent); + os->writeU8(down); + os->pad(2); + os->writeU32(key); + endMsg(); +} + + +void CMsgWriter::pointerEvent(const Point& pos, int buttonMask) +{ + Point p(pos); + if (p.x < 0) p.x = 0; + if (p.y < 0) p.y = 0; + if (p.x >= cp->width) p.x = cp->width - 1; + if (p.y >= cp->height) p.y = cp->height - 1; + + startMsg(msgTypePointerEvent); + os->writeU8(buttonMask); + os->writeU16(p.x); + os->writeU16(p.y); + endMsg(); +} + + +void CMsgWriter::clientCutText(const char* str, int len) +{ + startMsg(msgTypeClientCutText); + os->pad(3); + os->writeU32(len); + os->writeBytes(str, len); + endMsg(); +} diff --git a/src/vnc/common/rfb/CMsgWriter.h b/src/vnc/common/rfb/CMsgWriter.h new file mode 100644 index 00000000..19be8df9 --- /dev/null +++ b/src/vnc/common/rfb/CMsgWriter.h @@ -0,0 +1,65 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +// +// CMsgWriter - class for writing RFB messages on the server side. +// + +#ifndef __RFB_CMSGWRITER_H__ +#define __RFB_CMSGWRITER_H__ + +#include + +namespace rdr { class OutStream; } + +namespace rfb { + + class PixelFormat; + class ConnParams; + struct Rect; + + class CMsgWriter : public InputHandler { + public: + virtual ~CMsgWriter(); + + // CMsgWriter abstract interface methods + virtual void writeClientInit(bool shared)=0; + virtual void startMsg(int type)=0; + virtual void endMsg()=0; + + // CMsgWriter implemented methods + virtual void writeSetPixelFormat(const PixelFormat& pf); + virtual void writeSetEncodings(int nEncodings, rdr::U32* encodings); + virtual void writeSetEncodings(int preferredEncoding, bool useCopyRect); + virtual void writeFramebufferUpdateRequest(const Rect& r,bool incremental); + + // InputHandler implementation + virtual void keyEvent(rdr::U32 key, bool down); + virtual void pointerEvent(const Point& pos, int buttonMask); + virtual void clientCutText(const char* str, int len); + + ConnParams* getConnParams() { return cp; } + rdr::OutStream* getOutStream() { return os; } + + protected: + CMsgWriter(ConnParams* cp, rdr::OutStream* os); + + ConnParams* cp; + rdr::OutStream* os; + }; +} +#endif diff --git a/src/vnc/common/rfb/CMsgWriterV3.cxx b/src/vnc/common/rfb/CMsgWriterV3.cxx new file mode 100644 index 00000000..a9807952 --- /dev/null +++ b/src/vnc/common/rfb/CMsgWriterV3.cxx @@ -0,0 +1,49 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#include +#include +#include +#include +#include + +using namespace rfb; + +CMsgWriterV3::CMsgWriterV3(ConnParams* cp, rdr::OutStream* os) + : CMsgWriter(cp, os) +{ +} + +CMsgWriterV3::~CMsgWriterV3() +{ +} + +void CMsgWriterV3::writeClientInit(bool shared) +{ + os->writeU8(shared); + endMsg(); +} + +void CMsgWriterV3::startMsg(int type) +{ + os->writeU8(type); +} + +void CMsgWriterV3::endMsg() +{ + os->flush(); +} diff --git a/src/vnc/common/rfb/CMsgWriterV3.h b/src/vnc/common/rfb/CMsgWriterV3.h new file mode 100644 index 00000000..0b2f9afe --- /dev/null +++ b/src/vnc/common/rfb/CMsgWriterV3.h @@ -0,0 +1,35 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#ifndef __RFB_CMSGWRITERV3_H__ +#define __RFB_CMSGWRITERV3_H__ + +#include + +namespace rfb { + class CMsgWriterV3 : public CMsgWriter { + public: + CMsgWriterV3(ConnParams* cp, rdr::OutStream* os); + virtual ~CMsgWriterV3(); + + virtual void writeClientInit(bool shared); + virtual void startMsg(int type); + virtual void endMsg(); + + }; +} +#endif diff --git a/src/vnc/common/rfb/CSecurity.h b/src/vnc/common/rfb/CSecurity.h new file mode 100644 index 00000000..90a01d70 --- /dev/null +++ b/src/vnc/common/rfb/CSecurity.h @@ -0,0 +1,52 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +// +// CSecurity - class on the client side for handling security handshaking. A +// derived class for a particular security type overrides the processMsg() +// method. + +// processMsg() is called first when the security type has been decided on, and +// will keep being called whenever there is data to read from the server. It +// should return false when it needs more data, or true when the security +// handshaking is over and we are now waiting for the SecurityResult message +// from the server. In the event of failure a suitable exception should be +// thrown. +// +// Note that the first time processMsg() is called, there is no guarantee that +// there is any data to read from the CConnection's InStream, but subsequent +// calls guarantee there is at least one byte which can be read without +// blocking. +// +// description is a string describing the level of encryption applied to the +// session, or null if no encryption will be used. + +#ifndef __RFB_CSECURITY_H__ +#define __RFB_CSECURITY_H__ + +namespace rfb { + class CConnection; + class CSecurity { + public: + virtual ~CSecurity() {} + virtual bool processMsg(CConnection* cc)=0; + virtual void destroy() { delete this; } + virtual int getType() const = 0; + virtual const char* description() const = 0; + }; +} +#endif diff --git a/src/vnc/common/rfb/CSecurityNone.h b/src/vnc/common/rfb/CSecurityNone.h new file mode 100644 index 00000000..54c10168 --- /dev/null +++ b/src/vnc/common/rfb/CSecurityNone.h @@ -0,0 +1,36 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +// +// CSecurityNone.h +// + +#ifndef __CSECURITYNONE_H__ +#define __CSECURITYNONE_H__ + +#include + +namespace rfb { + + class CSecurityNone : public CSecurity { + public: + virtual bool processMsg(CConnection* cc) { return true; } + virtual int getType() const {return secTypeNone;} + virtual const char* description() const {return "No Encryption";} + }; +} +#endif diff --git a/src/vnc/common/rfb/CSecurityVncAuth.cxx b/src/vnc/common/rfb/CSecurityVncAuth.cxx new file mode 100644 index 00000000..2d991701 --- /dev/null +++ b/src/vnc/common/rfb/CSecurityVncAuth.cxx @@ -0,0 +1,74 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +// +// CSecurityVncAuth +// +// XXX not thread-safe, because d3des isn't - do we need to worry about this? +// + +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include +} + + +using namespace rfb; + +static const int vncAuthChallengeSize = 16; + + +CSecurityVncAuth::CSecurityVncAuth(UserPasswdGetter* upg_) + : upg(upg_) +{ +} + +CSecurityVncAuth::~CSecurityVncAuth() +{ +} + +bool CSecurityVncAuth::processMsg(CConnection* cc) +{ + rdr::InStream* is = cc->getInStream(); + rdr::OutStream* os = cc->getOutStream(); + + // Read the challenge & obtain the user's password + rdr::U8 challenge[vncAuthChallengeSize]; + is->readBytes(challenge, vncAuthChallengeSize); + PlainPasswd passwd(strDup("test")); + //upg->getUserPasswd(0, &passwd.buf); + + // Calculate the correct response + rdr::U8 key[8]; + int pwdLen = strlen(passwd.buf); + for (int i=0; i<8; i++) + key[i] = iwriteBytes(challenge, vncAuthChallengeSize); + os->flush(); + return true; +} diff --git a/src/vnc/common/rfb/CSecurityVncAuth.h b/src/vnc/common/rfb/CSecurityVncAuth.h new file mode 100644 index 00000000..8d38d878 --- /dev/null +++ b/src/vnc/common/rfb/CSecurityVncAuth.h @@ -0,0 +1,39 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#ifndef __RFB_CSECURITYVNCAUTH_H__ +#define __RFB_CSECURITYVNCAUTH_H__ + +#include +#include + +namespace rfb { + + class UserPasswdGetter; + + class CSecurityVncAuth : public CSecurity { + public: + CSecurityVncAuth(UserPasswdGetter* pg); + virtual ~CSecurityVncAuth(); + virtual bool processMsg(CConnection* cc); + virtual int getType() const {return secTypeVncAuth;}; + virtual const char* description() const {return "No Encryption";} + private: + UserPasswdGetter* upg; + }; +} +#endif diff --git a/src/vnc/common/rfb/ColourCube.h b/src/vnc/common/rfb/ColourCube.h new file mode 100644 index 00000000..b83cbba8 --- /dev/null +++ b/src/vnc/common/rfb/ColourCube.h @@ -0,0 +1,96 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +// +// ColourCube - structure to represent a colour cube. The colour cube consists +// of its dimensions (nRed x nGreen x nBlue) and a table mapping an (r,g,b) +// triple to a pixel value. +// +// A colour cube is used in two cases. The first is internally in a viewer +// when it cannot use a trueColour format, nor can it have exclusive access to +// a writable colour map. This is most notably the case for an X viewer +// wishing to use a PseudoColor X server's default colormap. +// +// The second use is on the server side when a client has asked for a colour +// map and the server is trueColour. Instead of setting an uneven trueColour +// format like bgr233, it can set the client's colour map up with a 6x6x6 +// colour cube. For this use the colour cube table has a null mapping, which +// makes it easy to perform the reverse lookup operation from pixel value to +// r,g,b values. + +#ifndef __RFB_COLOURCUBE_H__ +#define __RFB_COLOURCUBE_H__ + +#include +#include + +namespace rfb { + + class ColourCube : public ColourMap { + public: + ColourCube(int nr, int ng, int nb, Pixel* table_=0) + : nRed(nr), nGreen(ng), nBlue(nb), table(table_), deleteTable(false) + { + if (!table) { + table = new Pixel[size()]; + deleteTable = true; + // set a null mapping by default + for (int i = 0; i < size(); i++) + table[i] = i; + } + } + + ColourCube() : deleteTable(false) {} + + virtual ~ColourCube() { + if (deleteTable) delete [] table; + } + + void set(int r, int g, int b, Pixel p) { + table[(r * nGreen + g) * nBlue + b] = p; + } + + Pixel lookup(int r, int g, int b) const { + return table[(r * nGreen + g) * nBlue + b]; + } + + int size() const { return nRed*nGreen*nBlue; } + int redMult() const { return nGreen*nBlue; } + int greenMult() const { return nBlue; } + int blueMult() const { return 1; } + + // ColourMap lookup() method. Note that this only works when the table has + // the default null mapping. + virtual void lookup(int i, int* r, int* g, int* b) { + if (i >= size()) return; + *b = i % nBlue; + i /= nBlue; + *g = i % nGreen; + *r = i / nGreen; + *r = (*r * 65535 + (nRed-1) / 2) / (nRed-1); + *g = (*g * 65535 + (nGreen-1) / 2) / (nGreen-1); + *b = (*b * 65535 + (nBlue-1) / 2) / (nBlue-1); + } + + int nRed; + int nGreen; + int nBlue; + Pixel* table; + bool deleteTable; + }; +} +#endif diff --git a/src/vnc/common/rfb/ColourMap.h b/src/vnc/common/rfb/ColourMap.h new file mode 100644 index 00000000..7868be05 --- /dev/null +++ b/src/vnc/common/rfb/ColourMap.h @@ -0,0 +1,36 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#ifndef __RFB_COLOURMAP_H__ +#define __RFB_COLOURMAP_H__ +namespace rfb { + struct Colour { + Colour() : r(0), g(0), b(0) {} + Colour(int r_, int g_, int b_) : r(r_), g(g_), b(b_) {} + virtual ~Colour() {} + int r, g, b; + bool operator==(const Colour& c) const {return c.r == r && c.g == g && c.b == b;} + bool operator!=(const Colour& c) const {return !(c == *this);} + }; + + class ColourMap { + public: + virtual ~ColourMap() {} + virtual void lookup(int index, int* r, int* g, int* b)=0; + }; +} +#endif diff --git a/src/vnc/common/rfb/ComparingUpdateTracker.cxx b/src/vnc/common/rfb/ComparingUpdateTracker.cxx new file mode 100644 index 00000000..ce3d68ae --- /dev/null +++ b/src/vnc/common/rfb/ComparingUpdateTracker.cxx @@ -0,0 +1,137 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ +#include +#include +#include +#include +#include + +using namespace rfb; + +ComparingUpdateTracker::ComparingUpdateTracker(PixelBuffer* buffer) + : fb(buffer), oldFb(fb->getPF(), 0, 0), firstCompare(true) +{ + changed.assign_union(fb->getRect()); +} + +ComparingUpdateTracker::~ComparingUpdateTracker() +{ +} + + +#define BLOCK_SIZE 16 + +void ComparingUpdateTracker::compare() +{ + std::vector rects; + std::vector::iterator i; + + if (firstCompare) { + // NB: We leave the change region untouched on this iteration, + // since in effect the entire framebuffer has changed. + oldFb.setSize(fb->width(), fb->height()); + for (int y=0; yheight(); y+=BLOCK_SIZE) { + Rect pos(0, y, fb->width(), __rfbmin(fb->height(), y+BLOCK_SIZE)); + int srcStride; + const rdr::U8* srcData = fb->getPixelsR(pos, &srcStride); + oldFb.imageRect(pos, srcData, srcStride); + } + firstCompare = false; + } else { + copied.get_rects(&rects, copy_delta.x<=0, copy_delta.y<=0); + for (i = rects.begin(); i != rects.end(); i++) + oldFb.copyRect(*i, copy_delta); + + Region to_check = changed.union_(copied); + to_check.get_rects(&rects); + + Region newChanged; + for (i = rects.begin(); i != rects.end(); i++) + compareRect(*i, &newChanged); + + copied.assign_subtract(newChanged); + changed = newChanged; + } +} + +void ComparingUpdateTracker::compareRect(const Rect& r, Region* newChanged) +{ + if (!r.enclosed_by(fb->getRect())) { + fprintf(stderr,"ComparingUpdateTracker: rect outside fb (%d,%d-%d,%d)\n", r.tl.x, r.tl.y, r.br.x, r.br.y); + return; + } + + int bytesPerPixel = fb->getPF().bpp/8; + int oldStride; + rdr::U8* oldData = oldFb.getPixelsRW(r, &oldStride); + int oldStrideBytes = oldStride * bytesPerPixel; + + std::vector changedBlocks; + + for (int blockTop = r.tl.y; blockTop < r.br.y; blockTop += BLOCK_SIZE) + { + // Get a strip of the source buffer + Rect pos(r.tl.x, blockTop, r.br.x, __rfbmin(r.br.y, blockTop+BLOCK_SIZE)); + int fbStride; + const rdr::U8* newBlockPtr = fb->getPixelsR(pos, &fbStride); + int newStrideBytes = fbStride * bytesPerPixel; + + rdr::U8* oldBlockPtr = oldData; + int blockBottom = __rfbmin(blockTop+BLOCK_SIZE, r.br.y); + + for (int blockLeft = r.tl.x; blockLeft < r.br.x; blockLeft += BLOCK_SIZE) + { + const rdr::U8* newPtr = newBlockPtr; + rdr::U8* oldPtr = oldBlockPtr; + + int blockRight = __rfbmin(blockLeft+BLOCK_SIZE, r.br.x); + int blockWidthInBytes = (blockRight-blockLeft) * bytesPerPixel; + + for (int y = blockTop; y < blockBottom; y++) + { + if (memcmp(oldPtr, newPtr, blockWidthInBytes) != 0) + { + // A block has changed - copy the remainder to the oldFb + changedBlocks.push_back(Rect(blockLeft, blockTop, + blockRight, blockBottom)); + for (int y2 = y; y2 < blockBottom; y2++) + { + memcpy(oldPtr, newPtr, blockWidthInBytes); + newPtr += newStrideBytes; + oldPtr += oldStrideBytes; + } + break; + } + + newPtr += newStrideBytes; + oldPtr += oldStrideBytes; + } + + oldBlockPtr += blockWidthInBytes; + newBlockPtr += blockWidthInBytes; + } + + oldData += oldStrideBytes * BLOCK_SIZE; + } + + if (!changedBlocks.empty()) { + Region temp; + temp.setOrderedRects(changedBlocks); + newChanged->assign_union(temp); + } +} diff --git a/src/vnc/common/rfb/ComparingUpdateTracker.h b/src/vnc/common/rfb/ComparingUpdateTracker.h new file mode 100644 index 00000000..5d2e5edf --- /dev/null +++ b/src/vnc/common/rfb/ComparingUpdateTracker.h @@ -0,0 +1,43 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifndef __RFB_COMPARINGUPDATETRACKER_H__ +#define __RFB_COMPARINGUPDATETRACKER_H__ + +#include + +namespace rfb { + + class ComparingUpdateTracker : public SimpleUpdateTracker { + public: + ComparingUpdateTracker(PixelBuffer* buffer); + ~ComparingUpdateTracker(); + + // compare() does the comparison and reduces its changed and copied regions + // as appropriate. + + virtual void compare(); + private: + void compareRect(const Rect& r, Region* newchanged); + PixelBuffer* fb; + ManagedPixelBuffer oldFb; + bool firstCompare; + }; + +} +#endif diff --git a/src/vnc/common/rfb/Configuration.cxx b/src/vnc/common/rfb/Configuration.cxx new file mode 100644 index 00000000..f41e64f1 --- /dev/null +++ b/src/vnc/common/rfb/Configuration.cxx @@ -0,0 +1,446 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software 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 software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// -=- Configuration.cxx + +#include +#include +#include +#include +#ifdef WIN32 +#define strcasecmp _stricmp +#define strncasecmp _strnicmp +#endif + +#include +#include +#include +#include +#include + +#ifdef __RFB_THREADING_IMPL +// On platforms that support Threading, we use Locks to make getData safe +#define LOCK_CONFIG Lock l(*configLock()) +rfb::Mutex* configLock_ = 0; +static rfb::Mutex* configLock() { + if (!configLock_) + configLock_ = new rfb::Mutex; + return configLock_; +} +#else +#define LOCK_CONFIG +#endif + +#include +#include + +using namespace rfb; + +static LogWriter vlog("Config"); + + +// -=- The Global Configuration object +Configuration* Configuration::global_ = 0; +Configuration* Configuration::global() { + if (!global_) + global_ = new Configuration("Global"); + return global_; +} + + +// -=- Configuration implementation + +Configuration::Configuration(const char* name_, Configuration* attachToGroup) +: name(strDup(name_)), head(0), _next(0) { + if (attachToGroup) { + _next = attachToGroup->_next; + attachToGroup->_next = this; + } +} + +Configuration& Configuration::operator=(const Configuration& src) { + VoidParameter* current = head; + while (current) { + VoidParameter* srcParam = ((Configuration&)src).get(current->getName()); + if (srcParam) { + current->immutable = false; + CharArray value(srcParam->getValueStr()); + vlog.debug("operator=(%s, %s)", current->getName(), value.buf); + current->setParam(value.buf); + } + current = current->_next; + } + if (_next) + *_next=src; + return *this; +} + +bool Configuration::set(const char* n, const char* v, bool immutable) { + return set(n, strlen(n), v, immutable); +} + +bool Configuration::set(const char* name, int len, + const char* val, bool immutable) +{ + VoidParameter* current = head; + while (current) { + if ((int)strlen(current->getName()) == len && + strncasecmp(current->getName(), name, len) == 0) + { + bool b = current->setParam(val); + if (b && immutable) + current->setImmutable(); + return b; + } + current = current->_next; + } + return _next ? _next->set(name, len, val, immutable) : false; +} + +bool Configuration::set(const char* config, bool immutable) { + bool hyphen = false; + if (config[0] == '-') { + hyphen = true; + config++; + if (config[0] == '-') config++; // allow gnu-style --