/* * 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 "CXWindowsEventQueueBuffer.h" #include "CXWindowsKeyState.h" #include "CXWindowsScreenSaver.h" #include "CXWindowsUtil.h" #include "CClipboard.h" #include "XScreen.h" #include "CLog.h" #include "CStopwatch.h" #include "CStringUtil.h" #include "IEventQueue.h" #include "TMethodEventJob.h" #include #if X_DISPLAY_MISSING # error X11 is required to build synergy #else # include # include # define XK_XKB_KEYS # include # if HAVE_X11_EXTENSIONS_XTEST_H # include # else # error The XTest extension is required to build synergy # endif # if HAVE_X11_EXTENSIONS_XINERAMA_H // Xinerama.h may lack extern "C" for inclusion by C++ extern "C" { # include } # endif #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, kKeyEject, 0, 0, 0, /* 0x30 */ kKeyWWWFavorites, 0, kKeyAppMedia, 0, 0, 0, 0, 0, /* 0x38 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0x40 */ kKeyAppUser1, kKeyAppUser2, 0, 0, 0, 0, 0, 0, /* 0x48 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0x50 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0x58 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0x60 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0x68 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0x70 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0x78 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0x80 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0x88 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0x90 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0x98 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0xa0 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0xa8 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0xb0 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0xb8 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0xc0 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0xc8 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0xd0 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0xd8 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0xe0 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0xe8 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0xf0 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 0xf8 */ 0, 0, 0, 0, 0, 0, 0, 0 }; // // CXWindowsScreen // // NOTE -- the X display is shared among several objects but is owned // by the CXWindowsScreen. Xlib is not reentrant so we must ensure // that no two objects can simultaneously call Xlib with the display. // this is easy since we only make X11 calls from the main thread. // we must also ensure that these objects do not use the display in // their destructors or, if they do, we can tell them not to. This // is to handle unexpected disconnection of the X display, when any // call on the display is invalid. In that situation we discard the // display and the X11 event queue buffer, ignore any calls that try // to use the display, and wait to be destroyed. CXWindowsScreen* CXWindowsScreen::s_screen = NULL; CXWindowsScreen::CXWindowsScreen(const char* displayName, bool isPrimary) : m_isPrimary(isPrimary), m_display(NULL), m_root(None), m_window(None), m_isOnScreen(m_isPrimary), m_x(0), m_y(0), m_w(0), m_h(0), m_xCenter(0), m_yCenter(0), m_xCursor(0), m_yCursor(0), m_keyState(NULL), m_lastFocus(None), m_lastFocusRevert(RevertToNone), m_im(NULL), m_ic(NULL), m_lastKeycode(0), m_sequenceNumber(0), m_screensaver(NULL), m_screensaverNotify(false), m_xtestIsXineramaUnaware(true) { assert(s_screen == NULL); s_screen = this; // set the X I/O error handler so we catch the display disconnecting XSetIOErrorHandler(&CXWindowsScreen::ioErrorHandler); try { m_display = openDisplay(displayName); m_root = DefaultRootWindow(m_display); saveShape(); m_window = openWindow(); m_screensaver = new CXWindowsScreenSaver(m_display, m_window, getEventTarget()); m_keyState = new CXWindowsKeyState(m_display); LOG((CLOG_DEBUG "screen shape: %d,%d %dx%d %s", m_x, m_y, m_w, m_h, m_xinerama ? "(xinerama)" : "")); LOG((CLOG_DEBUG "window is 0x%08x", m_window)); } catch (...) { if (m_display != NULL) { XCloseDisplay(m_display); } throw; } // primary/secondary screen only initialization if (m_isPrimary) { // start watching for events on other windows selectEvents(m_root); // prepare to use input methods openIM(); } else { // become impervious to server grabs XTestGrabControl(m_display, True); } // initialize the clipboards for (ClipboardID id = 0; id < kClipboardEnd; ++id) { m_clipboard[id] = new CXWindowsClipboard(m_display, m_window, id); } // install event handlers EVENTQUEUE->adoptHandler(CEvent::kSystem, IEventQueue::getSystemTarget(), new TMethodEventJob(this, &CXWindowsScreen::handleSystemEvent)); // install the platform event queue EVENTQUEUE->adoptBuffer(new CXWindowsEventQueueBuffer(m_display, m_window)); } CXWindowsScreen::~CXWindowsScreen() { assert(s_screen != NULL); assert(m_display != NULL); EVENTQUEUE->adoptBuffer(NULL); EVENTQUEUE->removeHandler(CEvent::kSystem, IEventQueue::getSystemTarget()); for (ClipboardID id = 0; id < kClipboardEnd; ++id) { delete m_clipboard[id]; } delete m_keyState; delete m_screensaver; m_keyState = NULL; m_screensaver = NULL; if (m_display != NULL) { // FIXME -- is it safe to clean up the IC and IM without a display? if (m_ic != NULL) { XDestroyIC(m_ic); } if (m_im != NULL) { XCloseIM(m_im); } XDestroyWindow(m_display, m_window); XCloseDisplay(m_display); } XSetIOErrorHandler(NULL); s_screen = NULL; } void CXWindowsScreen::enable() { if (!m_isPrimary) { // get the keyboard control state XKeyboardState keyControl; XGetKeyboardControl(m_display, &keyControl); m_autoRepeat = (keyControl.global_auto_repeat == AutoRepeatModeOn); // 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); } updateKeys(); } void CXWindowsScreen::disable() { // release input context focus if (m_ic != NULL) { XUnsetICFocus(m_ic); } // unmap the hider/grab window. this also ungrabs the mouse and // keyboard if they're grabbed. XUnmapWindow(m_display, m_window); // restore auto-repeat state if (!m_isPrimary && m_autoRepeat) { XAutoRepeatOn(m_display); } } void CXWindowsScreen::enter() { // 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); // set the input focus to what it had been when we took it if (m_lastFocus != None) { // the window may not exist anymore so ignore errors CXWindowsUtil::CErrorLock lock(m_display); XSetInputFocus(m_display, m_lastFocus, m_lastFocusRevert, CurrentTime); } /* 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() { if (!m_isPrimary) { // restore the previous keyboard auto-repeat state. if the user // changed the auto-repeat configuration while on the client then // that state is lost. that's because we can't get notified by // the X server when the auto-repeat configuration is changed so // we can't track the desired configuration. if (m_autoRepeat) { XAutoRepeatOn(m_display); } // move hider window under the cursor center XMoveWindow(m_display, m_window, m_xCenter, m_yCenter); } // raise and show the window XMapRaised(m_display, m_window); // grab the mouse and keyboard, if primary and possible if (m_isPrimary && !grabMouseAndKeyboard()) { XUnmapWindow(m_display, m_window); return false; } // save current focus XGetInputFocus(m_display, &m_lastFocus, &m_lastFocusRevert); // take focus XSetInputFocus(m_display, m_window, RevertToPointerRoot, CurrentTime); // now warp the mouse. we warp after showing the window so we're // guaranteed to get the mouse leave event and to prevent the // keyboard focus from changing under point-to-focus policies. if (m_isPrimary) { warpCursor(m_xCenter, m_yCenter); } else { fakeMouseMove(m_xCenter, m_yCenter); } // set input context focus to our window if (m_ic != NULL) { XmbResetIC(m_ic); XSetICFocus(m_ic); } // now off screen m_isOnScreen = false; return true; } bool CXWindowsScreen::setClipboard(ClipboardID id, const IClipboard* clipboard) { // fail if we don't have the requested clipboard if (m_clipboard[id] == NULL) { return false; } // get the actual time. ICCCM does not allow CurrentTime. Time timestamp = CXWindowsUtil::getCurrentTime( m_display, m_clipboard[id]->getWindow()); if (clipboard != NULL) { // save clipboard data return CClipboard::copy(m_clipboard[id], clipboard, timestamp); } else { // assert clipboard ownership if (!m_clipboard[id]->open(timestamp)) { return false; } m_clipboard[id]->empty(); m_clipboard[id]->close(); return true; } } void CXWindowsScreen::checkClipboards() { // do nothing, we're always up to date } void CXWindowsScreen::openScreensaver(bool notify) { m_screensaverNotify = notify; if (!m_screensaverNotify) { m_screensaver->disable(); } } void CXWindowsScreen::closeScreensaver() { if (!m_screensaverNotify) { m_screensaver->enable(); } } void CXWindowsScreen::screensaver(bool activate) { if (activate) { m_screensaver->activate(); } else { m_screensaver->deactivate(); } } void CXWindowsScreen::resetOptions() { m_xtestIsXineramaUnaware = true; } 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::setSequenceNumber(UInt32 seqNum) { m_sequenceNumber = seqNum; } bool CXWindowsScreen::isPrimary() const { return m_isPrimary; } void* CXWindowsScreen::getEventTarget() const { return const_cast(this); } bool CXWindowsScreen::getClipboard(ClipboardID id, IClipboard* clipboard) const { assert(clipboard != NULL); // fail if we don't have the requested clipboard if (m_clipboard[id] == NULL) { return false; } // get the actual time. ICCCM does not allow CurrentTime. Time timestamp = CXWindowsUtil::getCurrentTime( m_display, m_clipboard[id]->getWindow()); // copy the clipboard return CClipboard::copy(clipboard, m_clipboard[id], timestamp); } void CXWindowsScreen::getShape(SInt32& x, SInt32& y, SInt32& w, SInt32& h) const { x = m_x; y = m_y; w = m_w; h = m_h; } void CXWindowsScreen::getCursorPos(SInt32& x, SInt32& y) const { Window root, window; int mx, my, xWindow, yWindow; unsigned int mask; if (XQueryPointer(m_display, m_root, &root, &window, &mx, &my, &xWindow, &yWindow, &mask)) { x = mx; y = my; } else { x = m_xCenter; y = m_yCenter; } } void CXWindowsScreen::reconfigure(UInt32) { // do nothing } void CXWindowsScreen::warpCursor(SInt32 x, SInt32 y) { // warp mouse warpCursorNoFlush(x, y); // remove all input events before and including warp XEvent event; while (XCheckMaskEvent(m_display, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | KeyPressMask | KeyReleaseMask | KeymapStateMask, &event)) { // do nothing } // save position as last position m_xCursor = x; m_yCursor = y; } SInt32 CXWindowsScreen::getJumpZoneSize() const { return 1; } bool CXWindowsScreen::isAnyMouseButtonDown() const { // query the pointer to get the button state Window root, window; int xRoot, yRoot, xWindow, yWindow; unsigned int state; if (XQueryPointer(m_display, m_root, &root, &window, &xRoot, &yRoot, &xWindow, &yWindow, &state)) { return ((state & (Button1Mask | Button2Mask | Button3Mask | Button4Mask | Button5Mask)) != 0); } return false; } void CXWindowsScreen::getCursorCenter(SInt32& x, SInt32& y) const { x = m_xCenter; y = m_yCenter; } void CXWindowsScreen::fakeMouseButton(ButtonID button, bool press) const { const unsigned int xButton = mapButtonToX(button); if (xButton != 0) { XTestFakeButtonEvent(m_display, xButton, press ? True : False, CurrentTime); XFlush(m_display); } } void CXWindowsScreen::fakeMouseMove(SInt32 x, SInt32 y) const { if (m_xinerama && m_xtestIsXineramaUnaware) { XWarpPointer(m_display, None, m_root, 0, 0, 0, 0, x, y); } else { XTestFakeMotionEvent(m_display, DefaultScreen(m_display), x, y, CurrentTime); } XFlush(m_display); } void CXWindowsScreen::fakeMouseRelativeMove(SInt32 dx, SInt32 dy) const { // FIXME -- ignore xinerama for now if (false && m_xinerama && m_xtestIsXineramaUnaware) { // XWarpPointer(m_display, None, m_root, 0, 0, 0, 0, x, y); } else { XTestFakeRelativeMotionEvent(m_display, dx, dy, CurrentTime); } XFlush(m_display); } void CXWindowsScreen::fakeMouseWheel(SInt32 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 for (; delta >= 120; delta -= 120) { XTestFakeButtonEvent(m_display, xButton, True, CurrentTime); XTestFakeButtonEvent(m_display, xButton, False, CurrentTime); } XFlush(m_display); } Display* CXWindowsScreen::openDisplay(const char* displayName) const { // get the DISPLAY if (displayName == NULL) { displayName = getenv("DISPLAY"); if (displayName == NULL) { displayName = ":0.0"; } } // open the display LOG((CLOG_DEBUG "XOpenDisplay(\"%s\")", displayName)); Display* display = XOpenDisplay(displayName); if (display == NULL) { throw XScreenUnavailable(60.0); } // verify the availability of the XTest extension if (!m_isPrimary) { int majorOpcode, firstEvent, firstError; if (!XQueryExtension(display, XTestExtensionName, &majorOpcode, &firstEvent, &firstError)) { LOG((CLOG_ERR "XTEST extension not available")); XCloseDisplay(display); throw XScreenOpenFailure(); } } return display; } void CXWindowsScreen::saveShape() { // get shape of default screen m_x = 0; m_y = 0; m_w = WidthOfScreen(DefaultScreenOfDisplay(m_display)); m_h = HeightOfScreen(DefaultScreenOfDisplay(m_display)); // get center of default screen m_xCenter = m_x + (m_w >> 1); m_yCenter = m_y + (m_h >> 1); // check if xinerama is enabled and there is more than one screen. // get center of first Xinerama screen. Xinerama appears to have // a bug when XWarpPointer() is used in combination with // XGrabPointer(). in that case, the warp is successful but the // next pointer motion warps the pointer again, apparently to // constrain it to some unknown region, possibly the region from // 0,0 to Wm,Hm where Wm (Hm) is the minimum width (height) over // all physical screens. this warp only seems to happen if the // pointer wasn't in that region before the XWarpPointer(). the // second (unexpected) warp causes synergy to think the pointer // has been moved when it hasn't. to work around the problem, // we warp the pointer to the center of the first physical // screen instead of the logical screen. m_xinerama = false; #if HAVE_X11_EXTENSIONS_XINERAMA_H int eventBase, errorBase; if (XineramaQueryExtension(m_display, &eventBase, &errorBase) && XineramaIsActive(m_display)) { int numScreens; XineramaScreenInfo* screens; screens = XineramaQueryScreens(m_display, &numScreens); if (screens != NULL) { if (numScreens > 1) { m_xinerama = true; m_xCenter = screens[0].x_org + (screens[0].width >> 1); m_yCenter = screens[0].y_org + (screens[0].height >> 1); } XFree(screens); } } #endif } Window CXWindowsScreen::openWindow() const { // default window attributes. we don't want the window manager // messing with our window and we don't want the cursor to be // visible inside the window. XSetWindowAttributes attr; attr.do_not_propagate_mask = 0; attr.override_redirect = True; attr.cursor = createBlankCursor(); // adjust attributes and get size and shape SInt32 x, y, w, h; if (m_isPrimary) { // grab window attributes. this window is used to capture user // input when the user is focused on another client. it covers // the whole screen. attr.event_mask = PointerMotionMask | ButtonPressMask | ButtonReleaseMask | KeyPressMask | KeyReleaseMask | KeymapStateMask | PropertyChangeMask; x = m_x; y = m_y; w = m_w; h = m_h; } else { // cursor hider window attributes. this window is used to hide the // cursor when it's not on the screen. the window is hidden as soon // as the cursor enters the screen or the display's real mouse is // moved. we'll reposition the window as necessary so its // position here doesn't matter. it only needs to be 1x1 because // it only needs to contain the cursor's hotspot. attr.event_mask = LeaveWindowMask; x = 0; y = 0; w = 1; h = 1; } // create and return the window Window window = XCreateWindow(m_display, m_root, x, y, w, h, 0, 0, InputOnly, CopyFromParent, CWDontPropagate | CWEventMask | CWOverrideRedirect | CWCursor, &attr); if (window == None) { throw XScreenOpenFailure(); } return window; } void CXWindowsScreen::openIM() { // open the input methods XIM im = XOpenIM(m_display, NULL, NULL, NULL); if (im == NULL) { LOG((CLOG_INFO "no support for IM")); return; } // find the appropriate style. synergy supports XIMPreeditNothing // only at the moment. XIMStyles* styles; if (XGetIMValues(im, XNQueryInputStyle, &styles, NULL) != NULL || styles == NULL) { LOG((CLOG_WARN "cannot get IM styles")); XCloseIM(im); return; } XIMStyle style = 0; for (unsigned short i = 0; i < styles->count_styles; ++i) { style = styles->supported_styles[i]; if ((style & XIMPreeditNothing) != 0) { if ((style & (XIMStatusNothing | XIMStatusNone)) != 0) { break; } } } XFree(styles); if (style == 0) { LOG((CLOG_INFO "no supported IM styles")); XCloseIM(im); return; } // create an input context for the style and tell it about our window XIC ic = XCreateIC(im, XNInputStyle, style, XNClientWindow, m_window, NULL); if (ic == NULL) { LOG((CLOG_WARN "cannot create IC")); XCloseIM(im); return; } // find out the events we must select for and do so unsigned long mask; if (XGetICValues(ic, XNFilterEvents, &mask, NULL) != NULL) { LOG((CLOG_WARN "cannot get IC filter events")); XDestroyIC(ic); XCloseIM(im); return; } // we have IM m_im = im; m_ic = ic; m_lastKeycode = 0; // select events on our window that IM requires XWindowAttributes attr; XGetWindowAttributes(m_display, m_window, &attr); XSelectInput(m_display, m_window, attr.your_event_mask | mask); } void CXWindowsScreen::sendEvent(CEvent::Type type, void* data) { EVENTQUEUE->addEvent(CEvent(type, getEventTarget(), data)); } void CXWindowsScreen::sendClipboardEvent(CEvent::Type type, ClipboardID id) { CClipboardInfo* info = (CClipboardInfo*)malloc(sizeof(CClipboardInfo)); info->m_id = id; info->m_sequenceNumber = m_sequenceNumber; sendEvent(type, info); } IKeyState* CXWindowsScreen::getKeyState() const { return m_keyState; } Bool CXWindowsScreen::findKeyEvent(Display*, XEvent* xevent, XPointer arg) { CKeyEventFilter* filter = reinterpret_cast(arg); return (xevent->type == filter->m_event && xevent->xkey.window == filter->m_window && xevent->xkey.time == filter->m_time && xevent->xkey.keycode == filter->m_keycode) ? True : False; } void CXWindowsScreen::handleSystemEvent(const CEvent& event, void*) { XEvent* xevent = reinterpret_cast(event.getData()); assert(xevent != NULL); // update key state bool isRepeat = false; if (m_isPrimary) { if (xevent->type == KeyRelease) { // check if this is a key repeat by getting the next // KeyPress event that has the same key and time as // this release event, if any. first prepare the // filter info. CKeyEventFilter filter; filter.m_event = KeyPress; filter.m_window = xevent->xkey.window; filter.m_time = xevent->xkey.time; filter.m_keycode = xevent->xkey.keycode; XEvent xevent2; isRepeat = (XCheckIfEvent(m_display, &xevent2, &CXWindowsScreen::findKeyEvent, (XPointer)&filter) == True); } if (xevent->type == KeyPress || xevent->type == KeyRelease) { if (!isRepeat) { m_keyState->setKeyDown(xevent->xkey.keycode, xevent->type == KeyPress); } } } // 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; } } // let screen saver have a go if (m_screensaver->handleXEvent(xevent)) { // screen saver handled it return; } // handle the event ourself switch (xevent->type) { case CreateNotify: if (m_isPrimary) { // select events on new window selectEvents(xevent->xcreatewindow.window); } break; 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); sendClipboardEvent(getClipboardGrabbedEvent(), id); return; } } break; case SelectionNotify: // notification of selection transferred. we shouldn't // get this here because we handle them in the selection // retrieval methods. we'll just delete the property // with the data (satisfying the usual ICCCM protocol). if (xevent->xselection.property != None) { XDeleteProperty(m_display, xevent->xselection.requestor, xevent->xselection.property); } break; case SelectionRequest: { // somebody is asking for clipboard data ClipboardID id = getClipboardID( xevent->xselectionrequest.selection); if (id != kClipboardEnd) { m_clipboard[id]->addRequest( xevent->xselectionrequest.owner, xevent->xselectionrequest.requestor, xevent->xselectionrequest.target, xevent->xselectionrequest.time, xevent->xselectionrequest.property); return; } } break; case PropertyNotify: // property delete may be part of a selection conversion if (xevent->xproperty.state == PropertyDelete) { processClipboardRequest(xevent->xproperty.window, xevent->xproperty.time, xevent->xproperty.atom); } break; case DestroyNotify: // looks like one of the windows that requested a clipboard // transfer has gone bye-bye. destroyClipboardRequest(xevent->xdestroywindow.window); break; case KeyPress: if (m_isPrimary) { onKeyPress(xevent->xkey); } return; case KeyRelease: if (m_isPrimary) { onKeyRelease(xevent->xkey, isRepeat); } return; case ButtonPress: if (m_isPrimary) { onMousePress(xevent->xbutton); } return; case ButtonRelease: if (m_isPrimary) { onMouseRelease(xevent->xbutton); } return; case MotionNotify: if (m_isPrimary) { onMouseMove(xevent->xmotion); } return; default: break; } } void CXWindowsScreen::onKeyPress(XKeyEvent& xkey) { LOG((CLOG_DEBUG1 "event: KeyPress code=%d, state=0x%04x", xkey.keycode, xkey.state)); const KeyModifierMask mask = m_keyState->mapModifiersFromX(xkey.state); KeyID key = mapKeyFromX(&xkey); if (key != kKeyNone) { // check for ctrl+alt+del emulation if ((key == kKeyPause || key == kKeyBreak) && (mask & (KeyModifierControl | KeyModifierAlt)) == (KeyModifierControl | KeyModifierAlt)) { // pretend it's ctrl+alt+del LOG((CLOG_DEBUG "emulate ctrl+alt+del")); key = kKeyDelete; } // get which button. see call to XFilterEvent() in onEvent() // for more info. KeyButton keycode = static_cast(xkey.keycode); if (keycode == 0) { keycode = static_cast(m_lastKeycode); } // handle key m_keyState->sendKeyEvent(getEventTarget(), true, false, key, mask, 1, keycode); } } void CXWindowsScreen::onKeyRelease(XKeyEvent& xkey, bool isRepeat) { const KeyModifierMask mask = m_keyState->mapModifiersFromX(xkey.state); KeyID key = mapKeyFromX(&xkey); if (key != kKeyNone) { // check for ctrl+alt+del emulation if ((key == kKeyPause || key == kKeyBreak) && (mask & (KeyModifierControl | KeyModifierAlt)) == (KeyModifierControl | KeyModifierAlt)) { // pretend it's ctrl+alt+del and ignore autorepeat LOG((CLOG_DEBUG "emulate ctrl+alt+del")); key = kKeyDelete; isRepeat = false; } KeyButton keycode = static_cast(xkey.keycode); if (!isRepeat) { // no press event follows so it's a plain release LOG((CLOG_DEBUG1 "event: KeyRelease code=%d, state=0x%04x", keycode, xkey.state)); m_keyState->sendKeyEvent(getEventTarget(), false, false, key, mask, 1, keycode); } else { // found a press event following so it's a repeat. // we could attempt to count the already queued // repeats but we'll just send a repeat of 1. // note that we discard the press event. LOG((CLOG_DEBUG1 "event: repeat code=%d, state=0x%04x", keycode, xkey.state)); m_keyState->sendKeyEvent(getEventTarget(), false, true, key, mask, 1, keycode); } } } void CXWindowsScreen::onMousePress(const XButtonEvent& xbutton) { LOG((CLOG_DEBUG1 "event: ButtonPress button=%d", xbutton.button)); const ButtonID button = mapButtonFromX(&xbutton); if (button != kButtonNone) { sendEvent(getButtonDownEvent(), CButtonInfo::alloc(button)); } } void CXWindowsScreen::onMouseRelease(const XButtonEvent& xbutton) { LOG((CLOG_DEBUG1 "event: ButtonRelease button=%d", xbutton.button)); const ButtonID button = mapButtonFromX(&xbutton); if (button != kButtonNone) { sendEvent(getButtonUpEvent(), CButtonInfo::alloc(button)); } else if (xbutton.button == 4) { // wheel forward (away from user) sendEvent(getWheelEvent(), CWheelInfo::alloc(120)); } else if (xbutton.button == 5) { // wheel backward (toward user) sendEvent(getWheelEvent(), CWheelInfo::alloc(-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 sendEvent(getMotionOnPrimaryEvent(), CMotionInfo::alloc(m_xCursor, m_yCursor)); } else { // motion on secondary screen. warp mouse back to // center. // // my lombard (powerbook g3) running linux and // using the adbmouse driver has two problems: // first, the driver only sends motions of +/-2 // pixels and, second, it seems to discard some // physical input after a warp. the former isn't a // big deal (we're just limited to every other // pixel) but the latter is a PITA. to work around // it we only warp when the mouse has moved more // than s_size pixels from the center. static const SInt32 s_size = 32; if (xmotion.x_root - m_xCenter < -s_size || xmotion.x_root - m_xCenter > s_size || xmotion.y_root - m_yCenter < -s_size || xmotion.y_root - m_yCenter > s_size) { warpCursorNoFlush(m_xCenter, m_yCenter); } // send event if mouse moved. do this after warping // back to center in case the motion takes us onto // the primary screen. if we sent the event first // in that case then the warp would happen after // warping to the primary screen's enter position, // effectively overriding it. if (x != 0 || y != 0) { sendEvent(getMotionOnSecondaryEvent(), CMotionInfo::alloc(x, y)); } } } Cursor CXWindowsScreen::createBlankCursor() const { // this seems just a bit more complicated than really necessary // get the closet cursor size to 1x1 unsigned int w, h; XQueryBestCursor(m_display, m_root, 1, 1, &w, &h); // make bitmap data for cursor of closet size. since the cursor // is blank we can use the same bitmap for shape and mask: all // zeros. const int size = ((w + 7) >> 3) * h; char* data = new char[size]; memset(data, 0, size); // make bitmap Pixmap bitmap = XCreateBitmapFromData(m_display, m_root, data, w, h); // need an arbitrary color for the cursor XColor color; color.pixel = 0; color.red = color.green = color.blue = 0; color.flags = DoRed | DoGreen | DoBlue; // make cursor from bitmap Cursor cursor = XCreatePixmapCursor(m_display, bitmap, bitmap, &color, &color, 0, 0); // don't need bitmap or the data anymore delete[] data; XFreePixmap(m_display, bitmap); return cursor; } ClipboardID CXWindowsScreen::getClipboardID(Atom selection) const { for (ClipboardID id = 0; id < kClipboardEnd; ++id) { if (m_clipboard[id] != NULL && m_clipboard[id]->getSelection() == selection) { return id; } } return kClipboardEnd; } void CXWindowsScreen::processClipboardRequest(Window requestor, Time time, Atom property) { // check every clipboard until one returns success for (ClipboardID id = 0; id < kClipboardEnd; ++id) { if (m_clipboard[id] != NULL && m_clipboard[id]->processRequest(requestor, time, property)) { break; } } } void CXWindowsScreen::destroyClipboardRequest(Window requestor) { // check every clipboard until one returns success for (ClipboardID id = 0; id < kClipboardEnd; ++id) { if (m_clipboard[id] != NULL && m_clipboard[id]->destroyRequest(requestor)) { break; } } } void CXWindowsScreen::onError() { // prevent further access to the X display EVENTQUEUE->adoptBuffer(NULL); m_screensaver->destroy(); m_screensaver = NULL; m_display = NULL; // notify of failure sendEvent(getErrorEvent(), NULL); // FIXME -- should ensure that we ignore operations that involve // m_display from now on. however, Xlib will simply exit the // application in response to the X I/O error so there's no // point in trying to really handle the error. if we did want // to handle the error, it'd probably be easiest to delegate to // one of two objects. one object would take the implementation // from this class. the other object would be stub methods that // don't use X11. on error, we'd switch to the latter. } int CXWindowsScreen::ioErrorHandler(Display*) { // the display has disconnected, probably because X is shutting // down. X forces us to exit at this point which is annoying. // we'll pretend as if we won't exit so we try to make sure we // don't access the display anymore. LOG((CLOG_CRIT "X display has unexpectedly disconnected")); s_screen->onError(); return 0; } void CXWindowsScreen::selectEvents(Window w) const { // ignore errors while we adjust event masks. windows could be // destroyed at any time after the XQueryTree() in doSelectEvents() // so we must ignore BadWindow errors. CXWindowsUtil::CErrorLock lock(m_display); // adjust event masks doSelectEvents(w); } void CXWindowsScreen::doSelectEvents(Window w) const { // we want to track the mouse everywhere on the display. to achieve // that we select PointerMotionMask on every window. we also select // SubstructureNotifyMask in order to get CreateNotify events so we // select events on new windows too. // // note that this can break certain clients due a design flaw of X. // X will deliver a PointerMotion event to the deepest window in the // hierarchy that contains the pointer and has PointerMotionMask // selected by *any* client. if another client doesn't select // motion events in a subwindow so the parent window will get them // then by selecting for motion events on the subwindow we break // that client because the parent will no longer get the events. // FIXME -- should provide some workaround for event selection // design flaw. perhaps only select for motion events on windows // that already do or are top-level windows or don't propagate // pointer events. or maybe an option to simply poll the mouse. // we don't want to adjust our grab window if (w == m_window) { return; } // select events of interest. do this before querying the tree so // we'll get notifications of children created after the XQueryTree() // so we won't miss them. XSelectInput(m_display, w, PointerMotionMask | SubstructureNotifyMask); // recurse on child windows Window rw, pw, *cw; unsigned int nc; if (XQueryTree(m_display, w, &rw, &pw, &cw, &nc)) { for (unsigned int i = 0; i < nc; ++i) { doSelectEvents(cw[i]); } XFree(cw); } } KeyID CXWindowsScreen::mapKeyFromX(XKeyEvent* event) const { // convert to a keysym KeySym keysym; if (event->type == KeyPress && m_ic != NULL) { // do multibyte lookup. can only call XmbLookupString with a // key press event and a valid XIC so we checked those above. char scratch[32]; int n = sizeof(scratch) / sizeof(scratch[0]); char* buffer = scratch; int status; n = XmbLookupString(m_ic, event, buffer, n, &keysym, &status); if (status == XBufferOverflow) { // not enough space. grow buffer and try again. buffer = new char[n]; n = XmbLookupString(m_ic, event, buffer, n, &keysym, &status); delete[] buffer; } // see what we got. since we don't care about the string // we'll just look for a keysym. switch (status) { default: case XLookupNone: case XLookupChars: keysym = 0; break; case XLookupKeySym: case XLookupBoth: break; } } else { // plain old lookup char dummy[1]; XLookupString(event, dummy, 0, &keysym, NULL); } // convert key 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(id); } void CXWindowsScreen::warpCursorNoFlush(SInt32 x, SInt32 y) { assert(m_window != None); // send an event that we can recognize before the mouse warp XEvent eventBefore; eventBefore.type = MotionNotify; eventBefore.xmotion.display = m_display; eventBefore.xmotion.window = m_window; eventBefore.xmotion.root = m_root; eventBefore.xmotion.subwindow = m_window; eventBefore.xmotion.time = CurrentTime; eventBefore.xmotion.x = x; eventBefore.xmotion.y = y; eventBefore.xmotion.x_root = x; eventBefore.xmotion.y_root = y; eventBefore.xmotion.state = 0; eventBefore.xmotion.is_hint = NotifyNormal; eventBefore.xmotion.same_screen = True; XEvent eventAfter = eventBefore; XSendEvent(m_display, m_window, False, 0, &eventBefore); // warp mouse XWarpPointer(m_display, None, m_root, 0, 0, 0, 0, x, y); // send an event that we can recognize after the mouse warp XSendEvent(m_display, m_window, False, 0, &eventAfter); XSync(m_display, False); LOG((CLOG_DEBUG2 "warped to %d,%d", x, y)); } void CXWindowsScreen::updateButtons() { // query the button mapping UInt32 numButtons = XGetPointerMapping(m_display, NULL, 0); unsigned char* tmpButtons = new unsigned char[numButtons]; XGetPointerMapping(m_display, tmpButtons, numButtons); // find the largest logical button id unsigned char maxButton = 0; for (UInt32 i = 0; i < numButtons; ++i) { if (tmpButtons[i] > maxButton) { maxButton = tmpButtons[i]; } } // allocate button array m_buttons.resize(maxButton); // fill in button array values. m_buttons[i] is the physical // button number for logical button i+1. for (UInt32 i = 0; i < numButtons; ++i) { m_buttons[i] = 0; } for (UInt32 i = 0; i < numButtons; ++i) { m_buttons[tmpButtons[i] - 1] = i + 1; } // clean up delete[] tmpButtons; } bool CXWindowsScreen::grabMouseAndKeyboard() { // 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; }