2004-03-26 20:59:26 +00:00
|
|
|
/*
|
|
|
|
* synergy -- mouse and keyboard sharing utility
|
|
|
|
* Copyright (C) 2004 Chris Schoeneman
|
|
|
|
*
|
|
|
|
* This package is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU General Public License
|
|
|
|
* found in the file COPYING that should have accompanied this file.
|
|
|
|
*
|
|
|
|
* This package is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "CMSWindowsDesks.h"
|
|
|
|
#include "CMSWindowsScreen.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 <malloc.h>
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
// 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 0x0100
|
|
|
|
#define MOUSEEVENTF_XUP 0x0200
|
|
|
|
#define XBUTTON1 0x0001
|
|
|
|
#define XBUTTON2 0x0002
|
|
|
|
#endif
|
|
|
|
#if !defined(VK_XBUTTON1)
|
|
|
|
#define VK_XBUTTON1 0x05
|
|
|
|
#define VK_XBUTTON2 0x06
|
|
|
|
#endif
|
|
|
|
|
|
|
|
// <unused>; <unused>
|
|
|
|
#define SYNERGY_MSG_SWITCH SYNERGY_HOOK_LAST_MSG + 1
|
|
|
|
// <unused>; <unused>
|
|
|
|
#define SYNERGY_MSG_ENTER SYNERGY_HOOK_LAST_MSG + 2
|
|
|
|
// <unused>; <unused>
|
|
|
|
#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
|
|
|
|
// delta; <unused>
|
|
|
|
#define SYNERGY_MSG_FAKE_WHEEL SYNERGY_HOOK_LAST_MSG + 7
|
|
|
|
// POINT*; <unused>
|
|
|
|
#define SYNERGY_MSG_CURSOR_POS SYNERGY_HOOK_LAST_MSG + 8
|
|
|
|
// IKeyState*; <unused>
|
|
|
|
#define SYNERGY_MSG_SYNC_KEYS SYNERGY_HOOK_LAST_MSG + 9
|
|
|
|
// install; <unused>
|
|
|
|
#define SYNERGY_MSG_SCREENSAVER SYNERGY_HOOK_LAST_MSG + 10
|
2004-05-02 08:04:15 +00:00
|
|
|
// dx; dy
|
|
|
|
#define SYNERGY_MSG_FAKE_REL_MOVE SYNERGY_HOOK_LAST_MSG + 11
|
2004-03-26 20:59:26 +00:00
|
|
|
|
|
|
|
//
|
|
|
|
// CMSWindowsDesks
|
|
|
|
//
|
|
|
|
|
|
|
|
CMSWindowsDesks::CMSWindowsDesks(
|
|
|
|
bool isPrimary, HINSTANCE hookLibrary,
|
|
|
|
const IScreenSaver* screensaver, IJob* updateKeys) :
|
|
|
|
m_isPrimary(isPrimary),
|
|
|
|
m_is95Family(CArchMiscWindows::isWindows95Family()),
|
|
|
|
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)
|
|
|
|
{
|
|
|
|
queryHookLibrary(hookLibrary);
|
|
|
|
m_cursor = createBlankCursor();
|
|
|
|
m_deskClass = createDeskWindowClass(m_isPrimary);
|
|
|
|
m_keyLayout = GetKeyboardLayout(GetCurrentThreadId());
|
|
|
|
}
|
|
|
|
|
|
|
|
CMSWindowsDesks::~CMSWindowsDesks()
|
|
|
|
{
|
|
|
|
disable();
|
|
|
|
destroyClass(m_deskClass);
|
|
|
|
destroyCursor(m_cursor);
|
|
|
|
delete m_updateKeys;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
CMSWindowsDesks::enable()
|
|
|
|
{
|
2004-06-12 20:48:04 +00:00
|
|
|
m_threadID = GetCurrentThreadId();
|
|
|
|
|
2004-03-26 20:59:26 +00:00
|
|
|
// 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<CMSWindowsDesks>(
|
|
|
|
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::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::getCursorPos(SInt32& x, SInt32& y) const
|
|
|
|
{
|
|
|
|
POINT pos;
|
|
|
|
sendMessage(SYNERGY_MSG_CURSOR_POS, reinterpret_cast<WPARAM>(&pos), 0);
|
|
|
|
x = pos.x;
|
|
|
|
y = pos.y;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
CMSWindowsDesks::fakeKeyEvent(
|
|
|
|
KeyButton button, UINT virtualKey,
|
|
|
|
bool press, bool /*isAutoRepeat*/) const
|
|
|
|
{
|
2004-03-28 14:07:58 +00:00
|
|
|
// 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
|
2004-03-26 20:59:26 +00:00
|
|
|
DWORD flags = 0;
|
|
|
|
if (((button & 0x100u) != 0)) {
|
|
|
|
flags |= KEYEVENTF_EXTENDEDKEY;
|
|
|
|
}
|
|
|
|
if (!press) {
|
|
|
|
flags |= KEYEVENTF_KEYUP;
|
|
|
|
}
|
|
|
|
sendMessage(SYNERGY_MSG_FAKE_KEY, flags,
|
|
|
|
MAKEWORD(static_cast<BYTE>(button & 0xffu),
|
|
|
|
static_cast<BYTE>(virtualKey & 0xffu)));
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
CMSWindowsDesks::fakeMouseButton(ButtonID button, bool press) const
|
|
|
|
{
|
|
|
|
// 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<WPARAM>(x),
|
|
|
|
static_cast<LPARAM>(y));
|
|
|
|
}
|
|
|
|
|
2004-05-02 08:04:15 +00:00
|
|
|
void
|
|
|
|
CMSWindowsDesks::fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const
|
|
|
|
{
|
|
|
|
sendMessage(SYNERGY_MSG_FAKE_REL_MOVE,
|
|
|
|
static_cast<WPARAM>(dx),
|
|
|
|
static_cast<LPARAM>(dy));
|
|
|
|
}
|
|
|
|
|
2004-03-26 20:59:26 +00:00
|
|
|
void
|
|
|
|
CMSWindowsDesks::fakeMouseWheel(SInt32 delta) const
|
|
|
|
{
|
|
|
|
sendMessage(SYNERGY_MSG_FAKE_WHEEL, delta, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
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_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::getInstance(),
|
|
|
|
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::getInstance();
|
|
|
|
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) {
|
2004-04-11 19:15:09 +00:00
|
|
|
UnregisterClass(reinterpret_cast<LPCTSTR>(windowClass),
|
|
|
|
CMSWindowsScreen::getInstance());
|
2004-03-26 20:59:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
HWND
|
|
|
|
CMSWindowsDesks::createWindow(ATOM windowClass, const char* name) const
|
|
|
|
{
|
|
|
|
HWND window = CreateWindowEx(WS_EX_TOPMOST |
|
|
|
|
WS_EX_TRANSPARENT |
|
|
|
|
WS_EX_TOOLWINDOW,
|
2004-04-11 19:15:09 +00:00
|
|
|
reinterpret_cast<LPCTSTR>(windowClass),
|
2004-03-26 20:59:26 +00:00
|
|
|
name,
|
|
|
|
WS_POPUP,
|
|
|
|
0, 0, 1, 1,
|
|
|
|
NULL, NULL,
|
|
|
|
CMSWindowsScreen::getInstance(),
|
|
|
|
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
|
2004-11-01 18:24:37 +00:00
|
|
|
// 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);
|
2004-03-26 20:59:26 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2004-11-01 18:24:37 +00:00
|
|
|
// windows 98 and Me are broken. you cannot set the absolute
|
2004-03-26 20:59:26 +00:00
|
|
|
// 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
|
2004-05-02 08:04:15 +00:00
|
|
|
// 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
|
2004-03-26 20:59:26 +00:00
|
|
|
// 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.
|
|
|
|
|
2004-05-02 08:04:15 +00:00
|
|
|
// 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);
|
|
|
|
}
|
2004-03-26 20:59:26 +00:00
|
|
|
|
2004-05-02 08:04:15 +00:00
|
|
|
// 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);
|
2004-03-26 20:59:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2004-07-29 21:50:17 +00:00
|
|
|
|
|
|
|
// this is here only because of the "ConsoleWindowClass" stuff in
|
|
|
|
// deskLeave.
|
|
|
|
EnableWindow(desk->m_window, desk->m_lowLevel ? FALSE : TRUE);
|
2004-03-26 20:59:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
CMSWindowsDesks::deskLeave(CDesk* desk, HKL keyLayout)
|
|
|
|
{
|
|
|
|
ShowCursor(FALSE);
|
|
|
|
if (m_isPrimary) {
|
|
|
|
// update key state
|
|
|
|
m_updateKeys->run();
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
2004-03-28 14:07:58 +00:00
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2004-07-29 21:50:17 +00:00
|
|
|
// if the active window is a console then activate our window.
|
|
|
|
// we do this because for some reason our hook reports unshifted
|
|
|
|
// characters when the shift is down and a console window is
|
|
|
|
// active. 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.
|
|
|
|
// FIXME -- figure out the real problem here and solve it.
|
|
|
|
else {
|
|
|
|
HWND foreground = GetForegroundWindow();
|
|
|
|
if (foreground != NULL) {
|
|
|
|
char className[40];
|
|
|
|
if (GetClassName(foreground, className,
|
|
|
|
sizeof(className) / sizeof(className[0])) &&
|
|
|
|
strcmp(className, "ConsoleWindowClass") == 0) {
|
|
|
|
EnableWindow(desk->m_window, TRUE);
|
|
|
|
SetActiveWindow(desk->m_window);
|
|
|
|
}
|
2004-11-01 22:10:34 +00:00
|
|
|
LOG((CLOG_DEBUG1 "active window class: %s", className));
|
2004-07-29 21:50:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2004-03-26 20:59:26 +00:00
|
|
|
// 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
|
|
|
|
deskMouseMove(m_xCenter, m_yCenter);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
CMSWindowsDesks::deskThread(void* vdesk)
|
|
|
|
{
|
|
|
|
MSG msg;
|
|
|
|
|
|
|
|
// use given desktop for this thread
|
|
|
|
CDesk* desk = reinterpret_cast<CDesk*>(vdesk);
|
|
|
|
desk->m_threadID = GetCurrentThreadId();
|
|
|
|
desk->m_window = 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_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;
|
|
|
|
}
|
2004-03-28 14:07:58 +00:00
|
|
|
|
|
|
|
// a window on the primary screen with low-level hooks
|
|
|
|
// should never activate.
|
|
|
|
EnableWindow(desk->m_window, desk->m_lowLevel ? FALSE : TRUE);
|
2004-03-26 20:59:26 +00:00
|
|
|
}
|
|
|
|
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), msg.wParam, 0);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SYNERGY_MSG_FAKE_BUTTON:
|
|
|
|
if (msg.wParam != 0) {
|
|
|
|
mouse_event(msg.wParam, 0, 0, msg.lParam, 0);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SYNERGY_MSG_FAKE_MOVE:
|
|
|
|
deskMouseMove(static_cast<SInt32>(msg.wParam),
|
|
|
|
static_cast<SInt32>(msg.lParam));
|
|
|
|
break;
|
|
|
|
|
2004-05-02 08:04:15 +00:00
|
|
|
case SYNERGY_MSG_FAKE_REL_MOVE:
|
|
|
|
deskMouseRelativeMove(static_cast<SInt32>(msg.wParam),
|
|
|
|
static_cast<SInt32>(msg.lParam));
|
|
|
|
break;
|
|
|
|
|
2004-03-26 20:59:26 +00:00
|
|
|
case SYNERGY_MSG_FAKE_WHEEL:
|
|
|
|
mouse_event(MOUSEEVENTF_WHEEL, 0, 0, msg.wParam, 0);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SYNERGY_MSG_CURSOR_POS: {
|
|
|
|
POINT* pos = reinterpret_cast<POINT*>(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 (msg.wParam != 0) {
|
|
|
|
m_installScreensaver();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
m_uninstallScreensaver();
|
|
|
|
}
|
|
|
|
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<CMSWindowsDesks>(
|
|
|
|
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 isAccessible = isDeskAccessible(desk);
|
|
|
|
if (isDeskAccessible(m_activeDesk) != isAccessible) {
|
|
|
|
if (isAccessible) {
|
|
|
|
LOG((CLOG_DEBUG "desktop is now accessible"));
|
|
|
|
sendMessage(SYNERGY_MSG_SYNC_KEYS, 0, 0);
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2004-06-12 20:48:04 +00:00
|
|
|
else if (name != m_activeDeskName) {
|
|
|
|
// screen saver might have started
|
|
|
|
PostThreadMessage(m_threadID, SYNERGY_MSG_SCREEN_SAVER, TRUE, 0);
|
|
|
|
}
|
2004-03-26 20:59:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
CMSWindowsDesks::isDeskAccessible(const CDesk* desk) const
|
|
|
|
{
|
|
|
|
return (desk != NULL && desk->m_desk != NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
CMSWindowsDesks::waitForDesk() const
|
|
|
|
{
|
|
|
|
CMSWindowsDesks* self = const_cast<CMSWindowsDesks*>(this);
|
|
|
|
|
|
|
|
CLock lock(&m_mutex);
|
|
|
|
while (!(bool)m_deskReady) {
|
|
|
|
m_deskReady.wait();
|
|
|
|
}
|
|
|
|
self->m_deskReady = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
CMSWindowsDesks::handleCheckDesk(const CEvent&, void*)
|
|
|
|
{
|
|
|
|
checkDesk();
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|