| /* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. |
| * Copyright 2011-2019 Pierre Ossman for Cendio AB |
| * |
| * This is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This software is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this software; if not, write to the Free Software |
| * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
| * USA. |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <assert.h> |
| #include <stdio.h> |
| #include <string.h> |
| |
| #include <rfb/CMsgWriter.h> |
| #include <rfb/LogWriter.h> |
| #include <rfb/Exception.h> |
| #include <rfb/ledStates.h> |
| |
| // FLTK can pull in the X11 headers on some systems |
| #ifndef XK_VoidSymbol |
| #define XK_LATIN1 |
| #define XK_MISCELLANY |
| #define XK_XKB_KEYS |
| #include <rfb/keysymdef.h> |
| #endif |
| |
| #ifndef XF86XK_ModeLock |
| #include <rfb/XF86keysym.h> |
| #endif |
| |
| #if ! (defined(WIN32) || defined(__APPLE__)) |
| #include <X11/XKBlib.h> |
| #endif |
| |
| #ifndef NoSymbol |
| #define NoSymbol 0 |
| #endif |
| |
| // Missing in at least some versions of MinGW |
| #ifndef MAPVK_VK_TO_VSC |
| #define MAPVK_VK_TO_VSC 0 |
| #endif |
| |
| #include "Viewport.h" |
| #include "CConn.h" |
| #include "OptionsDialog.h" |
| #include "DesktopWindow.h" |
| #include "i18n.h" |
| #include "fltk_layout.h" |
| #include "parameters.h" |
| #include "keysym2ucs.h" |
| #include "menukey.h" |
| #include "vncviewer.h" |
| |
| #include "PlatformPixelBuffer.h" |
| |
| #include <FL/fl_draw.H> |
| #include <FL/fl_ask.H> |
| |
| #include <FL/Fl_Menu.H> |
| #include <FL/Fl_Menu_Button.H> |
| |
| #if !defined(WIN32) && !defined(__APPLE__) |
| #include <X11/XKBlib.h> |
| extern const struct _code_map_xkb_to_qnum { |
| const char * from; |
| const unsigned short to; |
| } code_map_xkb_to_qnum[]; |
| extern const unsigned int code_map_xkb_to_qnum_len; |
| |
| static int code_map_keycode_to_qnum[256]; |
| #endif |
| |
| #ifdef __APPLE__ |
| #include "cocoa.h" |
| extern const unsigned short code_map_osx_to_qnum[]; |
| extern const unsigned int code_map_osx_to_qnum_len; |
| #endif |
| |
| #ifdef WIN32 |
| #include "win32.h" |
| #endif |
| |
| |
| using namespace rfb; |
| using namespace rdr; |
| |
| static rfb::LogWriter vlog("Viewport"); |
| |
| // Menu constants |
| |
| enum { ID_EXIT, ID_FULLSCREEN, ID_MINIMIZE, ID_RESIZE, |
| ID_CTRL, ID_ALT, ID_MENUKEY, ID_CTRLALTDEL, |
| ID_REFRESH, ID_OPTIONS, ID_INFO, ID_ABOUT, ID_DISMISS }; |
| |
| // Used to detect fake input (0xaa is not a real key) |
| #ifdef WIN32 |
| static const WORD SCAN_FAKE = 0xaa; |
| #endif |
| |
| Viewport::Viewport(int w, int h, const rfb::PixelFormat& serverPF, CConn* cc_) |
| : Fl_Widget(0, 0, w, h), cc(cc_), frameBuffer(NULL), |
| lastPointerPos(0, 0), lastButtonMask(0), |
| #ifdef WIN32 |
| altGrArmed(false), |
| #endif |
| firstLEDState(true), |
| pendingServerClipboard(false), pendingClientClipboard(false), |
| menuCtrlKey(false), menuAltKey(false), cursor(NULL) |
| { |
| #if !defined(WIN32) && !defined(__APPLE__) |
| XkbDescPtr xkb; |
| Status status; |
| |
| xkb = XkbGetMap(fl_display, 0, XkbUseCoreKbd); |
| if (!xkb) |
| throw rfb::Exception("XkbGetMap"); |
| |
| status = XkbGetNames(fl_display, XkbKeyNamesMask, xkb); |
| if (status != Success) |
| throw rfb::Exception("XkbGetNames"); |
| |
| memset(code_map_keycode_to_qnum, 0, sizeof(code_map_keycode_to_qnum)); |
| for (KeyCode keycode = xkb->min_key_code; |
| keycode < xkb->max_key_code; |
| keycode++) { |
| const char *keyname = xkb->names->keys[keycode].name; |
| unsigned short rfbcode; |
| |
| if (keyname[0] == '\0') |
| continue; |
| |
| rfbcode = 0; |
| for (unsigned i = 0;i < code_map_xkb_to_qnum_len;i++) { |
| if (strncmp(code_map_xkb_to_qnum[i].from, |
| keyname, XkbKeyNameLength) == 0) { |
| rfbcode = code_map_xkb_to_qnum[i].to; |
| break; |
| } |
| } |
| if (rfbcode != 0) |
| code_map_keycode_to_qnum[keycode] = rfbcode; |
| else |
| vlog.debug("No key mapping for key %.4s", keyname); |
| } |
| |
| XkbFreeKeyboard(xkb, 0, True); |
| #endif |
| |
| Fl::add_clipboard_notify(handleClipboardChange, this); |
| |
| // We need to intercept keyboard events early |
| Fl::add_system_handler(handleSystemEvent, this); |
| |
| frameBuffer = new PlatformPixelBuffer(w, h); |
| assert(frameBuffer); |
| cc->setFramebuffer(frameBuffer); |
| |
| contextMenu = new Fl_Menu_Button(0, 0, 0, 0); |
| // Setting box type to FL_NO_BOX prevents it from trying to draw the |
| // button component (which we don't want) |
| contextMenu->box(FL_NO_BOX); |
| |
| // The (invisible) button associated with this widget can mess with |
| // things like Fl_Scroll so we need to get rid of any parents. |
| // Unfortunately that's not possible because of STR #2654, but |
| // reparenting to the current window works for most cases. |
| window()->add(contextMenu); |
| |
| setMenuKey(); |
| |
| OptionsDialog::addCallback(handleOptions, this); |
| } |
| |
| |
| Viewport::~Viewport() |
| { |
| // Unregister all timeouts in case they get a change tro trigger |
| // again later when this object is already gone. |
| Fl::remove_timeout(handlePointerTimeout, this); |
| #ifdef WIN32 |
| Fl::remove_timeout(handleAltGrTimeout, this); |
| #endif |
| |
| Fl::remove_system_handler(handleSystemEvent); |
| |
| Fl::remove_clipboard_notify(handleClipboardChange); |
| |
| OptionsDialog::removeCallback(handleOptions); |
| |
| if (cursor) { |
| if (!cursor->alloc_array) |
| delete [] cursor->array; |
| delete cursor; |
| } |
| |
| // FLTK automatically deletes all child widgets, so we shouldn't touch |
| // them ourselves here |
| } |
| |
| |
| const rfb::PixelFormat &Viewport::getPreferredPF() |
| { |
| return frameBuffer->getPF(); |
| } |
| |
| |
| // Copy the areas of the framebuffer that have been changed (damaged) |
| // to the displayed window. |
| |
| void Viewport::updateWindow() |
| { |
| Rect r; |
| |
| r = frameBuffer->getDamage(); |
| damage(FL_DAMAGE_USER1, r.tl.x + x(), r.tl.y + y(), r.width(), r.height()); |
| } |
| |
| static const char * dotcursor_xpm[] = { |
| "5 5 2 1", |
| ". c #000000", |
| " c #FFFFFF", |
| " ", |
| " ... ", |
| " ... ", |
| " ... ", |
| " "}; |
| |
| void Viewport::setCursor(int width, int height, const Point& hotspot, |
| const rdr::U8* data) |
| { |
| int i; |
| |
| if (cursor) { |
| if (!cursor->alloc_array) |
| delete [] cursor->array; |
| delete cursor; |
| } |
| |
| for (i = 0; i < width*height; i++) |
| if (data[i*4 + 3] != 0) break; |
| |
| if ((i == width*height) && dotWhenNoCursor) { |
| vlog.debug("cursor is empty - using dot"); |
| |
| Fl_Pixmap pxm(dotcursor_xpm); |
| cursor = new Fl_RGB_Image(&pxm); |
| cursorHotspot.x = cursorHotspot.y = 2; |
| } else { |
| if ((width == 0) || (height == 0)) { |
| U8 *buffer = new U8[4]; |
| memset(buffer, 0, 4); |
| cursor = new Fl_RGB_Image(buffer, 1, 1, 4); |
| cursorHotspot.x = cursorHotspot.y = 0; |
| } else { |
| U8 *buffer = new U8[width * height * 4]; |
| memcpy(buffer, data, width * height * 4); |
| cursor = new Fl_RGB_Image(buffer, width, height, 4); |
| cursorHotspot = hotspot; |
| } |
| } |
| |
| if (Fl::belowmouse() == this) |
| window()->cursor(cursor, cursorHotspot.x, cursorHotspot.y); |
| } |
| |
| void Viewport::handleClipboardRequest() |
| { |
| Fl::paste(*this, clipboardSource); |
| } |
| |
| void Viewport::handleClipboardAnnounce(bool available) |
| { |
| if (!acceptClipboard) |
| return; |
| |
| if (available) |
| vlog.debug("Got notification of new clipboard on server"); |
| else |
| vlog.debug("Clipboard is no longer available on server"); |
| |
| if (!available) { |
| pendingServerClipboard = false; |
| return; |
| } |
| |
| pendingClientClipboard = false; |
| |
| if (!hasFocus()) { |
| pendingServerClipboard = true; |
| return; |
| } |
| |
| cc->requestClipboard(); |
| } |
| |
| void Viewport::handleClipboardData(const char* data) |
| { |
| size_t len; |
| |
| if (!hasFocus()) |
| return; |
| |
| len = strlen(data); |
| |
| vlog.debug("Got clipboard data (%d bytes)", (int)len); |
| |
| // RFB doesn't have separate selection and clipboard concepts, so we |
| // dump the data into both variants. |
| #if !defined(WIN32) && !defined(__APPLE__) |
| if (setPrimary) |
| Fl::copy(data, len, 0); |
| #endif |
| Fl::copy(data, len, 1); |
| } |
| |
| void Viewport::setLEDState(unsigned int state) |
| { |
| vlog.debug("Got server LED state: 0x%08x", state); |
| |
| // The first message is just considered to be the server announcing |
| // support for this extension. We will push our state to sync up the |
| // server when we get focus. If we already have focus we need to push |
| // it here though. |
| if (firstLEDState) { |
| firstLEDState = false; |
| if (hasFocus()) |
| pushLEDState(); |
| return; |
| } |
| |
| if (!hasFocus()) |
| return; |
| |
| #if defined(WIN32) |
| INPUT input[6]; |
| UINT count; |
| UINT ret; |
| |
| memset(input, 0, sizeof(input)); |
| count = 0; |
| |
| if (!!(state & ledCapsLock) != !!(GetKeyState(VK_CAPITAL) & 0x1)) { |
| input[count].type = input[count+1].type = INPUT_KEYBOARD; |
| input[count].ki.wVk = input[count+1].ki.wVk = VK_CAPITAL; |
| input[count].ki.wScan = input[count+1].ki.wScan = SCAN_FAKE; |
| input[count].ki.dwFlags = 0; |
| input[count+1].ki.dwFlags = KEYEVENTF_KEYUP; |
| count += 2; |
| } |
| |
| if (!!(state & ledNumLock) != !!(GetKeyState(VK_NUMLOCK) & 0x1)) { |
| input[count].type = input[count+1].type = INPUT_KEYBOARD; |
| input[count].ki.wVk = input[count+1].ki.wVk = VK_NUMLOCK; |
| input[count].ki.wScan = input[count+1].ki.wScan = SCAN_FAKE; |
| input[count].ki.dwFlags = KEYEVENTF_EXTENDEDKEY; |
| input[count+1].ki.dwFlags = KEYEVENTF_KEYUP | KEYEVENTF_EXTENDEDKEY; |
| count += 2; |
| } |
| |
| if (!!(state & ledScrollLock) != !!(GetKeyState(VK_SCROLL) & 0x1)) { |
| input[count].type = input[count+1].type = INPUT_KEYBOARD; |
| input[count].ki.wVk = input[count+1].ki.wVk = VK_SCROLL; |
| input[count].ki.wScan = input[count+1].ki.wScan = SCAN_FAKE; |
| input[count].ki.dwFlags = 0; |
| input[count+1].ki.dwFlags = KEYEVENTF_KEYUP; |
| count += 2; |
| } |
| |
| if (count == 0) |
| return; |
| |
| ret = SendInput(count, input, sizeof(*input)); |
| if (ret < count) |
| vlog.error(_("Failed to update keyboard LED state: %lu"), GetLastError()); |
| #elif defined(__APPLE__) |
| int ret; |
| |
| ret = cocoa_set_caps_lock_state(state & ledCapsLock); |
| if (ret != 0) { |
| vlog.error(_("Failed to update keyboard LED state: %d"), ret); |
| return; |
| } |
| |
| ret = cocoa_set_num_lock_state(state & ledNumLock); |
| if (ret != 0) { |
| vlog.error(_("Failed to update keyboard LED state: %d"), ret); |
| return; |
| } |
| |
| // No support for Scroll Lock // |
| |
| #else |
| unsigned int affect, values; |
| unsigned int mask; |
| |
| Bool ret; |
| |
| affect = values = 0; |
| |
| affect |= LockMask; |
| if (state & ledCapsLock) |
| values |= LockMask; |
| |
| mask = getModifierMask(XK_Num_Lock); |
| affect |= mask; |
| if (state & ledNumLock) |
| values |= mask; |
| |
| mask = getModifierMask(XK_Scroll_Lock); |
| affect |= mask; |
| if (state & ledScrollLock) |
| values |= mask; |
| |
| ret = XkbLockModifiers(fl_display, XkbUseCoreKbd, affect, values); |
| if (!ret) |
| vlog.error(_("Failed to update keyboard LED state")); |
| #endif |
| } |
| |
| void Viewport::pushLEDState() |
| { |
| unsigned int state; |
| |
| // Server support? |
| if (cc->server.ledState() == ledUnknown) |
| return; |
| |
| state = 0; |
| |
| #if defined(WIN32) |
| if (GetKeyState(VK_CAPITAL) & 0x1) |
| state |= ledCapsLock; |
| if (GetKeyState(VK_NUMLOCK) & 0x1) |
| state |= ledNumLock; |
| if (GetKeyState(VK_SCROLL) & 0x1) |
| state |= ledScrollLock; |
| #elif defined(__APPLE__) |
| int ret; |
| bool on; |
| |
| ret = cocoa_get_caps_lock_state(&on); |
| if (ret != 0) { |
| vlog.error(_("Failed to get keyboard LED state: %d"), ret); |
| return; |
| } |
| if (on) |
| state |= ledCapsLock; |
| |
| ret = cocoa_get_num_lock_state(&on); |
| if (ret != 0) { |
| vlog.error(_("Failed to get keyboard LED state: %d"), ret); |
| return; |
| } |
| if (on) |
| state |= ledNumLock; |
| |
| // No support for Scroll Lock // |
| state |= (cc->server.ledState() & ledScrollLock); |
| |
| #else |
| unsigned int mask; |
| |
| Status status; |
| XkbStateRec xkbState; |
| |
| status = XkbGetState(fl_display, XkbUseCoreKbd, &xkbState); |
| if (status != Success) { |
| vlog.error(_("Failed to get keyboard LED state: %d"), status); |
| return; |
| } |
| |
| if (xkbState.locked_mods & LockMask) |
| state |= ledCapsLock; |
| |
| mask = getModifierMask(XK_Num_Lock); |
| if (xkbState.locked_mods & mask) |
| state |= ledNumLock; |
| |
| mask = getModifierMask(XK_Scroll_Lock); |
| if (xkbState.locked_mods & mask) |
| state |= ledScrollLock; |
| #endif |
| |
| if ((state & ledCapsLock) != (cc->server.ledState() & ledCapsLock)) { |
| vlog.debug("Inserting fake CapsLock to get in sync with server"); |
| handleKeyPress(0x3a, XK_Caps_Lock); |
| handleKeyRelease(0x3a); |
| } |
| if ((state & ledNumLock) != (cc->server.ledState() & ledNumLock)) { |
| vlog.debug("Inserting fake NumLock to get in sync with server"); |
| handleKeyPress(0x45, XK_Num_Lock); |
| handleKeyRelease(0x45); |
| } |
| if ((state & ledScrollLock) != (cc->server.ledState() & ledScrollLock)) { |
| vlog.debug("Inserting fake ScrollLock to get in sync with server"); |
| handleKeyPress(0x46, XK_Scroll_Lock); |
| handleKeyRelease(0x46); |
| } |
| } |
| |
| |
| void Viewport::draw(Surface* dst) |
| { |
| int X, Y, W, H; |
| |
| // Check what actually needs updating |
| fl_clip_box(x(), y(), w(), h(), X, Y, W, H); |
| if ((W == 0) || (H == 0)) |
| return; |
| |
| frameBuffer->draw(dst, X - x(), Y - y(), X, Y, W, H); |
| } |
| |
| |
| void Viewport::draw() |
| { |
| int X, Y, W, H; |
| |
| // Check what actually needs updating |
| fl_clip_box(x(), y(), w(), h(), X, Y, W, H); |
| if ((W == 0) || (H == 0)) |
| return; |
| |
| frameBuffer->draw(X - x(), Y - y(), X, Y, W, H); |
| } |
| |
| |
| void Viewport::resize(int x, int y, int w, int h) |
| { |
| if ((w != frameBuffer->width()) || (h != frameBuffer->height())) { |
| vlog.debug("Resizing framebuffer from %dx%d to %dx%d", |
| frameBuffer->width(), frameBuffer->height(), w, h); |
| |
| frameBuffer = new PlatformPixelBuffer(w, h); |
| assert(frameBuffer); |
| cc->setFramebuffer(frameBuffer); |
| } |
| |
| Fl_Widget::resize(x, y, w, h); |
| } |
| |
| |
| int Viewport::handle(int event) |
| { |
| char *filtered; |
| int buttonMask, wheelMask; |
| DownMap::const_iterator iter; |
| |
| switch (event) { |
| case FL_PASTE: |
| filtered = convertLF(Fl::event_text(), Fl::event_length()); |
| |
| vlog.debug("Sending clipboard data (%d bytes)", (int)strlen(filtered)); |
| |
| try { |
| cc->sendClipboardData(filtered); |
| } catch (rdr::Exception& e) { |
| vlog.error("%s", e.str()); |
| exit_vncviewer(e.str()); |
| } |
| |
| strFree(filtered); |
| |
| return 1; |
| |
| case FL_ENTER: |
| if (cursor) |
| window()->cursor(cursor, cursorHotspot.x, cursorHotspot.y); |
| // Yes, we would like some pointer events please! |
| return 1; |
| |
| case FL_LEAVE: |
| window()->cursor(FL_CURSOR_DEFAULT); |
| // We want a last move event to help trigger edge stuff |
| handlePointerEvent(Point(Fl::event_x() - x(), Fl::event_y() - y()), 0); |
| return 1; |
| |
| case FL_PUSH: |
| case FL_RELEASE: |
| case FL_DRAG: |
| case FL_MOVE: |
| case FL_MOUSEWHEEL: |
| buttonMask = 0; |
| if (Fl::event_button1()) |
| buttonMask |= 1; |
| if (Fl::event_button2()) |
| buttonMask |= 2; |
| if (Fl::event_button3()) |
| buttonMask |= 4; |
| |
| if (event == FL_MOUSEWHEEL) { |
| wheelMask = 0; |
| if (Fl::event_dy() < 0) |
| wheelMask |= 8; |
| if (Fl::event_dy() > 0) |
| wheelMask |= 16; |
| if (Fl::event_dx() < 0) |
| wheelMask |= 32; |
| if (Fl::event_dx() > 0) |
| wheelMask |= 64; |
| |
| // A quick press of the wheel "button", followed by a immediate |
| // release below |
| handlePointerEvent(Point(Fl::event_x() - x(), Fl::event_y() - y()), |
| buttonMask | wheelMask); |
| } |
| |
| handlePointerEvent(Point(Fl::event_x() - x(), Fl::event_y() - y()), buttonMask); |
| return 1; |
| |
| case FL_FOCUS: |
| Fl::disable_im(); |
| |
| flushPendingClipboard(); |
| |
| // We may have gotten our lock keys out of sync with the server |
| // whilst we didn't have focus. Try to sort this out. |
| pushLEDState(); |
| |
| // Resend Ctrl/Alt if needed |
| if (menuCtrlKey) |
| handleKeyPress(0x1d, XK_Control_L); |
| if (menuAltKey) |
| handleKeyPress(0x38, XK_Alt_L); |
| |
| // Yes, we would like some focus please! |
| return 1; |
| |
| case FL_UNFOCUS: |
| // Release all keys that were pressed as that generally makes most |
| // sense (e.g. Alt+Tab where we only see the Alt press) |
| while (!downKeySym.empty()) |
| handleKeyRelease(downKeySym.begin()->first); |
| Fl::enable_im(); |
| return 1; |
| |
| case FL_KEYDOWN: |
| case FL_KEYUP: |
| // Just ignore these as keys were handled in the event handler |
| return 1; |
| } |
| |
| return Fl_Widget::handle(event); |
| } |
| |
| |
| bool Viewport::hasFocus() |
| { |
| Fl_Widget* focus; |
| |
| focus = Fl::grab(); |
| if (!focus) |
| focus = Fl::focus(); |
| |
| return focus == this; |
| } |
| |
| #if ! (defined(WIN32) || defined(__APPLE__)) |
| unsigned int Viewport::getModifierMask(unsigned int keysym) |
| { |
| XkbDescPtr xkb; |
| unsigned int mask, keycode; |
| XkbAction *act; |
| |
| mask = 0; |
| |
| xkb = XkbGetMap(fl_display, XkbAllComponentsMask, XkbUseCoreKbd); |
| if (xkb == NULL) |
| return 0; |
| |
| for (keycode = xkb->min_key_code; keycode <= xkb->max_key_code; keycode++) { |
| unsigned int state_out; |
| KeySym ks; |
| |
| XkbTranslateKeyCode(xkb, keycode, 0, &state_out, &ks); |
| if (ks == NoSymbol) |
| continue; |
| |
| if (ks == keysym) |
| break; |
| } |
| |
| // KeySym not mapped? |
| if (keycode > xkb->max_key_code) |
| goto out; |
| |
| act = XkbKeyAction(xkb, keycode, 0); |
| if (act == NULL) |
| goto out; |
| if (act->type != XkbSA_LockMods) |
| goto out; |
| |
| if (act->mods.flags & XkbSA_UseModMapMods) |
| mask = xkb->map->modmap[keycode]; |
| else |
| mask = act->mods.mask; |
| |
| out: |
| XkbFreeKeyboard(xkb, XkbAllComponentsMask, True); |
| |
| return mask; |
| } |
| #endif |
| |
| |
| void Viewport::handleClipboardChange(int source, void *data) |
| { |
| Viewport *self = (Viewport *)data; |
| |
| assert(self); |
| |
| if (!sendClipboard) |
| return; |
| |
| #if !defined(WIN32) && !defined(__APPLE__) |
| if (!sendPrimary && (source == 0)) |
| return; |
| #endif |
| |
| self->clipboardSource = source; |
| |
| self->pendingServerClipboard = false; |
| |
| if (!self->hasFocus()) { |
| self->pendingClientClipboard = true; |
| // Clear any older client clipboard from the server |
| self->cc->announceClipboard(false); |
| return; |
| } |
| |
| try { |
| self->cc->announceClipboard(true); |
| } catch (rdr::Exception& e) { |
| vlog.error("%s", e.str()); |
| exit_vncviewer(e.str()); |
| } |
| } |
| |
| |
| void Viewport::flushPendingClipboard() |
| { |
| if (pendingServerClipboard) { |
| try { |
| cc->requestClipboard(); |
| } catch (rdr::Exception& e) { |
| vlog.error("%s", e.str()); |
| exit_vncviewer(e.str()); |
| } |
| } |
| if (pendingClientClipboard) { |
| try { |
| cc->announceClipboard(true); |
| } catch (rdr::Exception& e) { |
| vlog.error("%s", e.str()); |
| exit_vncviewer(e.str()); |
| } |
| } |
| |
| pendingServerClipboard = false; |
| pendingClientClipboard = false; |
| } |
| |
| |
| void Viewport::handlePointerEvent(const rfb::Point& pos, int buttonMask) |
| { |
| if (!viewOnly) { |
| if (pointerEventInterval == 0 || buttonMask != lastButtonMask) { |
| try { |
| cc->writer()->writePointerEvent(pos, buttonMask); |
| } catch (rdr::Exception& e) { |
| vlog.error("%s", e.str()); |
| exit_vncviewer(e.str()); |
| } |
| } else { |
| if (!Fl::has_timeout(handlePointerTimeout, this)) |
| Fl::add_timeout((double)pointerEventInterval/1000.0, |
| handlePointerTimeout, this); |
| } |
| lastPointerPos = pos; |
| lastButtonMask = buttonMask; |
| } |
| } |
| |
| |
| void Viewport::handlePointerTimeout(void *data) |
| { |
| Viewport *self = (Viewport *)data; |
| |
| assert(self); |
| |
| try { |
| self->cc->writer()->writePointerEvent(self->lastPointerPos, |
| self->lastButtonMask); |
| } catch (rdr::Exception& e) { |
| vlog.error("%s", e.str()); |
| exit_vncviewer(e.str()); |
| } |
| } |
| |
| |
| void Viewport::handleKeyPress(int keyCode, rdr::U32 keySym) |
| { |
| static bool menuRecursion = false; |
| |
| // Prevent recursion if the menu wants to send its own |
| // activation key. |
| if (menuKeySym && (keySym == menuKeySym) && !menuRecursion) { |
| menuRecursion = true; |
| popupContextMenu(); |
| menuRecursion = false; |
| return; |
| } |
| |
| if (viewOnly) |
| return; |
| |
| if (keyCode == 0) { |
| vlog.error(_("No key code specified on key press")); |
| return; |
| } |
| |
| #ifdef __APPLE__ |
| // Alt on OS X behaves more like AltGr on other systems, and to get |
| // sane behaviour we should translate things in that manner for the |
| // remote VNC server. However that means we lose the ability to use |
| // Alt as a shortcut modifier. Do what RealVNC does and hijack the |
| // left command key as an Alt replacement. |
| switch (keySym) { |
| case XK_Super_L: |
| keySym = XK_Alt_L; |
| break; |
| case XK_Super_R: |
| keySym = XK_Super_L; |
| break; |
| case XK_Alt_L: |
| keySym = XK_Mode_switch; |
| break; |
| case XK_Alt_R: |
| keySym = XK_ISO_Level3_Shift; |
| break; |
| } |
| #endif |
| |
| // Because of the way keyboards work, we cannot expect to have the same |
| // symbol on release as when pressed. This breaks the VNC protocol however, |
| // so we need to keep track of what keysym a key _code_ generated on press |
| // and send the same on release. |
| downKeySym[keyCode] = keySym; |
| |
| #if defined(WIN32) || defined(__APPLE__) |
| vlog.debug("Key pressed: 0x%04x => 0x%04x", keyCode, keySym); |
| #else |
| vlog.debug("Key pressed: 0x%04x => XK_%s (0x%04x)", |
| keyCode, XKeysymToString(keySym), keySym); |
| #endif |
| |
| try { |
| // Fake keycode? |
| if (keyCode > 0xff) |
| cc->writer()->writeKeyEvent(keySym, 0, true); |
| else |
| cc->writer()->writeKeyEvent(keySym, keyCode, true); |
| } catch (rdr::Exception& e) { |
| vlog.error("%s", e.str()); |
| exit_vncviewer(e.str()); |
| } |
| } |
| |
| |
| void Viewport::handleKeyRelease(int keyCode) |
| { |
| DownMap::iterator iter; |
| |
| if (viewOnly) |
| return; |
| |
| iter = downKeySym.find(keyCode); |
| if (iter == downKeySym.end()) { |
| // These occur somewhat frequently so let's not spam them unless |
| // logging is turned up. |
| vlog.debug("Unexpected release of key code %d", keyCode); |
| return; |
| } |
| |
| #if defined(WIN32) || defined(__APPLE__) |
| vlog.debug("Key released: 0x%04x => 0x%04x", keyCode, iter->second); |
| #else |
| vlog.debug("Key released: 0x%04x => XK_%s (0x%04x)", |
| keyCode, XKeysymToString(iter->second), iter->second); |
| #endif |
| |
| try { |
| if (keyCode > 0xff) |
| cc->writer()->writeKeyEvent(iter->second, 0, false); |
| else |
| cc->writer()->writeKeyEvent(iter->second, keyCode, false); |
| } catch (rdr::Exception& e) { |
| vlog.error("%s", e.str()); |
| exit_vncviewer(e.str()); |
| } |
| |
| downKeySym.erase(iter); |
| } |
| |
| |
| int Viewport::handleSystemEvent(void *event, void *data) |
| { |
| Viewport *self = (Viewport *)data; |
| |
| assert(self); |
| |
| if (!self->hasFocus()) |
| return 0; |
| |
| assert(event); |
| |
| #if defined(WIN32) |
| MSG *msg = (MSG*)event; |
| |
| if ((msg->message == WM_KEYDOWN) || (msg->message == WM_SYSKEYDOWN)) { |
| UINT vKey; |
| bool isExtended; |
| int keyCode; |
| rdr::U32 keySym; |
| |
| vKey = msg->wParam; |
| isExtended = (msg->lParam & (1 << 24)) != 0; |
| |
| keyCode = ((msg->lParam >> 16) & 0xff); |
| |
| // Windows' touch keyboard doesn't set a scan code for the Alt |
| // portion of the AltGr sequence, so we need to help it out |
| if (!isExtended && (keyCode == 0x00) && (vKey == VK_MENU)) { |
| isExtended = true; |
| keyCode = 0x38; |
| } |
| |
| // Windows doesn't have a proper AltGr, but handles it using fake |
| // Ctrl+Alt. However the remote end might not be Windows, so we need |
| // to merge those in to a single AltGr event. We detect this case |
| // by seeing the two key events directly after each other with a very |
| // short time between them (<50ms) and supress the Ctrl event. |
| if (self->altGrArmed) { |
| self->altGrArmed = false; |
| Fl::remove_timeout(handleAltGrTimeout); |
| |
| if (isExtended && (keyCode == 0x38) && (vKey == VK_MENU) && |
| ((msg->time - self->altGrCtrlTime) < 50)) { |
| // Alt seen, so this is an AltGr sequence |
| } else { |
| // Not Alt, so fire the queued up Ctrl event |
| self->handleKeyPress(0x1d, XK_Control_L); |
| } |
| } |
| |
| if (keyCode == SCAN_FAKE) { |
| vlog.debug("Ignoring fake key press (virtual key 0x%02x)", vKey); |
| return 1; |
| } |
| |
| // Windows sets the scan code to 0x00 for multimedia keys, so we |
| // have to do a reverse lookup based on the vKey. |
| if (keyCode == 0x00) { |
| keyCode = MapVirtualKey(vKey, MAPVK_VK_TO_VSC); |
| if (keyCode == 0x00) { |
| if (isExtended) |
| vlog.error(_("No scan code for extended virtual key 0x%02x"), (int)vKey); |
| else |
| vlog.error(_("No scan code for virtual key 0x%02x"), (int)vKey); |
| return 1; |
| } |
| } |
| |
| if (keyCode & ~0x7f) { |
| vlog.error(_("Invalid scan code 0x%02x"), (int)keyCode); |
| return 1; |
| } |
| |
| if (isExtended) |
| keyCode |= 0x80; |
| |
| |
| // Fortunately RFB and Windows use the same scan code set (mostly), |
| // so there is no conversion needed |
| // (as long as we encode the extended keys with the high bit) |
| |
| // However Pause sends a code that conflicts with NumLock, so use |
| // the code most RFB implementations use (part of the sequence for |
| // Ctrl+Pause, i.e. Break) |
| if (keyCode == 0x45) |
| keyCode = 0xc6; |
| |
| // And NumLock incorrectly has the extended bit set |
| if (keyCode == 0xc5) |
| keyCode = 0x45; |
| |
| // And Alt+PrintScreen (i.e. SysRq) sends a different code than |
| // PrintScreen |
| if (keyCode == 0xb7) |
| keyCode = 0x54; |
| |
| keySym = win32_vkey_to_keysym(vKey, isExtended); |
| if (keySym == NoSymbol) { |
| if (isExtended) |
| vlog.error(_("No symbol for extended virtual key 0x%02x"), (int)vKey); |
| else |
| vlog.error(_("No symbol for virtual key 0x%02x"), (int)vKey); |
| } |
| |
| // Windows sends the same vKey for both shifts, so we need to look |
| // at the scan code to tell them apart |
| if ((keySym == XK_Shift_L) && (keyCode == 0x36)) |
| keySym = XK_Shift_R; |
| |
| // AltGr handling (see above) |
| if (win32_has_altgr()) { |
| if ((keyCode == 0xb8) && (keySym == XK_Alt_R)) |
| keySym = XK_ISO_Level3_Shift; |
| |
| // Possible start of AltGr sequence? |
| if ((keyCode == 0x1d) && (keySym == XK_Control_L)) { |
| self->altGrArmed = true; |
| self->altGrCtrlTime = msg->time; |
| Fl::add_timeout(0.1, handleAltGrTimeout, self); |
| return 1; |
| } |
| } |
| |
| self->handleKeyPress(keyCode, keySym); |
| |
| return 1; |
| } else if ((msg->message == WM_KEYUP) || (msg->message == WM_SYSKEYUP)) { |
| UINT vKey; |
| bool isExtended; |
| int keyCode; |
| |
| vKey = msg->wParam; |
| isExtended = (msg->lParam & (1 << 24)) != 0; |
| |
| keyCode = ((msg->lParam >> 16) & 0xff); |
| |
| // Touch keyboard AltGr (see above) |
| if (!isExtended && (keyCode == 0x00) && (vKey == VK_MENU)) { |
| isExtended = true; |
| keyCode = 0x38; |
| } |
| |
| // We can't get a release in the middle of an AltGr sequence, so |
| // abort that detection |
| if (self->altGrArmed) { |
| self->altGrArmed = false; |
| Fl::remove_timeout(handleAltGrTimeout); |
| self->handleKeyPress(0x1d, XK_Control_L); |
| } |
| |
| if (keyCode == SCAN_FAKE) { |
| vlog.debug("Ignoring fake key release (virtual key 0x%02x)", vKey); |
| return 1; |
| } |
| |
| if (keyCode == 0x00) |
| keyCode = MapVirtualKey(vKey, MAPVK_VK_TO_VSC); |
| if (isExtended) |
| keyCode |= 0x80; |
| if (keyCode == 0x45) |
| keyCode = 0xc6; |
| if (keyCode == 0xc5) |
| keyCode = 0x45; |
| if (keyCode == 0xb7) |
| keyCode = 0x54; |
| |
| self->handleKeyRelease(keyCode); |
| |
| // Windows has a rather nasty bug where it won't send key release |
| // events for a Shift button if the other Shift is still pressed |
| if ((keyCode == 0x2a) || (keyCode == 0x36)) { |
| if (self->downKeySym.count(0x2a)) |
| self->handleKeyRelease(0x2a); |
| if (self->downKeySym.count(0x36)) |
| self->handleKeyRelease(0x36); |
| } |
| |
| return 1; |
| } |
| #elif defined(__APPLE__) |
| if (cocoa_is_keyboard_event(event)) { |
| int keyCode; |
| |
| keyCode = cocoa_event_keycode(event); |
| if ((unsigned)keyCode >= code_map_osx_to_qnum_len) |
| keyCode = 0; |
| else |
| keyCode = code_map_osx_to_qnum[keyCode]; |
| |
| if (cocoa_is_key_press(event)) { |
| rdr::U32 keySym; |
| |
| keySym = cocoa_event_keysym(event); |
| if (keySym == NoSymbol) { |
| vlog.error(_("No symbol for key code 0x%02x (in the current state)"), |
| (int)keyCode); |
| } |
| |
| self->handleKeyPress(keyCode, keySym); |
| |
| // We don't get any release events for CapsLock, so we have to |
| // send the release right away. |
| if (keySym == XK_Caps_Lock) |
| self->handleKeyRelease(keyCode); |
| } else { |
| self->handleKeyRelease(keyCode); |
| } |
| |
| return 1; |
| } |
| #else |
| XEvent *xevent = (XEvent*)event; |
| |
| if (xevent->type == KeyPress) { |
| int keycode; |
| char str; |
| KeySym keysym; |
| |
| keycode = code_map_keycode_to_qnum[xevent->xkey.keycode]; |
| |
| // Generate a fake keycode just for tracking if we can't figure |
| // out the proper one |
| if (keycode == 0) |
| keycode = 0x100 | xevent->xkey.keycode; |
| |
| XLookupString(&xevent->xkey, &str, 1, &keysym, NULL); |
| if (keysym == NoSymbol) { |
| vlog.error(_("No symbol for key code %d (in the current state)"), |
| (int)xevent->xkey.keycode); |
| } |
| |
| switch (keysym) { |
| // For the first few years, there wasn't a good consensus on what the |
| // Windows keys should be mapped to for X11. So we need to help out a |
| // bit and map all variants to the same key... |
| case XK_Hyper_L: |
| keysym = XK_Super_L; |
| break; |
| case XK_Hyper_R: |
| keysym = XK_Super_R; |
| break; |
| // There has been several variants for Shift-Tab over the years. |
| // RFB states that we should always send a normal tab. |
| case XK_ISO_Left_Tab: |
| keysym = XK_Tab; |
| break; |
| } |
| |
| self->handleKeyPress(keycode, keysym); |
| return 1; |
| } else if (xevent->type == KeyRelease) { |
| int keycode = code_map_keycode_to_qnum[xevent->xkey.keycode]; |
| if (keycode == 0) |
| keycode = 0x100 | xevent->xkey.keycode; |
| self->handleKeyRelease(keycode); |
| return 1; |
| } |
| #endif |
| |
| return 0; |
| } |
| |
| #ifdef WIN32 |
| void Viewport::handleAltGrTimeout(void *data) |
| { |
| Viewport *self = (Viewport *)data; |
| |
| assert(self); |
| |
| self->altGrArmed = false; |
| self->handleKeyPress(0x1d, XK_Control_L); |
| } |
| #endif |
| |
| void Viewport::initContextMenu() |
| { |
| contextMenu->clear(); |
| |
| fltk_menu_add(contextMenu, p_("ContextMenu|", "E&xit viewer"), |
| 0, NULL, (void*)ID_EXIT, FL_MENU_DIVIDER); |
| |
| fltk_menu_add(contextMenu, p_("ContextMenu|", "&Full screen"), |
| 0, NULL, (void*)ID_FULLSCREEN, |
| FL_MENU_TOGGLE | (window()->fullscreen_active()?FL_MENU_VALUE:0)); |
| fltk_menu_add(contextMenu, p_("ContextMenu|", "Minimi&ze"), |
| 0, NULL, (void*)ID_MINIMIZE, 0); |
| fltk_menu_add(contextMenu, p_("ContextMenu|", "Resize &window to session"), |
| 0, NULL, (void*)ID_RESIZE, |
| (window()->fullscreen_active()?FL_MENU_INACTIVE:0) | |
| FL_MENU_DIVIDER); |
| |
| fltk_menu_add(contextMenu, p_("ContextMenu|", "&Ctrl"), |
| 0, NULL, (void*)ID_CTRL, |
| FL_MENU_TOGGLE | (menuCtrlKey?FL_MENU_VALUE:0)); |
| fltk_menu_add(contextMenu, p_("ContextMenu|", "&Alt"), |
| 0, NULL, (void*)ID_ALT, |
| FL_MENU_TOGGLE | (menuAltKey?FL_MENU_VALUE:0)); |
| |
| if (menuKeySym) { |
| char sendMenuKey[64]; |
| snprintf(sendMenuKey, 64, p_("ContextMenu|", "Send %s"), (const char *)menuKey); |
| fltk_menu_add(contextMenu, sendMenuKey, 0, NULL, (void*)ID_MENUKEY, 0); |
| fltk_menu_add(contextMenu, "Secret shortcut menu key", menuKeyFLTK, NULL, |
| (void*)ID_MENUKEY, FL_MENU_INVISIBLE); |
| } |
| |
| fltk_menu_add(contextMenu, p_("ContextMenu|", "Send Ctrl-Alt-&Del"), |
| 0, NULL, (void*)ID_CTRLALTDEL, FL_MENU_DIVIDER); |
| |
| fltk_menu_add(contextMenu, p_("ContextMenu|", "&Refresh screen"), |
| 0, NULL, (void*)ID_REFRESH, FL_MENU_DIVIDER); |
| |
| fltk_menu_add(contextMenu, p_("ContextMenu|", "&Options..."), |
| 0, NULL, (void*)ID_OPTIONS, 0); |
| fltk_menu_add(contextMenu, p_("ContextMenu|", "Connection &info..."), |
| 0, NULL, (void*)ID_INFO, 0); |
| fltk_menu_add(contextMenu, p_("ContextMenu|", "About &TigerVNC viewer..."), |
| 0, NULL, (void*)ID_ABOUT, FL_MENU_DIVIDER); |
| |
| fltk_menu_add(contextMenu, p_("ContextMenu|", "Dismiss &menu"), |
| 0, NULL, (void*)ID_DISMISS, 0); |
| } |
| |
| |
| void Viewport::popupContextMenu() |
| { |
| const Fl_Menu_Item *m; |
| char buffer[1024]; |
| |
| // Make sure the menu is reset to its initial state between goes or |
| // it will start up highlighting the previously selected entry. |
| contextMenu->value(-1); |
| |
| // initialize context menu before display |
| initContextMenu(); |
| |
| // Unfortunately FLTK doesn't reliably restore the mouse pointer for |
| // menus, so we have to help it out. |
| if (Fl::belowmouse() == this) |
| window()->cursor(FL_CURSOR_DEFAULT); |
| |
| // FLTK also doesn't switch focus properly for menus |
| handle(FL_UNFOCUS); |
| |
| m = contextMenu->popup(); |
| |
| handle(FL_FOCUS); |
| |
| // Back to our proper mouse pointer. |
| if ((Fl::belowmouse() == this) && cursor) |
| window()->cursor(cursor, cursorHotspot.x, cursorHotspot.y); |
| |
| if (m == NULL) |
| return; |
| |
| switch (m->argument()) { |
| case ID_EXIT: |
| exit_vncviewer(); |
| break; |
| case ID_FULLSCREEN: |
| if (window()->fullscreen_active()) |
| window()->fullscreen_off(); |
| else |
| ((DesktopWindow*)window())->fullscreen_on(); |
| break; |
| case ID_MINIMIZE: |
| window()->iconize(); |
| break; |
| case ID_RESIZE: |
| if (window()->fullscreen_active()) |
| break; |
| window()->size(w(), h()); |
| break; |
| case ID_CTRL: |
| if (m->value()) |
| handleKeyPress(0x1d, XK_Control_L); |
| else |
| handleKeyRelease(0x1d); |
| menuCtrlKey = !menuCtrlKey; |
| break; |
| case ID_ALT: |
| if (m->value()) |
| handleKeyPress(0x38, XK_Alt_L); |
| else |
| handleKeyRelease(0x38); |
| menuAltKey = !menuAltKey; |
| break; |
| case ID_MENUKEY: |
| handleKeyPress(menuKeyCode, menuKeySym); |
| handleKeyRelease(menuKeyCode); |
| break; |
| case ID_CTRLALTDEL: |
| handleKeyPress(0x1d, XK_Control_L); |
| handleKeyPress(0x38, XK_Alt_L); |
| handleKeyPress(0xd3, XK_Delete); |
| |
| handleKeyRelease(0xd3); |
| handleKeyRelease(0x38); |
| handleKeyRelease(0x1d); |
| break; |
| case ID_REFRESH: |
| cc->refreshFramebuffer(); |
| break; |
| case ID_OPTIONS: |
| OptionsDialog::showDialog(); |
| break; |
| case ID_INFO: |
| if (fltk_escape(cc->connectionInfo(), buffer, sizeof(buffer)) < sizeof(buffer)) { |
| fl_message_title(_("VNC connection info")); |
| fl_message("%s", buffer); |
| } |
| break; |
| case ID_ABOUT: |
| about_vncviewer(); |
| break; |
| case ID_DISMISS: |
| // Don't need to do anything |
| break; |
| } |
| } |
| |
| |
| void Viewport::setMenuKey() |
| { |
| getMenuKey(&menuKeyFLTK, &menuKeyCode, &menuKeySym); |
| } |
| |
| |
| void Viewport::handleOptions(void *data) |
| { |
| Viewport *self = (Viewport*)data; |
| |
| self->setMenuKey(); |
| } |