barrier/server/CMSWindowsPrimaryScreen.cpp
crs a16e7217ce fixed bugs in mouse motion. wasn't taking care to capture all
motion events relative to the previous mouse position.  for
example, if two mouse events arrive, the first at x+1,y and
the second at x+2,y, we used to compute deltas of 1,0 and 2,0
instead of 1,0 and 1,0.  that's fixed.  also worked around a
bug (probably) in windows that caused a motion event after a
SetCursorPos() to be lost or reported one pixel off from the
correct position.  now using mouse_event() which doesn't
have that problem.  also fixed calculation of normalized
coordinates for mouse_event() when there are multiple
displays.
2002-06-19 20:24:35 +00:00

1455 lines
37 KiB
C++

#include "CMSWindowsPrimaryScreen.h"
#include "CServer.h"
#include "CMSWindowsClipboard.h"
#include "CPlatform.h"
#include "XScreen.h"
#include "XSynergy.h"
#include "CThread.h"
#include "CLog.h"
#include <cstring>
//
// CMSWindowsPrimaryScreen
//
CMSWindowsPrimaryScreen::CMSWindowsPrimaryScreen() :
m_server(NULL),
m_threadID(0),
m_desk(NULL),
m_deskName(),
m_window(NULL),
m_active(false),
m_mark(0),
m_markReceived(0),
m_nextClipboardWindow(NULL),
m_clipboardOwner(NULL)
{
// load the hook library
m_hookLibrary = LoadLibrary("synrgyhk");
if (m_hookLibrary == NULL) {
log((CLOG_ERR "failed to load hook library"));
throw XScreenOpenFailure();
}
m_setZone = (SetZoneFunc)GetProcAddress(m_hookLibrary, "setZone");
m_setRelay = (SetRelayFunc)GetProcAddress(m_hookLibrary, "setRelay");
m_install = (InstallFunc)GetProcAddress(m_hookLibrary, "install");
m_uninstall = (UninstallFunc)GetProcAddress(m_hookLibrary, "uninstall");
if (m_setZone == NULL ||
m_setRelay == NULL ||
m_install == NULL ||
m_uninstall == NULL) {
log((CLOG_ERR "invalid hook library"));
FreeLibrary(m_hookLibrary);
throw XScreenOpenFailure();
}
// detect operating system
m_is95Family = CPlatform::isWindows95Family();
// make sure this thread has a message queue
MSG dummy;
PeekMessage(&dummy, NULL, WM_USER, WM_USER, PM_NOREMOVE);
}
CMSWindowsPrimaryScreen::~CMSWindowsPrimaryScreen()
{
assert(m_hookLibrary != NULL);
assert(m_window == NULL);
// done with hook library
FreeLibrary(m_hookLibrary);
}
void
CMSWindowsPrimaryScreen::run()
{
// must call run() from same thread as open()
assert(m_threadID == GetCurrentThreadId());
// change our priority
CThread::getCurrentThread().setPriority(-3);
// poll input desktop to see if it changes (preTranslateMessage()
// handles WM_TIMER)
UINT timer = 0;
if (!m_is95Family) {
SetTimer(NULL, 0, 200, NULL);
}
// run event loop
log((CLOG_INFO "entering event loop"));
doRun();
log((CLOG_INFO "exiting event loop"));
// remove timer
if (!m_is95Family) {
KillTimer(NULL, timer);
}
}
void
CMSWindowsPrimaryScreen::stop()
{
doStop();
}
void
CMSWindowsPrimaryScreen::open(CServer* server)
{
assert(m_server == NULL);
assert(server != NULL);
// set the server
m_server = server;
// open the display
openDisplay();
// initialize marks
m_mark = 0;
m_markReceived = 0;
nextMark();
// save cursor pos
POINT pos;
GetCursorPos(&pos);
m_x = pos.x;
m_y = pos.y;
// send screen info
SInt32 x, y, w, h;
getScreenShape(x, y, w, h);
m_server->setInfo(x, y, w, h, getJumpZoneSize(), m_x, m_y);
// compute center pixel of primary screen
m_xCenter = GetSystemMetrics(SM_CXSCREEN) >> 1;
m_yCenter = GetSystemMetrics(SM_CYSCREEN) >> 1;
// get keyboard state
updateKeys();
// enter the screen
enterNoWarp();
}
void
CMSWindowsPrimaryScreen::close()
{
assert(m_server != NULL);
// close the display
closeDisplay();
// done with server
m_server = NULL;
}
void
CMSWindowsPrimaryScreen::enter(SInt32 x, SInt32 y)
{
log((CLOG_INFO "entering primary at %d,%d", x, y));
assert(m_active == true);
// enter the screen
enterNoWarp();
// warp to requested location
warpCursor(x, y);
}
bool
CMSWindowsPrimaryScreen::leave()
{
log((CLOG_INFO "leaving primary"));
assert(m_active == false);
// all messages prior to now are invalid
nextMark();
// save active window, show ours, and grab mouse/keyboard
if (!onLeave()) {
return false;
}
// relay all mouse and keyboard events
m_setRelay();
// get state of keys as we leave
updateKeys();
// warp mouse to center of screen
warpCursorToCenter();
// ignore this many mouse motion events (not including the already
// queued events). on (at least) the win2k login desktop, one
// motion event is reported using a position from before the above
// warpCursor(). i don't know why it does that and other desktops
// don't have the same problem. anyway, simply ignoring that event
// works around it.
// FIXME -- is this true now that we're using mouse_event?
m_mouseMoveIgnore = 1;
// disable ctrl+alt+del, alt+tab, etc
if (m_is95Family) {
DWORD dummy = 0;
SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, TRUE, &dummy, 0);
}
// discard messages until after the warp
nextMark();
// local client now active
m_active = true;
// 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.
HWND clipboardOwner = GetClipboardOwner();
if (m_clipboardOwner != clipboardOwner) {
try {
m_clipboardOwner = clipboardOwner;
if (m_clipboardOwner != m_window) {
m_server->grabClipboard(kClipboardClipboard);
m_server->grabClipboard(kClipboardSelection);
}
}
catch (XBadClient&) {
// ignore
}
}
return true;
}
void
CMSWindowsPrimaryScreen::onConfigure()
{
if ((m_is95Family || m_desk != NULL) && !m_active) {
SInt32 x, y, w, h;
getScreenShape(x, y, w, h);
m_setZone(m_server->getActivePrimarySides(),
x, y, w, h, getJumpZoneSize());
}
}
void
CMSWindowsPrimaryScreen::warpCursor(SInt32 x, SInt32 y)
{
// set the cursor position without generating an event
SetCursorPos(x, y);
// save position as last position
m_x = x;
m_y = y;
}
void
CMSWindowsPrimaryScreen::warpCursorToCenter()
{
// warp to center. the extra info tells the hook DLL to send
// SYNERGY_MSG_POST_WARP instead of SYNERGY_MSG_MOUSE_MOVE.
SInt32 x, y, w, h;
getScreenShape(x, y, w, h);
mouse_event(MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE,
(DWORD)((65535.99 * (m_xCenter - x)) / (w - 1)),
(DWORD)((65535.99 * (m_yCenter - y)) / (h - 1)),
0,
0x12345678);
}
void
CMSWindowsPrimaryScreen::setClipboard(ClipboardID /*id*/,
const IClipboard* src)
{
assert(m_window != NULL);
CMSWindowsClipboard dst(m_window);
CClipboard::copy(&dst, src);
}
void
CMSWindowsPrimaryScreen::grabClipboard(ClipboardID /*id*/)
{
assert(m_window != NULL);
CMSWindowsClipboard clipboard(m_window);
if (clipboard.open(0)) {
clipboard.close();
}
}
void
CMSWindowsPrimaryScreen::getShape(
SInt32& x, SInt32& y, SInt32& w, SInt32& h) const
{
getScreenShape(x, y, w, h);
}
SInt32
CMSWindowsPrimaryScreen::getJumpZoneSize() const
{
return 1;
}
void
CMSWindowsPrimaryScreen::getClipboard(ClipboardID /*id*/,
IClipboard* dst) const
{
assert(m_window != NULL);
CMSWindowsClipboard src(m_window);
CClipboard::copy(dst, &src);
}
KeyModifierMask
CMSWindowsPrimaryScreen::getToggleMask() const
{
KeyModifierMask mask = 0;
if ((GetKeyState(VK_CAPITAL) & 0x01) != 0)
mask |= KeyModifierCapsLock;
if ((GetKeyState(VK_NUMLOCK) & 0x01) != 0)
mask |= KeyModifierNumLock;
if ((GetKeyState(VK_SCROLL) & 0x01) != 0)
mask |= KeyModifierScrollLock;
return mask;
}
bool
CMSWindowsPrimaryScreen::isLockedToScreen() const
{
// check buttons
if (GetAsyncKeyState(VK_LBUTTON) < 0 ||
GetAsyncKeyState(VK_MBUTTON) < 0 ||
GetAsyncKeyState(VK_RBUTTON) < 0) {
return true;
}
// check keys
BYTE keys[256];
if (GetKeyboardState(keys)) {
for (unsigned int i = 0; i < sizeof(keys); ++i) {
if ((keys[i] & 0x80) != 0) {
return true;
}
}
}
// not locked
return false;
}
void
CMSWindowsPrimaryScreen::onOpenDisplay()
{
assert(m_window == NULL);
assert(m_server != NULL);
// save thread id. we'll need to pass this to the hook library.
m_threadID = GetCurrentThreadId();
// get the input desktop and switch to it
if (m_is95Family) {
if (!openDesktop()) {
throw XScreenOpenFailure();
}
}
else {
if (!switchDesktop(openInputDesktop())) {
throw XScreenOpenFailure();
}
}
}
void
CMSWindowsPrimaryScreen::onCloseDisplay()
{
// disconnect from desktop
if (m_is95Family) {
closeDesktop();
}
else {
switchDesktop(NULL);
}
// clear thread id
m_threadID = 0;
assert(m_window == NULL);
assert(m_desk == NULL);
}
bool
CMSWindowsPrimaryScreen::onPreTranslate(MSG* msg)
{
// handle event
switch (msg->message) {
case SYNERGY_MSG_MARK:
m_markReceived = msg->wParam;
return true;
case SYNERGY_MSG_KEY:
// ignore message if posted prior to last mark change
if (m_markReceived == m_mark) {
KeyModifierMask mask;
const KeyID key = mapKey(msg->wParam, msg->lParam, &mask);
if (key != kKeyNone) {
if ((msg->lParam & 0x80000000) == 0) {
// key press
const SInt32 repeat = (SInt32)(msg->lParam & 0xffff);
if (repeat >= 2) {
log((CLOG_DEBUG1 "event: key repeat key=%d mask=0x%04x count=%d", key, mask, repeat));
m_server->onKeyRepeat(key, mask, repeat);
}
else {
log((CLOG_DEBUG1 "event: key press key=%d mask=0x%04x", key, mask));
m_server->onKeyDown(key, mask);
}
// update key state
updateKey(msg->wParam, true);
}
else {
// key release
log((CLOG_DEBUG1 "event: key release key=%d mask=0x%04x", key, mask));
m_server->onKeyUp(key, mask);
// update key state
updateKey(msg->wParam, false);
}
}
else {
log((CLOG_DEBUG2 "event: cannot map key wParam=%d lParam=0x%08x", msg->wParam, msg->lParam));
}
}
return true;
case SYNERGY_MSG_MOUSE_BUTTON:
// ignore message if posted prior to last mark change
if (m_markReceived == m_mark) {
const ButtonID button = mapButton(msg->wParam);
switch (msg->wParam) {
case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
log((CLOG_DEBUG1 "event: button press button=%d", button));
if (button != kButtonNone) {
m_server->onMouseDown(button);
}
break;
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
log((CLOG_DEBUG1 "event: button release button=%d", button));
if (button != kButtonNone) {
m_server->onMouseUp(button);
}
break;
}
}
return true;
case SYNERGY_MSG_MOUSE_WHEEL:
// ignore message if posted prior to last mark change
if (m_markReceived == m_mark) {
log((CLOG_ERR "event: button wheel delta=%d %d", msg->wParam, msg->lParam));
m_server->onMouseWheel(msg->wParam);
}
return true;
case SYNERGY_MSG_MOUSE_MOVE:
// ignore message if posted prior to last mark change
if (m_markReceived == m_mark) {
SInt32 x = static_cast<SInt32>(msg->wParam);
SInt32 y = static_cast<SInt32>(msg->lParam);
if (!m_active) {
m_server->onMouseMovePrimary(x, y);
}
else {
// compute motion delta. this is relative to the
// last known mouse position.
x -= m_x;
y -= m_y;
// save position to compute delta of next motion
m_x = static_cast<SInt32>(msg->wParam);
m_y = static_cast<SInt32>(msg->lParam);
// ignore if the mouse didn't move or we're ignoring
// motion.
if (m_mouseMoveIgnore == 0) {
if (x != 0 || y != 0) {
// back to center
warpCursorToCenter();
// send motion
m_server->onMouseMoveSecondary(x, y);
}
}
else {
// ignored one more motion event
--m_mouseMoveIgnore;
}
}
}
return true;
case SYNERGY_MSG_POST_WARP:
m_x = static_cast<SInt32>(msg->wParam);
m_y = static_cast<SInt32>(msg->lParam);
return true;
case WM_TIMER:
// if current desktop is not the input desktop then switch to it
if (!m_is95Family) {
HDESK desk = openInputDesktop();
if (desk != NULL) {
if (isCurrentDesktop(desk)) {
CloseDesktop(desk);
}
else {
switchDesktop(desk);
}
}
}
return true;
}
return false;
}
LRESULT
CMSWindowsPrimaryScreen::onEvent(HWND hwnd, UINT msg,
WPARAM wParam, LPARAM lParam)
{
switch (msg) {
case WM_QUERYENDSESSION:
if (m_is95Family) {
return TRUE;
}
break;
case WM_ENDSESSION:
if (m_is95Family) {
if (wParam == TRUE && lParam == 0) {
stop();
}
return 0;
}
break;
case WM_PAINT:
ValidateRect(hwnd, NULL);
return 0;
case WM_DRAWCLIPBOARD:
log((CLOG_DEBUG "clipboard was taken"));
// first pass it on
if (m_nextClipboardWindow != NULL) {
SendMessage(m_nextClipboardWindow, msg, wParam, lParam);
}
// now notify server that somebody changed the clipboard.
// skip that if we're the new owner.
try {
m_clipboardOwner = GetClipboardOwner();
if (m_clipboardOwner != m_window) {
m_server->grabClipboard(kClipboardClipboard);
m_server->grabClipboard(kClipboardSelection);
}
}
catch (XBadClient&) {
// ignore. this can happen if we receive this event
// before we've fully started up.
}
return 0;
case WM_CHANGECBCHAIN:
if (m_nextClipboardWindow == (HWND)wParam) {
m_nextClipboardWindow = (HWND)lParam;
}
else if (m_nextClipboardWindow != NULL) {
SendMessage(m_nextClipboardWindow, msg, wParam, lParam);
}
return 0;
case WM_DISPLAYCHANGE:
{
// screen resolution may have changed
SInt32 xOld, yOld, wOld, hOld;
getScreenShape(xOld, yOld, wOld, hOld);
updateScreenShape();
SInt32 x, y, w, h;
getScreenShape(x, y, w, h);
// do nothing if resolution hasn't changed
if (x != xOld || y != yOld || w != wOld || h != hOld) {
// recompute center pixel of primary screen
m_xCenter = GetSystemMetrics(SM_CXSCREEN) >> 1;
m_yCenter = GetSystemMetrics(SM_CYSCREEN) >> 1;
// warp mouse to center if active
if (m_active) {
warpCursorToCenter();
}
// tell hook about resize if not active
else {
onConfigure();
}
// send new screen info
POINT pos;
GetCursorPos(&pos);
m_server->setInfo(x, y, w, h, getJumpZoneSize(), pos.x, pos.y);
}
return 0;
}
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
void
CMSWindowsPrimaryScreen::enterNoWarp()
{
// not active anymore
m_active = false;
// reset motion ignore count
m_mouseMoveIgnore = 0;
// enable ctrl+alt+del, alt+tab, etc
if (m_is95Family) {
DWORD dummy = 0;
SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, FALSE, &dummy, 0);
}
// install jump zones
onConfigure();
// restore active window and hide our window
onEnter();
// all messages prior to now are invalid
nextMark();
}
void
CMSWindowsPrimaryScreen::onEnter()
{
// restore the active window and hide our window. we can only set
// the active window for another thread if we first attach our input
// to that thread.
ReleaseCapture();
if (m_lastActiveWindow != NULL) {
DWORD myThread = GetCurrentThreadId();
if (AttachThreadInput(myThread, m_lastActiveThread, TRUE)) {
// FIXME -- shouldn't raise window if X-Mouse is enabled
// but i have no idea how to do that or check if enabled.
SetActiveWindow(m_lastActiveWindow);
AttachThreadInput(myThread, m_lastActiveThread, FALSE);
}
}
ShowWindow(m_window, SW_HIDE);
}
bool
CMSWindowsPrimaryScreen::onLeave()
{
// remember the active window before we leave. GetActiveWindow()
// will only return the active window for the thread's queue (i.e.
// our app) but we need the globally active window. get that by
// attaching input to the foreground window's thread then calling
// GetActiveWindow() and then detaching our input.
m_lastActiveWindow = NULL;
m_lastForegroundWindow = GetForegroundWindow();
m_lastActiveThread = GetWindowThreadProcessId(
m_lastForegroundWindow, NULL);
if (m_lastActiveThread != 0) {
DWORD myThread = GetCurrentThreadId();
if (AttachThreadInput(myThread, m_lastActiveThread, TRUE)) {
m_lastActiveWindow = GetActiveWindow();
AttachThreadInput(myThread, m_lastActiveThread, FALSE);
}
}
// show our window
ShowWindow(m_window, SW_SHOW);
// get keyboard input and capture mouse
SetActiveWindow(m_window);
SetFocus(m_window);
SetCapture(m_window);
return true;
}
void
CMSWindowsPrimaryScreen::nextMark()
{
// next mark
++m_mark;
// mark point in message queue where the mark was changed
PostThreadMessage(m_threadID, SYNERGY_MSG_MARK, m_mark, 0);
}
bool
CMSWindowsPrimaryScreen::openDesktop()
{
// install hooks
m_install(m_threadID);
// note -- we use a fullscreen window to grab input. it should
// be possible to use a 1x1 window but i've run into problems
// with losing keyboard input (focus?) in that case.
// unfortunately, hiding the full screen window (when entering
// the scren causes all other windows to redraw).
SInt32 x, y, w, h;
getScreenShape(x, y, w, h);
// create the window
m_window = CreateWindowEx(WS_EX_TOPMOST |
WS_EX_TRANSPARENT |
WS_EX_TOOLWINDOW,
(LPCTSTR)getClass(),
"Synergy",
WS_POPUP,
x, y, w, h, NULL, NULL,
getInstance(),
NULL);
if (m_window == NULL) {
log((CLOG_ERR "failed to create window: %d", GetLastError()));
m_uninstall();
return false;
}
// install our clipboard snooper
m_nextClipboardWindow = SetClipboardViewer(m_window);
return true;
}
void
CMSWindowsPrimaryScreen::closeDesktop()
{
// destroy old window
if (m_window != NULL) {
// restore active window and hide ours
if (m_active) {
onEnter();
}
// first remove clipboard snooper
ChangeClipboardChain(m_window, m_nextClipboardWindow);
m_nextClipboardWindow = NULL;
// we no longer own the clipboard
if (m_clipboardOwner == m_window) {
m_clipboardOwner = NULL;
}
// now destroy window
DestroyWindow(m_window);
m_window = NULL;
}
// unhook
m_uninstall();
}
bool
CMSWindowsPrimaryScreen::switchDesktop(HDESK desk)
{
// did we own the clipboard?
bool ownClipboard = (m_clipboardOwner == m_window);
// destroy old window
if (m_window != NULL) {
// restore active window and hide ours
if (m_active) {
onEnter();
}
// first remove clipboard snooper
ChangeClipboardChain(m_window, m_nextClipboardWindow);
m_nextClipboardWindow = NULL;
// we no longer own the clipboard
if (ownClipboard) {
m_clipboardOwner = NULL;
}
// now destroy window
DestroyWindow(m_window);
m_window = NULL;
}
// unhook
if (m_desk != NULL) {
m_uninstall();
CloseDesktop(m_desk);
m_desk = NULL;
m_deskName = "";
}
// if no new desktop then we're done
if (desk == NULL) {
log((CLOG_INFO "disconnecting desktop"));
return true;
}
// set the desktop. can only do this when there are no windows
// and hooks on the current desktop owned by this thread.
if (SetThreadDesktop(desk) == 0) {
log((CLOG_ERR "failed to set desktop: %d", GetLastError()));
CloseDesktop(desk);
return false;
}
// install hooks
m_install(m_threadID);
// note -- we use a fullscreen window to grab input. it should
// be possible to use a 1x1 window but i've run into problems
// with losing keyboard input (focus?) in that case.
// unfortunately, hiding the full screen window (when entering
// the scren causes all other windows to redraw).
SInt32 x, y, w, h;
getScreenShape(x, y, w, h);
// create the window
m_window = CreateWindowEx(WS_EX_TOPMOST |
WS_EX_TRANSPARENT |
WS_EX_TOOLWINDOW,
(LPCTSTR)getClass(),
"Synergy",
WS_POPUP,
x, y, w, h, NULL, NULL,
getInstance(),
NULL);
if (m_window == NULL) {
log((CLOG_ERR "failed to create window: %d", GetLastError()));
m_uninstall();
CloseDesktop(desk);
return false;
}
// install our clipboard snooper
m_nextClipboardWindow = SetClipboardViewer(m_window);
// reassert clipboard ownership
if (ownClipboard) {
// FIXME
}
// save new desktop
m_desk = desk;
m_deskName = getDesktopName(m_desk);
log((CLOG_INFO "switched to desktop %s", m_deskName.c_str()));
// get active window and show ours
if (m_active) {
onLeave();
}
else {
// set jump zones
onConfigure();
// all messages prior to now are invalid
nextMark();
}
return true;
}
CString
CMSWindowsPrimaryScreen::getCurrentDesktopName() const
{
return m_deskName;
}
static const KeyID g_virtualKey[] =
{
/* 0x00 */ kKeyNone, // reserved
/* 0x01 */ kKeyNone, // VK_LBUTTON
/* 0x02 */ kKeyNone, // VK_RBUTTON
/* 0x03 */ 0xff6b, // VK_CANCEL XK_Break
/* 0x04 */ kKeyNone, // VK_MBUTTON
/* 0x05 */ kKeyNone, // undefined
/* 0x06 */ kKeyNone, // undefined
/* 0x07 */ kKeyNone, // undefined
/* 0x08 */ 0xff08, // VK_BACK XK_Backspace
/* 0x09 */ 0xff09, // VK_TAB VK_Tab
/* 0x0a */ kKeyNone, // undefined
/* 0x0b */ kKeyNone, // undefined
/* 0x0c */ 0xff0b, // VK_CLEAR XK_Clear
/* 0x0d */ 0xff0d, // VK_RETURN XK_Return
/* 0x0e */ kKeyNone, // undefined
/* 0x0f */ kKeyNone, // undefined
/* 0x10 */ 0xffe1, // VK_SHIFT XK_Shift_L
/* 0x11 */ 0xffe3, // VK_CONTROL XK_Control_L
/* 0x12 */ 0xffe9, // VK_MENU XK_Alt_L
/* 0x13 */ 0xff13, // VK_PAUSE XK_Pause
/* 0x14 */ 0xffe5, // VK_CAPITAL XK_Caps_Lock
/* 0x15 */ kKeyNone, // VK_KANA
/* 0x16 */ kKeyNone, // VK_HANGUL
/* 0x17 */ kKeyNone, // VK_JUNJA
/* 0x18 */ kKeyNone, // VK_FINAL
/* 0x19 */ kKeyNone, // VK_KANJI
/* 0x1a */ kKeyNone, // undefined
/* 0x1b */ 0xff1b, // VK_ESCAPE XK_Escape
/* 0x1c */ kKeyNone, // VK_CONVERT
/* 0x1d */ kKeyNone, // VK_NONCONVERT
/* 0x1e */ kKeyNone, // VK_ACCEPT
/* 0x1f */ kKeyNone, // VK_MODECHANGE
/* 0x20 */ 0x0020, // VK_SPACE XK_space
/* 0x21 */ 0xff55, // VK_PRIOR XK_Prior
/* 0x22 */ 0xff56, // VK_NEXT XK_Next
/* 0x23 */ 0xff57, // VK_END XK_End
/* 0x24 */ 0xff50, // VK_HOME XK_Home
/* 0x25 */ 0xff51, // VK_LEFT XK_Left
/* 0x26 */ 0xff52, // VK_UP XK_Up
/* 0x27 */ 0xff53, // VK_RIGHT XK_Right
/* 0x28 */ 0xff54, // VK_DOWN XK_Down
/* 0x29 */ 0xff60, // VK_SELECT XK_Select
/* 0x2a */ kKeyNone, // VK_PRINT
/* 0x2b */ 0xff62, // VK_EXECUTE XK_Execute
/* 0x2c */ 0xff61, // VK_SNAPSHOT XK_Print
/* 0x2d */ 0xff63, // VK_INSERT XK_Insert
/* 0x2e */ 0xffff, // VK_DELETE XK_Delete
/* 0x2f */ 0xff6a, // VK_HELP XK_Help
/* 0x30 */ kKeyNone, // VK_0 XK_0
/* 0x31 */ kKeyNone, // VK_1 XK_1
/* 0x32 */ kKeyNone, // VK_2 XK_2
/* 0x33 */ kKeyNone, // VK_3 XK_3
/* 0x34 */ kKeyNone, // VK_4 XK_4
/* 0x35 */ kKeyNone, // VK_5 XK_5
/* 0x36 */ kKeyNone, // VK_6 XK_6
/* 0x37 */ kKeyNone, // VK_7 XK_7
/* 0x38 */ kKeyNone, // VK_8 XK_8
/* 0x39 */ kKeyNone, // VK_9 XK_9
/* 0x3a */ kKeyNone, // undefined
/* 0x3b */ kKeyNone, // undefined
/* 0x3c */ kKeyNone, // undefined
/* 0x3d */ kKeyNone, // undefined
/* 0x3e */ kKeyNone, // undefined
/* 0x3f */ kKeyNone, // undefined
/* 0x40 */ kKeyNone, // undefined
/* 0x41 */ kKeyNone, // VK_A XK_A
/* 0x42 */ kKeyNone, // VK_B XK_B
/* 0x43 */ kKeyNone, // VK_C XK_C
/* 0x44 */ kKeyNone, // VK_D XK_D
/* 0x45 */ kKeyNone, // VK_E XK_E
/* 0x46 */ kKeyNone, // VK_F XK_F
/* 0x47 */ kKeyNone, // VK_G XK_G
/* 0x48 */ kKeyNone, // VK_H XK_H
/* 0x49 */ kKeyNone, // VK_I XK_I
/* 0x4a */ kKeyNone, // VK_J XK_J
/* 0x4b */ kKeyNone, // VK_K XK_K
/* 0x4c */ kKeyNone, // VK_L XK_L
/* 0x4d */ kKeyNone, // VK_M XK_M
/* 0x4e */ kKeyNone, // VK_N XK_N
/* 0x4f */ kKeyNone, // VK_O XK_O
/* 0x50 */ kKeyNone, // VK_P XK_P
/* 0x51 */ kKeyNone, // VK_Q XK_Q
/* 0x52 */ kKeyNone, // VK_R XK_R
/* 0x53 */ kKeyNone, // VK_S XK_S
/* 0x54 */ kKeyNone, // VK_T XK_T
/* 0x55 */ kKeyNone, // VK_U XK_U
/* 0x56 */ kKeyNone, // VK_V XK_V
/* 0x57 */ kKeyNone, // VK_W XK_W
/* 0x58 */ kKeyNone, // VK_X XK_X
/* 0x59 */ kKeyNone, // VK_Y XK_Y
/* 0x5a */ kKeyNone, // VK_Z XK_Z
/* 0x5b */ 0xffe7, // VK_LWIN XK_Meta_L
/* 0x5c */ 0xffe8, // VK_RWIN XK_Meta_R
/* 0x5d */ 0xff67, // VK_APPS XK_Menu
/* 0x5e */ kKeyNone, // undefined
/* 0x5f */ kKeyNone, // undefined
/* 0x60 */ 0xffb0, // VK_NUMPAD0 XK_KP_0
/* 0x61 */ 0xffb1, // VK_NUMPAD1 XK_KP_1
/* 0x62 */ 0xffb2, // VK_NUMPAD2 XK_KP_2
/* 0x63 */ 0xffb3, // VK_NUMPAD3 XK_KP_3
/* 0x64 */ 0xffb4, // VK_NUMPAD4 XK_KP_4
/* 0x65 */ 0xffb5, // VK_NUMPAD5 XK_KP_5
/* 0x66 */ 0xffb6, // VK_NUMPAD6 XK_KP_6
/* 0x67 */ 0xffb7, // VK_NUMPAD7 XK_KP_7
/* 0x68 */ 0xffb8, // VK_NUMPAD8 XK_KP_8
/* 0x69 */ 0xffb9, // VK_NUMPAD9 XK_KP_9
/* 0x6a */ 0xffaa, // VK_MULTIPLY XK_KP_Multiply
/* 0x6b */ 0xffab, // VK_ADD XK_KP_Add
/* 0x6c */ 0xffac, // VK_SEPARATOR XK_KP_Separator
/* 0x6d */ 0xffad, // VK_SUBTRACT XK_KP_Subtract
/* 0x6e */ 0xffae, // VK_DECIMAL XK_KP_Decimal
/* 0x6f */ 0xffaf, // VK_DIVIDE XK_KP_Divide
/* 0x70 */ 0xffbe, // VK_F1 XK_F1
/* 0x71 */ 0xffbf, // VK_F2 XK_F2
/* 0x72 */ 0xffc0, // VK_F3 XK_F3
/* 0x73 */ 0xffc1, // VK_F4 XK_F4
/* 0x74 */ 0xffc2, // VK_F5 XK_F5
/* 0x75 */ 0xffc3, // VK_F6 XK_F6
/* 0x76 */ 0xffc4, // VK_F7 XK_F7
/* 0x77 */ 0xffc5, // VK_F8 XK_F8
/* 0x78 */ 0xffc6, // VK_F9 XK_F9
/* 0x79 */ 0xffc7, // VK_F10 XK_F10
/* 0x7a */ 0xffc8, // VK_F11 XK_F11
/* 0x7b */ 0xffc9, // VK_F12 XK_F12
/* 0x7c */ 0xffca, // VK_F13 XK_F13
/* 0x7d */ 0xffcb, // VK_F14 XK_F14
/* 0x7e */ 0xffcc, // VK_F15 XK_F15
/* 0x7f */ 0xffcd, // VK_F16 XK_F16
/* 0x80 */ 0xffce, // VK_F17 XK_F17
/* 0x81 */ 0xffcf, // VK_F18 XK_F18
/* 0x82 */ 0xffd0, // VK_F19 XK_F19
/* 0x83 */ 0xffd1, // VK_F20 XK_F20
/* 0x84 */ 0xffd2, // VK_F21 XK_F21
/* 0x85 */ 0xffd3, // VK_F22 XK_F22
/* 0x86 */ 0xffd4, // VK_F23 XK_F23
/* 0x87 */ 0xffd5, // VK_F24 XK_F24
/* 0x88 */ kKeyNone, // unassigned
/* 0x89 */ kKeyNone, // unassigned
/* 0x8a */ kKeyNone, // unassigned
/* 0x8b */ kKeyNone, // unassigned
/* 0x8c */ kKeyNone, // unassigned
/* 0x8d */ kKeyNone, // unassigned
/* 0x8e */ kKeyNone, // unassigned
/* 0x8f */ kKeyNone, // unassigned
/* 0x90 */ 0xff7f, // VK_NUMLOCK XK_Num_Lock
/* 0x91 */ 0xff14, // VK_SCROLL XK_Scroll_Lock
/* 0x92 */ kKeyNone, // unassigned
/* 0x93 */ kKeyNone, // unassigned
/* 0x94 */ kKeyNone, // unassigned
/* 0x95 */ kKeyNone, // unassigned
/* 0x96 */ kKeyNone, // unassigned
/* 0x97 */ kKeyNone, // unassigned
/* 0x98 */ kKeyNone, // unassigned
/* 0x99 */ kKeyNone, // unassigned
/* 0x9a */ kKeyNone, // unassigned
/* 0x9b */ kKeyNone, // unassigned
/* 0x9c */ kKeyNone, // unassigned
/* 0x9d */ kKeyNone, // unassigned
/* 0x9e */ kKeyNone, // unassigned
/* 0x9f */ kKeyNone, // unassigned
/* 0xa0 */ 0xffe1, // VK_LSHIFT XK_Shift_L
/* 0xa1 */ 0xffe2, // VK_RSHIFT XK_Shift_R
/* 0xa2 */ 0xffe3, // VK_LCONTROL XK_Control_L
/* 0xa3 */ 0xffe4, // VK_RCONTROL XK_Control_R
/* 0xa4 */ 0xffe9, // VK_LMENU XK_Alt_L
/* 0xa5 */ 0xffea, // VK_RMENU XK_Alt_R
/* 0xa6 */ kKeyNone, // unassigned
/* 0xa7 */ kKeyNone, // unassigned
/* 0xa8 */ kKeyNone, // unassigned
/* 0xa9 */ kKeyNone, // unassigned
/* 0xaa */ kKeyNone, // unassigned
/* 0xab */ kKeyNone, // unassigned
/* 0xac */ kKeyNone, // unassigned
/* 0xad */ kKeyNone, // unassigned
/* 0xae */ kKeyNone, // unassigned
/* 0xaf */ kKeyNone, // unassigned
/* 0xb0 */ kKeyNone, // unassigned
/* 0xb1 */ kKeyNone, // unassigned
/* 0xb2 */ kKeyNone, // unassigned
/* 0xb3 */ kKeyNone, // unassigned
/* 0xb4 */ kKeyNone, // unassigned
/* 0xb5 */ kKeyNone, // unassigned
/* 0xb6 */ kKeyNone, // unassigned
/* 0xb7 */ kKeyNone, // unassigned
/* 0xb8 */ kKeyNone, // unassigned
/* 0xb9 */ kKeyNone, // unassigned
/* 0xba */ kKeyNone, // OEM specific
/* 0xbb */ kKeyNone, // OEM specific
/* 0xbc */ kKeyNone, // OEM specific
/* 0xbd */ kKeyNone, // OEM specific
/* 0xbe */ kKeyNone, // OEM specific
/* 0xbf */ kKeyNone, // OEM specific
/* 0xc0 */ kKeyNone, // OEM specific
/* 0xc1 */ kKeyNone, // unassigned
/* 0xc2 */ kKeyNone, // unassigned
/* 0xc3 */ kKeyNone, // unassigned
/* 0xc4 */ kKeyNone, // unassigned
/* 0xc5 */ kKeyNone, // unassigned
/* 0xc6 */ kKeyNone, // unassigned
/* 0xc7 */ kKeyNone, // unassigned
/* 0xc8 */ kKeyNone, // unassigned
/* 0xc9 */ kKeyNone, // unassigned
/* 0xca */ kKeyNone, // unassigned
/* 0xcb */ kKeyNone, // unassigned
/* 0xcc */ kKeyNone, // unassigned
/* 0xcd */ kKeyNone, // unassigned
/* 0xce */ kKeyNone, // unassigned
/* 0xcf */ kKeyNone, // unassigned
/* 0xd0 */ kKeyNone, // unassigned
/* 0xd1 */ kKeyNone, // unassigned
/* 0xd2 */ kKeyNone, // unassigned
/* 0xd3 */ kKeyNone, // unassigned
/* 0xd4 */ kKeyNone, // unassigned
/* 0xd5 */ kKeyNone, // unassigned
/* 0xd6 */ kKeyNone, // unassigned
/* 0xd7 */ kKeyNone, // unassigned
/* 0xd8 */ kKeyNone, // unassigned
/* 0xd9 */ kKeyNone, // unassigned
/* 0xda */ kKeyNone, // unassigned
/* 0xdb */ kKeyNone, // OEM specific
/* 0xdc */ kKeyNone, // OEM specific
/* 0xdd */ kKeyNone, // OEM specific
/* 0xde */ kKeyNone, // OEM specific
/* 0xdf */ kKeyNone, // OEM specific
/* 0xe0 */ kKeyNone, // OEM specific
/* 0xe1 */ kKeyNone, // OEM specific
/* 0xe2 */ kKeyNone, // OEM specific
/* 0xe3 */ kKeyNone, // OEM specific
/* 0xe4 */ kKeyNone, // OEM specific
/* 0xe5 */ kKeyNone, // unassigned
/* 0xe6 */ kKeyNone, // OEM specific
/* 0xe7 */ kKeyNone, // unassigned
/* 0xe8 */ kKeyNone, // unassigned
/* 0xe9 */ kKeyNone, // OEM specific
/* 0xea */ kKeyNone, // OEM specific
/* 0xeb */ kKeyNone, // OEM specific
/* 0xec */ kKeyNone, // OEM specific
/* 0xed */ kKeyNone, // OEM specific
/* 0xee */ kKeyNone, // OEM specific
/* 0xef */ kKeyNone, // OEM specific
/* 0xf0 */ kKeyNone, // OEM specific
/* 0xf1 */ kKeyNone, // OEM specific
/* 0xf2 */ kKeyNone, // OEM specific
/* 0xf3 */ kKeyNone, // OEM specific
/* 0xf4 */ kKeyNone, // OEM specific
/* 0xf5 */ kKeyNone, // OEM specific
/* 0xf6 */ kKeyNone, // VK_ATTN
/* 0xf7 */ kKeyNone, // VK_CRSEL
/* 0xf8 */ kKeyNone, // VK_EXSEL
/* 0xf9 */ kKeyNone, // VK_EREOF
/* 0xfa */ kKeyNone, // VK_PLAY
/* 0xfb */ kKeyNone, // VK_ZOOM
/* 0xfc */ kKeyNone, // reserved
/* 0xfd */ kKeyNone, // VK_PA1
/* 0xfe */ kKeyNone, // VK_OEM_CLEAR
/* 0xff */ kKeyNone // reserved
};
KeyID
CMSWindowsPrimaryScreen::mapKey(
WPARAM vkCode,
LPARAM info,
KeyModifierMask* maskOut)
{
// note: known microsoft bugs
// Q72583 -- MapVirtualKey() maps keypad keys incorrectly
// 95,98: num pad vk code -> invalid scan code
// 95,98,NT4: num pad scan code -> bad vk code except
// SEPARATOR, MULTIPLY, SUBTRACT, ADD
static const KeyID XK_Multi_key = 0xff20;
assert(maskOut != NULL);
// map modifier key
KeyModifierMask mask = 0;
if (((m_keys[VK_LSHIFT] |
m_keys[VK_RSHIFT] |
m_keys[VK_SHIFT]) & 0x80) != 0) {
mask |= KeyModifierShift;
}
if (((m_keys[VK_LCONTROL] |
m_keys[VK_RCONTROL] |
m_keys[VK_CONTROL]) & 0x80) != 0) {
mask |= KeyModifierControl;
}
if (((m_keys[VK_LMENU] |
m_keys[VK_RMENU] |
m_keys[VK_MENU]) & 0x80) != 0) {
mask |= KeyModifierAlt;
}
if (((m_keys[VK_LWIN] |
m_keys[VK_RWIN]) & 0x80) != 0) {
mask |= KeyModifierMeta;
}
if ((m_keys[VK_CAPITAL] & 0x01) != 0) {
mask |= KeyModifierCapsLock;
}
if ((m_keys[VK_NUMLOCK] & 0x01) != 0) {
mask |= KeyModifierNumLock;
}
if ((m_keys[VK_SCROLL] & 0x01) != 0) {
mask |= KeyModifierScrollLock;
}
*maskOut = mask;
log((CLOG_DEBUG2 "key in vk=%d info=0x%08x mask=0x%04x", vkCode, info, mask));
// get the scan code
UINT scanCode = static_cast<UINT>((info & 0xff0000) >> 16);
// convert virtual key to one that distinguishes between left and
// right for keys that have left/right versions. known scan codes
// that don't have left/right versions are passed through unchanged.
// unknown scan codes return 0.
UINT vkCode2 = MapVirtualKey(scanCode, 3);
// work around bug Q72583 (bad num pad conversion in MapVirtualKey())
if (vkCode >= VK_NUMPAD0 && vkCode <= VK_DIVIDE) {
vkCode2 = vkCode;
}
// MapVirtualKey() appears to map VK_LWIN, VK_RWIN, VK_APPS to
// some other meaningless virtual key. work around that bug.
else if (vkCode >= VK_LWIN && vkCode <= VK_APPS) {
vkCode2 = vkCode;
}
// if MapVirtualKey failed then use original virtual key
else if (vkCode2 == 0) {
vkCode2 = vkCode;
}
// sadly, win32 will not distinguish between the left and right
// control and alt keys using the above function. however, we
// can check for those: if bit 24 of info is set then the key
// is a "extended" key, such as the right control and right alt
// keys.
if ((info & 0x1000000) != 0) {
switch (vkCode2) {
case VK_CONTROL:
case VK_LCONTROL:
vkCode2 = VK_RCONTROL;
break;
case VK_MENU:
case VK_LMENU:
vkCode2 = VK_RMENU;
break;
}
}
// use left/right distinguishing virtual key
vkCode = vkCode2;
log((CLOG_DEBUG1 "key vk=%d scan=%d", vkCode, scanCode));
// handle some keys via table lookup
KeyID id = g_virtualKey[vkCode];
if (id != kKeyNone) {
return id;
}
// check for dead keys
if (MapVirtualKey(vkCode, 2) >= 0x8000) {
return XK_Multi_key;
}
// ToAscii() maps ctrl+letter to the corresponding control code
// and ctrl+backspace to delete. if we've got a control code or
// delete then do ToAscii() again but without the control state.
// ToAscii() interprets the control modifier state which we don't
// want. so save the control state then clear it.
BYTE lControl = m_keys[VK_LCONTROL];
BYTE rControl = m_keys[VK_RCONTROL];
BYTE control = m_keys[VK_CONTROL];
m_keys[VK_LCONTROL] = 0;
m_keys[VK_RCONTROL] = 0;
m_keys[VK_CONTROL] = 0;
// convert to ascii
WORD ascii;
int result = ToAscii(vkCode, scanCode, m_keys, &ascii, 0);
// restore control state
m_keys[VK_LCONTROL] = lControl;
m_keys[VK_RCONTROL] = rControl;
m_keys[VK_CONTROL] = control;
// if result is less than zero then it was a dead key. that key
// is remembered by the keyboard which we don't want. remove it
// by calling ToAscii() again with arbitrary arguments.
if (result < 0) {
ToAscii(vkCode, scanCode, m_keys, &ascii, 0);
return XK_Multi_key;
}
// if result is 1 then the key was succesfully converted
else if (result == 1) {
return static_cast<KeyID>(ascii & 0x00ff);
}
// if result is 2 then a previous dead key could not be composed.
// put the old dead key back.
else if (result == 2) {
// get the scan code of the dead key and the shift state
// required to generate it.
vkCode = VkKeyScan(static_cast<TCHAR>(ascii & 0x00ff));
// set shift state required to generate key
BYTE keys[256];
memset(keys, 0, sizeof(keys));
// FIXME -- surely these masks should be different in each if expression
if (vkCode & 0x0100) {
keys[VK_SHIFT] = 0x80;
}
if (vkCode & 0x0100) {
keys[VK_CONTROL] = 0x80;
}
if (vkCode & 0x0100) {
keys[VK_MENU] = 0x80;
}
// strip shift state off of virtual key code
vkCode &= 0x00ff;
// get the scan code for the key
scanCode = MapVirtualKey(vkCode, 0);
// put it back
ToAscii(vkCode, scanCode, keys, &ascii, 0);
return XK_Multi_key;
}
// cannot convert key
return kKeyNone;
}
ButtonID
CMSWindowsPrimaryScreen::mapButton(WPARAM button) const
{
switch (button) {
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
return kButtonLeft;
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
return kButtonMiddle;
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
return kButtonRight;
default:
return kButtonNone;
}
}
void
CMSWindowsPrimaryScreen::updateKeys()
{
// not using GetKeyboardState() because that doesn't seem to give
// up-to-date results. i don't know why that is or why GetKeyState()
// should give different results.
// clear key state
memset(m_keys, 0, sizeof(m_keys));
// we only care about the modifier key states
m_keys[VK_LSHIFT] = static_cast<BYTE>(GetKeyState(VK_LSHIFT));
m_keys[VK_RSHIFT] = static_cast<BYTE>(GetKeyState(VK_RSHIFT));
m_keys[VK_SHIFT] = static_cast<BYTE>(GetKeyState(VK_SHIFT));
m_keys[VK_LCONTROL] = static_cast<BYTE>(GetKeyState(VK_LCONTROL));
m_keys[VK_RCONTROL] = static_cast<BYTE>(GetKeyState(VK_RCONTROL));
m_keys[VK_CONTROL] = static_cast<BYTE>(GetKeyState(VK_CONTROL));
m_keys[VK_LMENU] = static_cast<BYTE>(GetKeyState(VK_LMENU));
m_keys[VK_RMENU] = static_cast<BYTE>(GetKeyState(VK_RMENU));
m_keys[VK_MENU] = static_cast<BYTE>(GetKeyState(VK_MENU));
m_keys[VK_LWIN] = static_cast<BYTE>(GetKeyState(VK_LWIN));
m_keys[VK_RWIN] = static_cast<BYTE>(GetKeyState(VK_RWIN));
m_keys[VK_APPS] = static_cast<BYTE>(GetKeyState(VK_APPS));
m_keys[VK_CAPITAL] = static_cast<BYTE>(GetKeyState(VK_CAPITAL));
m_keys[VK_NUMLOCK] = static_cast<BYTE>(GetKeyState(VK_NUMLOCK));
m_keys[VK_SCROLL] = static_cast<BYTE>(GetKeyState(VK_SCROLL));
}
void
CMSWindowsPrimaryScreen::updateKey(UINT vkCode, bool press)
{
if (press) {
switch (vkCode) {
case VK_LSHIFT:
case VK_RSHIFT:
case VK_SHIFT:
m_keys[vkCode] |= 0x80;
m_keys[VK_SHIFT] |= 0x80;
break;
case VK_LCONTROL:
case VK_RCONTROL:
case VK_CONTROL:
m_keys[vkCode] |= 0x80;
m_keys[VK_CONTROL] |= 0x80;
break;
case VK_LMENU:
case VK_RMENU:
case VK_MENU:
m_keys[vkCode] |= 0x80;
m_keys[VK_MENU] |= 0x80;
break;
case VK_LWIN:
case VK_RWIN:
case VK_APPS:
m_keys[vkCode] |= 0x80;
break;
case VK_CAPITAL:
case VK_NUMLOCK:
case VK_SCROLL:
// toggle keys
m_keys[vkCode] |= 0x80;
break;
}
}
else {
switch (vkCode) {
case VK_LSHIFT:
case VK_RSHIFT:
case VK_SHIFT:
m_keys[vkCode] &= ~0x80;
if (((m_keys[VK_LSHIFT] | m_keys[VK_RSHIFT]) & 0x80) == 0) {
m_keys[VK_SHIFT] &= ~0x80;
}
break;
case VK_LCONTROL:
case VK_RCONTROL:
case VK_CONTROL:
m_keys[vkCode] &= ~0x80;
if (((m_keys[VK_LCONTROL] | m_keys[VK_RCONTROL]) & 0x80) == 0) {
m_keys[VK_CONTROL] &= ~0x80;
}
break;
case VK_LMENU:
case VK_RMENU:
case VK_MENU:
m_keys[vkCode] &= ~0x80;
if (((m_keys[VK_LMENU] | m_keys[VK_RMENU]) & 0x80) == 0) {
m_keys[VK_MENU] &= ~0x80;
}
break;
case VK_LWIN:
case VK_RWIN:
case VK_APPS:
m_keys[vkCode] &= ~0x80;
break;
case VK_CAPITAL:
case VK_NUMLOCK:
case VK_SCROLL:
// toggle keys
m_keys[vkCode] &= ~0x80;
m_keys[vkCode] ^= 0x01;
break;
}
}
}