/* * synergy -- mouse and keyboard sharing utility * Copyright (C) 2002 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 "CXWindowsScreen.h" #include "CXWindowsClipboard.h" #include "CXWindowsScreenSaver.h" #include "CXWindowsUtil.h" #include "CClipboard.h" #include "IScreenReceiver.h" #include "IPrimaryScreenReceiver.h" #include "XScreen.h" #include "CLock.h" #include "CThread.h" #include "CLog.h" #include "CStopwatch.h" #include "CStringUtil.h" #include "IJob.h" #include #if defined(X_DISPLAY_MISSING) # error X11 is required to build synergy #else # include # include # define XK_XKB_KEYS # include # if defined(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 #endif #if UNIX_LIKE # 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 # if HAVE_UNISTD_H # include # endif # endif #endif #include "CArch.h" // map "Internet" keys to KeyIDs static const KeySym g_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, 0, 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 }; // // CXWindowsScreen // CXWindowsScreen* CXWindowsScreen::s_screen = NULL; CXWindowsScreen::CXWindowsScreen(IScreenReceiver* receiver, IPrimaryScreenReceiver* primaryReceiver) : m_isPrimary(primaryReceiver != NULL), m_display(NULL), m_root(None), m_window(None), m_receiver(receiver), m_primaryReceiver(primaryReceiver), m_isOnScreen(m_isPrimary), m_x(0), m_y(0), m_w(0), m_h(0), m_xCursor(0), m_yCursor(0), m_xCenter(0), m_yCenter(0), m_keyState(NULL), m_keyMapper(), m_im(NULL), m_ic(NULL), m_lastKeycode(0), m_atomQuit(None), m_screensaver(NULL), m_screensaverNotify(false), m_atomScreensaver(None), m_oneShotTimer(NULL), m_xtestIsXineramaUnaware(true) { assert(s_screen == NULL); assert(m_receiver != NULL); s_screen = this; // no clipboards to start with for (ClipboardID id = 0; id < kClipboardEnd; ++id) { m_clipboard[id] = NULL; } } CXWindowsScreen::~CXWindowsScreen() { assert(s_screen != NULL); assert(m_display == NULL); delete m_oneShotTimer; s_screen = NULL; } void CXWindowsScreen::open(IKeyState* keyState) { assert(m_display == NULL); try { // set the X I/O error handler so we catch the display disconnecting XSetIOErrorHandler(&CXWindowsScreen::ioErrorHandler); // get the DISPLAY const char* display = getenv("DISPLAY"); if (display == NULL) { display = ":0.0"; } // open the display LOG((CLOG_DEBUG "XOpenDisplay(\"%s\")", display)); m_display = XOpenDisplay(display); if (m_display == NULL) { throw XScreenUnavailable(60.0); } // verify the availability of the XTest extension if (!m_isPrimary) { int majorOpcode, firstEvent, firstError; if (!XQueryExtension(m_display, XTestExtensionName, &majorOpcode, &firstEvent, &firstError)) { LOG((CLOG_ERR "XTEST extension not available")); throw XScreenOpenFailure(); } } // get root window m_root = DefaultRootWindow(m_display); // get shape of default screen m_x = 0; m_y = 0; m_w = WidthOfScreen(DefaultScreenOfDisplay(m_display)); m_h = HeightOfScreen(DefaultScreenOfDisplay(m_display)); LOG((CLOG_DEBUG "screen shape: %d,%d %dx%d", m_x, m_y, m_w, m_h)); // 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 // create the window m_window = createWindow(); if (m_window == None) { throw XScreenOpenFailure(); } LOG((CLOG_DEBUG "window is 0x%08x", m_window)); if (m_isPrimary) { // start watching for events on other windows selectEvents(m_root); // prepare to use input methods openIM(); } // initialize the clipboards for (ClipboardID id = 0; id < kClipboardEnd; ++id) { m_clipboard[id] = new CXWindowsClipboard(m_display, m_window, id); } // initialize the screen saver m_atomScreensaver = XInternAtom(m_display, "SYNERGY_SCREENSAVER", False); m_screensaver = new CXWindowsScreenSaver(this, m_display); } catch (...) { close(); throw; } // save the IKeyState m_keyState = keyState; // we'll send ourself an event of this type to exit the main loop m_atomQuit = XInternAtom(m_display, "SYNERGY_QUIT", False); if (!m_isPrimary) { // become impervious to server grabs XTestGrabControl(m_display, True); } } void CXWindowsScreen::close() { // done with m_keyState m_keyState = NULL; // done with screen saver delete m_screensaver; // destroy clipboards for (ClipboardID id = 0; id < kClipboardEnd; ++id) { delete m_clipboard[id]; m_clipboard[id] = NULL; } // done with input methods if (m_ic != NULL) { XDestroyIC(m_ic); } if (m_im != NULL) { XCloseIM(m_im); } // done with window if (m_window != None) { XDestroyWindow(m_display, m_window); } // close the display if (m_display != NULL) { XCloseDisplay(m_display); } // restore error handler XSetIOErrorHandler(NULL); // reset state m_atomQuit = None; m_screensaver = NULL; m_atomScreensaver = None; m_ic = NULL; m_im = NULL; m_window = None; m_root = None; m_display = NULL; } void CXWindowsScreen::enable() { CLock lock(&m_mutex); if (!m_isPrimary) { // get the keyboard control state XKeyboardState keyControl; XGetKeyboardControl(m_display, &keyControl); m_autoRepeat = (keyControl.global_auto_repeat == AutoRepeatModeOn); // 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() { CLock lock(&m_mutex); // 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::mainLoop() { // wait for an event in a cancellable way and don't lock the // display while we're waiting. XEvent event; m_mutex.lock(); for (;;) { #if UNIX_LIKE // compute timeout to next timer double dtimeout; { CLock timersLock(&m_timersMutex); dtimeout = (m_timers.empty() ? -1.0 : m_timers.top()); if (m_oneShotTimer != NULL && (dtimeout == -1.0 || *m_oneShotTimer < dtimeout)) { dtimeout = *m_oneShotTimer; } } // 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[1]; pfds[0].fd = ConnectionNumber(m_display); pfds[0].events = POLLIN; int timeout = static_cast(1000.0 * dtimeout); #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); #endif // wait for message from X server or for timeout. also check // if the thread has been cancelled. poll() should return -1 // with EINTR when the thread is cancelled. m_mutex.unlock(); CThread::testCancel(); #if HAVE_POLL poll(pfds, 1, timeout); #else select(ConnectionNumber(m_display) + 1, SELECT_TYPE_ARG234 &rfds, SELECT_TYPE_ARG234 NULL, SELECT_TYPE_ARG234 NULL, SELECT_TYPE_ARG5 timeoutPtr); #endif CThread::testCancel(); m_mutex.lock(); // process timers processTimers(); #else // !UNIX_LIKE // poll for pending events and process timers while (XPending(m_display) == 0) { // check timers if (processTimers()) { continue; } // wait m_mutex.unlock(); CThread::sleep(0.01); m_mutex.lock(); } #endif // !UNIX_LIKE // process events while (XPending(m_display) > 0) { // get the event XNextEvent(m_display, &event); if (isQuitEvent(&event)) { return; } // process the event onEvent(&event); } } } void CXWindowsScreen::exitMainLoop() { // send ourself a quit event. this will wake up the event loop // and cause it to exit. if (m_atomQuit != None) { XEvent event; event.xclient.type = ClientMessage; event.xclient.display = m_display; event.xclient.window = m_window; event.xclient.message_type = m_atomQuit; event.xclient.format = 32; event.xclient.data.l[0] = 0; event.xclient.data.l[1] = 0; event.xclient.data.l[2] = 0; event.xclient.data.l[3] = 0; event.xclient.data.l[4] = 0; CLock lock(&m_mutex); CXWindowsUtil::CErrorLock errorLock(m_display); XSendEvent(m_display, m_window, False, 0, &event); } } void CXWindowsScreen::enter() { CLock lock(&m_mutex); // 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); /* 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); // 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() { CLock lock(&m_mutex); 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 // FIXME -- take focus? 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; } // 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); } // now off screen m_isOnScreen = false; return true; } bool CXWindowsScreen::setClipboard(ClipboardID id, const IClipboard* clipboard) { CLock lock(&m_mutex); // 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) { CLock lock(&m_mutex); assert(m_screensaver != NULL); m_screensaverNotify = notify; if (m_screensaverNotify) { m_screensaver->setNotify(m_window); } else { m_screensaver->disable(); } } void CXWindowsScreen::closeScreensaver() { CLock lock(&m_mutex); if (m_screensaver != NULL) { if (m_screensaverNotify) { m_screensaver->setNotify(None); } else { m_screensaver->enable(); } } } void CXWindowsScreen::screensaver(bool activate) { CLock lock(&m_mutex); assert(m_screensaver != NULL); if (activate) { m_screensaver->activate(); } else { m_screensaver->deactivate(); } } void CXWindowsScreen::resetOptions() { m_xtestIsXineramaUnaware = true; } 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")); } } } void CXWindowsScreen::updateKeys() { CLock lock(&m_mutex); // update keyboard and mouse button mappings m_keyMapper.update(m_display, m_keyState); updateButtons(); } bool CXWindowsScreen::isPrimary() const { return m_isPrimary; } bool CXWindowsScreen::getClipboard(ClipboardID id, IClipboard* clipboard) const { assert(clipboard != NULL); // block others from using the display while we get the clipboard CLock lock(&m_mutex); // 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 { CLock lock(&m_mutex); assert(m_display != NULL); 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) { CLock lock(&m_mutex); // 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::addOneShotTimer(double timeout) { CLock lock(&m_timersMutex); // FIXME -- support multiple one-shot timers m_oneShotTimer = new CTimer(NULL, m_time.getTime(), timeout); return 0; } SInt32 CXWindowsScreen::getJumpZoneSize() const { return 1; } bool CXWindowsScreen::isAnyMouseButtonDown() const { CLock lock(&m_mutex); // 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; } const char* CXWindowsScreen::getKeyName(KeyButton keycode) const { CLock lock(&m_mutex); KeySym keysym = XKeycodeToKeysym(m_display, keycode, 0); char* name = XKeysymToString(keysym); if (name != NULL) { return name; } else { static char buffer[20]; return strcpy(buffer, CStringUtil::print("keycode %d", keycode).c_str()); } } void CXWindowsScreen::fakeKeyEvent(KeyButton keycode, bool press) const { CLock lock(&m_mutex); XTestFakeKeyEvent(m_display, keycode, press ? True : False, CurrentTime); XFlush(m_display); } bool CXWindowsScreen::fakeCtrlAltDel() const { // pass keys through unchanged return false; } void CXWindowsScreen::fakeMouseButton(ButtonID button, bool press) const { const unsigned int xButton = mapButtonToX(button); if (xButton != 0) { CLock lock(&m_mutex); XTestFakeButtonEvent(m_display, xButton, press ? True : False, CurrentTime); XFlush(m_display); } } void CXWindowsScreen::fakeMouseMove(SInt32 x, SInt32 y) const { CLock lock(&m_mutex); 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::fakeMouseWheel(SInt32 delta) const { // choose button depending on rotation direction const unsigned int xButton = mapButtonToX(static_cast( (delta >= 0) ? -1 : -2)); if (xButton == 0) { return; } // now use absolute value of delta if (delta < 0) { delta = -delta; } // send as many clicks as necessary CLock lock(&m_mutex); for (; delta >= 120; delta -= 120) { XTestFakeButtonEvent(m_display, xButton, True, CurrentTime); XTestFakeButtonEvent(m_display, xButton, False, CurrentTime); } XFlush(m_display); } KeyButton CXWindowsScreen::mapKey(IKeyState::Keystrokes& keys, const IKeyState& keyState, KeyID id, KeyModifierMask desiredMask, bool isAutoRepeat) const { return m_keyMapper.mapKey(keys, keyState, id, desiredMask, isAutoRepeat); } bool CXWindowsScreen::isQuitEvent(XEvent* event) const { return (m_atomQuit != None && event->type == ClientMessage && event->xclient.window == m_window && event->xclient.message_type == m_atomQuit); } Window CXWindowsScreen::createWindow() 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 return XCreateWindow(m_display, m_root, x, y, w, h, 0, 0, InputOnly, CopyFromParent, CWDontPropagate | CWEventMask | CWOverrideRedirect | CWCursor, &attr); } void CXWindowsScreen::openIM() { // open the input methods XIM im = XOpenIM(m_display, NULL, NULL, NULL); if (im == NULL) { 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_WARN "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::addTimer(IJob* job, double timeout) { CLock lock(&m_timersMutex); removeTimerNoLock(job); m_timers.push(CTimer(job, m_time.getTime(), timeout)); } void CXWindowsScreen::removeTimer(IJob* job) { CLock lock(&m_timersMutex); removeTimerNoLock(job); } void CXWindowsScreen::removeTimerNoLock(IJob* job) { // do it the hard way. first collect all jobs that are not // the removed job. CTimerPriorityQueue::container_type tmp; for (CTimerPriorityQueue::iterator index = m_timers.begin(); index != m_timers.end(); ++index) { if (index->getJob() != job) { tmp.push_back(*index); } } // now swap in the new list m_timers.swap(tmp); } void CXWindowsScreen::onEvent(XEvent* xevent) { assert(xevent != NULL); // 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)) { return; } } switch (xevent->type) { case CreateNotify: if (m_isPrimary) { // select events on new window selectEvents(xevent->xcreatewindow.window); } return; case MappingNotify: if (XPending(m_display) > 0) { XEvent tmpEvent; XPeekEvent(m_display, &tmpEvent); if (tmpEvent.type == MappingNotify) { // discard this MappingNotify since another follows. // we tend to get a bunch of these in a row. return; } } // keyboard mapping changed XRefreshKeyboardMapping(&xevent->xmapping); m_keyState->updateKeys(); 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); m_receiver->onGrabClipboard(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); } return; 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); return; } break; case ClientMessage: if (m_isPrimary && xevent->xclient.message_type == m_atomScreensaver && xevent->xclient.format == 32) { // screen saver activation/deactivation event m_primaryReceiver->onScreensaver(xevent->xclient.data.l[0] != 0); return; } 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); } 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: break; } // let screen saver have a go m_screensaver->onPreDispatch(xevent); } void CXWindowsScreen::onKeyPress(XKeyEvent& xkey) { LOG((CLOG_DEBUG1 "event: KeyPress code=%d, state=0x%04x", xkey.keycode, xkey.state)); const KeyModifierMask mask = m_keyMapper.mapModifier(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. KeyButton keycode = static_cast(xkey.keycode); if (keycode == 0) { keycode = static_cast(m_lastKeycode); } // handle key m_primaryReceiver->onKeyDown(key, mask, keycode); KeyModifierMask keyMask = m_keyState->getMaskForKey(keycode); if (m_keyState->isHalfDuplex(keyMask)) { m_primaryReceiver->onKeyUp(key, mask | keyMask, keycode); } } } Bool CXWindowsScreen::findKeyEvent(Display*, XEvent* xevent, XPointer arg) { CKeyEventInfo* 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::onKeyRelease(XKeyEvent& xkey) { const KeyModifierMask mask = m_keyMapper.mapModifier(xkey.state); KeyID key = mapKeyFromX(&xkey); if (key != kKeyNone) { // 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. CKeyEventInfo filter; filter.m_event = KeyPress; filter.m_window = xkey.window; filter.m_time = xkey.time; filter.m_keycode = xkey.keycode; // now check for event bool hasPress; { XEvent xevent2; hasPress = (XCheckIfEvent(m_display, &xevent2, &CXWindowsScreen::findKeyEvent, (XPointer)&filter) == True); } // 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; hasPress = false; } KeyButton keycode = static_cast(xkey.keycode); if (!hasPress) { // no press event follows so it's a plain release LOG((CLOG_DEBUG1 "event: KeyRelease code=%d, state=0x%04x", keycode, xkey.state)); KeyModifierMask keyMask = m_keyState->getMaskForKey(keycode); if (m_keyState->isHalfDuplex(keyMask)) { m_primaryReceiver->onKeyDown(key, mask, keycode); } m_primaryReceiver->onKeyUp(key, mask, 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_primaryReceiver->onKeyRepeat(key, mask, 1, keycode); } } } void CXWindowsScreen::onMousePress(const XButtonEvent& xbutton) { LOG((CLOG_DEBUG1 "event: ButtonPress button=%d", xbutton.button)); const ButtonID button = mapButtonFromX(&xbutton); if (button != kButtonNone) { m_primaryReceiver->onMouseDown(button); } } void CXWindowsScreen::onMouseRelease(const XButtonEvent& xbutton) { LOG((CLOG_DEBUG1 "event: ButtonRelease button=%d", xbutton.button)); const ButtonID button = mapButtonFromX(&xbutton); if (button != kButtonNone) { m_primaryReceiver->onMouseUp(button); } else if (xbutton.button == 4) { // wheel forward (away from user) m_primaryReceiver->onMouseWheel(120); } else if (xbutton.button == 5) { // wheel backward (toward user) m_primaryReceiver->onMouseWheel(-120); } } 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; do { XMaskEvent(m_display, PointerMotionMask, &xevent); } while (!xevent.xany.send_event); } else if (m_isOnScreen) { // motion on primary screen m_primaryReceiver->onMouseMovePrimary(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) { m_primaryReceiver->onMouseMoveSecondary(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; } bool CXWindowsScreen::processTimers() { bool oneShot = false; std::vector jobs; { CLock lock(&m_timersMutex); // get current time const double time = m_time.getTime(); // done if no timers have expired if ((m_oneShotTimer == NULL || *m_oneShotTimer > time) && (m_timers.empty() || m_timers.top() > time)) { return false; } // handle one shot timers if (m_oneShotTimer != NULL) { *m_oneShotTimer -= time; if (*m_oneShotTimer <= 0.0) { delete m_oneShotTimer; m_oneShotTimer = NULL; oneShot = true; } } // subtract current time from all timers. note that this won't // change the order of elements in the priority queue (except // for floating point round off which we'll ignore). for (CTimerPriorityQueue::iterator index = m_timers.begin(); index != m_timers.end(); ++index) { (*index) -= time; } // process all timers at or below zero, saving the jobs if (!m_timers.empty()) { while (m_timers.top() <= 0.0) { CTimer timer = m_timers.top(); jobs.push_back(timer.getJob()); timer.reset(); m_timers.pop(); m_timers.push(timer); } } // reset the clock m_time.reset(); } // now notify of the one shot timers if (oneShot) { m_mutex.unlock(); m_primaryReceiver->onOneShotTimerExpired(0); m_mutex.lock(); } // now run the jobs. note that if one of these jobs removes // a timer later in the jobs list and deletes that job pointer // then this will crash when it tries to run that job. for (std::vector::iterator index = jobs.begin(); index != jobs.end(); ++index) { (*index)->run(); } } 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; } } } int CXWindowsScreen::ioErrorHandler(Display*) { // the display has disconnected, probably because X is shutting // down. X forces us to exit at this point. that's arguably // a flaw in X but, realistically, it's difficult to gracefully // handle not having a Display* anymore. we'll simply log the // error, notify the subclass (which must not use the display // so we set it to NULL), and exit. LOG((CLOG_WARN "X display has unexpectedly disconnected")); s_screen->m_display = NULL; s_screen->m_receiver->onError(); LOG((CLOG_CRIT "quiting due to X display disconnection")); exit(17); } 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 switch (keysym & 0xffffff00) { case 0x0000: // Latin-1 return static_cast(keysym); case 0xfe00: // ISO 9995 Function and Modifier Keys if (keysym == XK_ISO_Left_Tab) { return kKeyLeftTab; } return kKeyNone; case 0xff00: // MISCELLANY return static_cast(keysym - 0xff00 + 0xef00); case 0x1008ff00: // "Internet" keys return g_map1008FF[keysym & 0xff]; default: { // lookup character in table UInt32 key = CXWindowsUtil::mapKeySymToUCS4(keysym); if (key != 0x0000ffff) { return static_cast(key); } // unknown character return kKeyNone; } } } 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(m_buttons[id - 1]); } 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() { // 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 result = XGrabPointer(m_display, m_window, True, 0, 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; } // // CXWindowsScreen::CTimer // CXWindowsScreen::CTimer::CTimer(IJob* job, double startTime, double resetTime) : m_job(job), m_timeout(resetTime), m_time(resetTime), m_startTime(startTime) { assert(m_timeout > 0.0); } CXWindowsScreen::CTimer::~CTimer() { // do nothing } void CXWindowsScreen::CTimer::run() { if (m_job != NULL) { m_job->run(); } } void CXWindowsScreen::CTimer::reset() { m_time = m_timeout; m_startTime = 0.0; } CXWindowsScreen::CTimer::CTimer& CXWindowsScreen::CTimer::operator-=(double dt) { m_time -= dt - m_startTime; m_startTime = 0.0; return *this; } CXWindowsScreen::CTimer::operator double() const { return m_time; } bool CXWindowsScreen::CTimer::operator<(const CTimer& t) const { return m_time < t.m_time; }