diff options
Diffstat (limited to 'vncviewer')
33 files changed, 1811 insertions, 793 deletions
diff --git a/vncviewer/CConn.cxx b/vncviewer/CConn.cxx index e567939c..4a15f853 100644 --- a/vncviewer/CConn.cxx +++ b/vncviewer/CConn.cxx @@ -30,6 +30,7 @@ #include <core/LogWriter.h> #include <core/Timer.h> #include <core/string.h> +#include <core/time.h> #include <rdr/FdInStream.h> #include <rdr/FdOutStream.h> @@ -130,6 +131,8 @@ CConn::CConn(const char* vncServerName, network::Socket* socket=nullptr) CConn::~CConn() { + struct timeval now; + close(); OptionsDialog::removeCallback(handleOptions); @@ -138,6 +141,36 @@ CConn::~CConn() if (desktop) delete desktop; + sock->shutdown(); + + // Do a graceful close by waiting for the peer (up to 250 ms) + // FIXME: should do this asynchronously + gettimeofday(&now, nullptr); + while (core::msSince(&now) < 250) { + bool done; + + done = false; + while (true) { + try { + sock->inStream().skip(sock->inStream().avail()); + if (!sock->inStream().hasData(1)) + break; + } catch (std::exception&) { + done = true; + break; + } + } + + if (done) + break; + +#ifdef WIN32 + Sleep(10); +#else + usleep(10000); +#endif + } + if (sock) Fl::remove_fd(sock->getFd()); delete sock; @@ -166,11 +199,6 @@ std::string CConn::connectionInfo() infoText += core::format(_("Pixel format: %s"), pfStr); infoText += "\n"; - // TRANSLATORS: Similar to the earlier "Pixel format" string - serverPF.print(pfStr, 100); - infoText += core::format(_("(server default %s)"), pfStr); - infoText += "\n"; - infoText += core::format(_("Requested encoding: %s"), rfb::encodingName(getPreferredEncoding())); infoText += "\n"; @@ -305,17 +333,12 @@ void CConn::initDone() if (server.beforeVersion(3, 8) && autoSelect) fullColour.setParam(true); - serverPF = server.pf(); - - desktop = new DesktopWindow(server.width(), server.height(), - server.name(), serverPF, this); + desktop = new DesktopWindow(server.width(), server.height(), this); fullColourPF = desktop->getPreferredPF(); // Force a switch to the format and encoding we'd like + updateEncoding(); updatePixelFormat(); - int encNum = rfb::encodingNum(::preferredEncoding.getValueStr().c_str()); - if (encNum != -1) - setPreferredEncoding(encNum); } void CConn::setExtendedDesktopSize(unsigned reason, unsigned result, @@ -332,7 +355,7 @@ void CConn::setExtendedDesktopSize(unsigned reason, unsigned result, void CConn::setName(const char* name) { CConnection::setName(name); - desktop->setName(name); + desktop->updateCaption(); } // framebufferUpdateStart() is called at the beginning of an update. @@ -385,18 +408,15 @@ void CConn::framebufferUpdateEnd() desktop->updateWindow(); // Compute new settings based on updated bandwidth values - if (autoSelect) - autoSelectFormatAndEncoding(); + if (autoSelect) { + updateEncoding(); + updateQualityLevel(); + updatePixelFormat(); + } } // The rest of the callbacks are fairly self-explanatory... -void CConn::setColourMapEntries(int /*firstColour*/, int /*nColours*/, - uint16_t* /*rgbs*/) -{ - vlog.error(_("Invalid SetColourMapEntries from server!")); -} - void CConn::bell() { fl_beep(); @@ -420,7 +440,9 @@ bool CConn::dataRect(const core::Rect& r, int encoding) void CConn::setCursor(int width, int height, const core::Point& hotspot, const uint8_t* data) { - desktop->setCursor(width, height, hotspot, data); + CConnection::setCursor(width, height, hotspot, data); + + desktop->setCursor(); } void CConn::setCursorPos(const core::Point& pos) @@ -428,20 +450,6 @@ void CConn::setCursorPos(const core::Point& pos) desktop->setCursorPos(pos); } -void CConn::fence(uint32_t flags, unsigned len, const uint8_t data[]) -{ - CMsgHandler::fence(flags, len, data); - - if (flags & rfb::fenceFlagRequest) { - // We handle everything synchronously so we trivially honor these modes - flags = flags & (rfb::fenceFlagBlockBefore | - rfb::fenceFlagBlockAfter); - - writer()->writeFence(flags, len, data); - return; - } -} - void CConn::setLEDState(unsigned int state) { CConnection::setLEDState(state); @@ -472,44 +480,59 @@ void CConn::resizeFramebuffer() desktop->resizeFramebuffer(server.width(), server.height()); } -// autoSelectFormatAndEncoding() chooses the format and encoding appropriate -// to the connection speed: -// -// First we wait for at least one second of bandwidth measurement. -// -// Above 16Mbps (i.e. LAN), we choose the second highest JPEG quality, -// which should be perceptually lossless. -// -// If the bandwidth is below that, we choose a more lossy JPEG quality. -// -// If the bandwidth drops below 256 Kbps, we switch to palette mode. -// -// Note: The system here is fairly arbitrary and should be replaced -// with something more intelligent at the server end. -// -void CConn::autoSelectFormatAndEncoding() +void CConn::updateEncoding() +{ + int encNum; + + if (autoSelect) + encNum = rfb::encodingTight; + else + encNum = rfb::encodingNum(::preferredEncoding.getValueStr().c_str()); + + if (encNum != -1) + setPreferredEncoding(encNum); +} + +void CConn::updateCompressLevel() +{ + if (customCompressLevel) + setCompressLevel(::compressLevel); + else + setCompressLevel(-1); +} + +void CConn::updateQualityLevel() { - bool newFullColour = fullColour; - int newQualityLevel = ::qualityLevel; + int newQualityLevel; - // Always use Tight - setPreferredEncoding(rfb::encodingTight); + if (noJpeg) + newQualityLevel = -1; + else if (!autoSelect) + newQualityLevel = ::qualityLevel; + else { + // Above 16Mbps (i.e. LAN), we choose the second highest JPEG + // quality, which should be perceptually lossless. If the bandwidth + // is below that, we choose a more lossy JPEG quality. - // Select appropriate quality level - if (!noJpeg) { if (bpsEstimate > 16000000) newQualityLevel = 8; else newQualityLevel = 6; - if (newQualityLevel != ::qualityLevel) { + if (newQualityLevel != getQualityLevel()) { vlog.info(_("Throughput %d kbit/s - changing to quality %d"), (int)(bpsEstimate/1000), newQualityLevel); - ::qualityLevel.setParam(newQualityLevel); - setQualityLevel(newQualityLevel); } } + setQualityLevel(newQualityLevel); +} + +void CConn::updatePixelFormat() +{ + bool useFullColour; + rfb::PixelFormat pf; + if (server.beforeVersion(3, 8)) { // Xvnc from TightVNC 1.2.9 sends out FramebufferUpdates with // cursors "asynchronously". If this happens in the middle of a @@ -520,28 +543,23 @@ void CConn::autoSelectFormatAndEncoding() // old servers. return; } - - // Select best color level - newFullColour = (bpsEstimate > 256000); - if (newFullColour != fullColour) { - if (newFullColour) - vlog.info(_("Throughput %d kbit/s - full color is now enabled"), - (int)(bpsEstimate/1000)); - else - vlog.info(_("Throughput %d kbit/s - full color is now disabled"), - (int)(bpsEstimate/1000)); - fullColour.setParam(newFullColour); - updatePixelFormat(); - } -} -// requestNewUpdate() requests an update from the server, having set the -// format and encoding appropriately. -void CConn::updatePixelFormat() -{ - rfb::PixelFormat pf; + useFullColour = fullColour; + + // If the bandwidth drops below 256 Kbps, we switch to palette mode. + if (autoSelect) { + useFullColour = (bpsEstimate > 256000); + if (useFullColour != (server.pf() == fullColourPF)) { + if (useFullColour) + vlog.info(_("Throughput %d kbit/s - full color is now enabled"), + (int)(bpsEstimate/1000)); + else + vlog.info(_("Throughput %d kbit/s - full color is now disabled"), + (int)(bpsEstimate/1000)); + } + } - if (fullColour) { + if (useFullColour) { pf = fullColourPF; } else { if (lowColourLevel == 0) @@ -552,37 +570,21 @@ void CConn::updatePixelFormat() pf = mediumColourPF; } - char str[256]; - pf.print(str, 256); - vlog.info(_("Using pixel format %s"),str); - setPF(pf); + if (pf != server.pf()) { + char str[256]; + pf.print(str, 256); + vlog.info(_("Using pixel format %s"),str); + setPF(pf); + } } void CConn::handleOptions(void *data) { CConn *self = (CConn*)data; - // Checking all the details of the current set of encodings is just - // a pain. Assume something has changed, as resending the encoding - // list is cheap. Avoid overriding what the auto logic has selected - // though. - if (!autoSelect) { - int encNum = rfb::encodingNum(::preferredEncoding.getValueStr().c_str()); - - if (encNum != -1) - self->setPreferredEncoding(encNum); - } - - if (customCompressLevel) - self->setCompressLevel(::compressLevel); - else - self->setCompressLevel(-1); - - if (!noJpeg && !autoSelect) - self->setQualityLevel(::qualityLevel); - else - self->setQualityLevel(-1); - + self->updateEncoding(); + self->updateCompressLevel(); + self->updateQualityLevel(); self->updatePixelFormat(); } diff --git a/vncviewer/CConn.h b/vncviewer/CConn.h index 5fc2650c..b45f58d7 100644 --- a/vncviewer/CConn.h +++ b/vncviewer/CConn.h @@ -42,6 +42,8 @@ public: unsigned getPixelCount(); unsigned getPosition(); +protected: + // Callback when socket is ready (or broken) static void socketEvent(FL_SOCKET fd, void *data); @@ -63,9 +65,6 @@ public: void setName(const char* name) override; - void setColourMapEntries(int firstColour, int nColours, - uint16_t* rgbs) override; - void bell() override; void framebufferUpdateStart() override; @@ -76,9 +75,6 @@ public: const uint8_t* data) override; void setCursorPos(const core::Point& pos) override; - void fence(uint32_t flags, unsigned len, - const uint8_t data[]) override; - void setLEDState(unsigned int state) override; void handleClipboardRequest() override; @@ -89,7 +85,9 @@ private: void resizeFramebuffer() override; - void autoSelectFormatAndEncoding(); + void updateEncoding(); + void updateCompressLevel(); + void updateQualityLevel(); void updatePixelFormat(); static void handleOptions(void *data); @@ -106,7 +104,6 @@ private: unsigned updateCount; unsigned pixelCount; - rfb::PixelFormat serverPF; rfb::PixelFormat fullColourPF; int lastServerEncoding; diff --git a/vncviewer/CMakeLists.txt b/vncviewer/CMakeLists.txt index d32ad2ea..8aba3cae 100644 --- a/vncviewer/CMakeLists.txt +++ b/vncviewer/CMakeLists.txt @@ -6,13 +6,13 @@ add_executable(vncviewer fltk/Fl_Monitor_Arrangement.cxx fltk/Fl_Navigation.cxx fltk/theme.cxx - menukey.cxx BaseTouchHandler.cxx CConn.cxx DesktopWindow.cxx EmulateMB.cxx UserDialog.cxx ServerDialog.cxx + ShortcutHandler.cxx Surface.cxx OptionsDialog.cxx PlatformPixelBuffer.cxx diff --git a/vncviewer/DesktopWindow.cxx b/vncviewer/DesktopWindow.cxx index 4a15117a..831bb107 100644 --- a/vncviewer/DesktopWindow.cxx +++ b/vncviewer/DesktopWindow.cxx @@ -26,6 +26,8 @@ #include <assert.h> #include <stdio.h> #include <string.h> +#include <time.h> +#include <unistd.h> #include <sys/time.h> #include <core/LogWriter.h> @@ -73,20 +75,21 @@ static int edge_scroll_size_y = 96; // default: roughly 60 fps for smooth motion #define EDGE_SCROLL_SECONDS_PER_FRAME 0.016666 +// Time before we show an overlay tip again +const time_t OVERLAY_REPEAT_TIMEOUT = 600; + static core::LogWriter vlog("DesktopWindow"); // Global due to http://www.fltk.org/str.php?L2177 and the similar // issue for Fl::event_dispatch. static std::set<DesktopWindow *> instances; -DesktopWindow::DesktopWindow(int w, int h, const char *name, - const rfb::PixelFormat& serverPF, - CConn* cc_) - : Fl_Window(w, h), cc(cc_), offscreen(nullptr), overlay(nullptr), +DesktopWindow::DesktopWindow(int w, int h, CConn* cc_) + : Fl_Window(w, h), cc(cc_), offscreen(nullptr), firstUpdate(true), delayedFullscreen(false), sentDesktopSize(false), pendingRemoteResize(false), lastResize({0, 0}), - keyboardGrabbed(false), mouseGrabbed(false), + keyboardGrabbed(false), mouseGrabbed(false), regrabOnFocus(false), statsLastUpdates(0), statsLastPixels(0), statsLastPosition(0), statsGraph(nullptr) { @@ -97,7 +100,7 @@ DesktopWindow::DesktopWindow(int w, int h, const char *name, group->resizable(nullptr); resizable(group); - viewport = new Viewport(w, h, serverPF, cc); + viewport = new Viewport(w, h, cc); // Position will be adjusted later hscroll = new Fl_Scrollbar(0, 0, 0, 0); @@ -110,7 +113,7 @@ DesktopWindow::DesktopWindow(int w, int h, const char *name, callback(handleClose, this); - setName(name); + updateCaption(); OptionsDialog::addCallback(handleOptions, this); @@ -229,8 +232,16 @@ DesktopWindow::DesktopWindow(int w, int h, const char *name, Fl::add_timeout(0, handleStatsTimeout, this); } - // Show hint about menu key - Fl::add_timeout(0.5, menuOverlay, this); + // Show hint about menu shortcut + unsigned modifierMask; + + modifierMask = 0; + for (core::EnumListEntry key : shortcutModifiers) + modifierMask |= ShortcutHandler::parseModifier(key.getValueStr().c_str()); + + if (modifierMask) + addOverlayTip(_("Press %sM to open the context menu"), + ShortcutHandler::modifierPrefix(modifierMask)); // By default we get a slight delay when we warp the pointer, something // we don't want or we'll get jerky movement @@ -251,17 +262,18 @@ DesktopWindow::~DesktopWindow() // Unregister all timeouts in case they get a change tro trigger // again later when this object is already gone. - Fl::remove_timeout(handleGrab, this); Fl::remove_timeout(handleResizeTimeout, this); Fl::remove_timeout(handleFullscreenTimeout, this); Fl::remove_timeout(handleEdgeScroll, this); Fl::remove_timeout(handleStatsTimeout, this); - Fl::remove_timeout(menuOverlay, this); Fl::remove_timeout(updateOverlay, this); OptionsDialog::removeCallback(handleOptions); - delete overlay; + while (!overlays.empty()) { + delete overlays.front().surface; + overlays.pop_front(); + } delete offscreen; delete statsGraph; @@ -284,46 +296,50 @@ const rfb::PixelFormat &DesktopWindow::getPreferredPF() } -void DesktopWindow::setName(const char *name) +void DesktopWindow::updateCaption() { - char windowNameStr[100]; + const size_t maxLen = 100; + std::string windowName; const char *labelFormat; size_t maxNameSize; - char truncatedName[sizeof(windowNameStr)]; + std::string name; - labelFormat = "%s - TigerVNC"; + // FIXME: All of this consideres bytes, not characters + + if (keyboardGrabbed) + labelFormat = _("%s - TigerVNC (keyboard grabbed)"); + else + labelFormat = _("%s - TigerVNC"); // Ignore the length of '%s' since it is // a format marker which won't take up space - maxNameSize = sizeof(windowNameStr) - 1 - strlen(labelFormat) + 2; - - if (maxNameSize > strlen(name)) { - // Guaranteed to fit, no need to truncate - strcpy(truncatedName, name); - } else if (maxNameSize <= strlen("...")) { - // Even an ellipsis won't fit - truncatedName[0] = '\0'; - } else { - int offset; + maxNameSize = maxLen - strlen(labelFormat) + 2; + + name = cc->server.name(); - // We need to truncate, add an ellipsis - offset = maxNameSize - strlen("..."); - strncpy(truncatedName, name, sizeof(truncatedName)); - strcpy(truncatedName + offset, "..."); + if (name.size() > maxNameSize) { + if (maxNameSize <= strlen("...")) { + // Even an ellipsis won't fit + name.clear(); + } + else { + int offset; + + // We need to truncate, add an ellipsis + offset = maxNameSize - strlen("..."); + name.resize(offset); + name += "..."; + } } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" - if (snprintf(windowNameStr, sizeof(windowNameStr), labelFormat, - truncatedName) >= (int)sizeof(windowNameStr)) { - // This is just to shut up the compiler, as we've already made sure - // we won't truncate anything - } + windowName = core::format(labelFormat, name.c_str()); #pragma GCC diagnostic pop - copy_label(windowNameStr); + copy_label(windowName.c_str()); } @@ -410,11 +426,9 @@ void DesktopWindow::setDesktopSizeDone(unsigned result) } -void DesktopWindow::setCursor(int width, int height, - const core::Point& hotspot, - const uint8_t* data) +void DesktopWindow::setCursor() { - viewport->setCursor(width, height, hotspot, data); + viewport->setCursor(); } @@ -536,9 +550,12 @@ void DesktopWindow::draw() } // Overlay (if active) - if (overlay) { + if (!overlays.empty()) { int ox, oy, ow, oh; int sx, sy, sw, sh; + struct Overlay overlay; + + overlay = overlays.front(); // Make sure it's properly seen by adjusting it relative to the // primary screen rather than the entire window @@ -576,18 +593,20 @@ void DesktopWindow::draw() sw = w(); } - ox = X = sx + (sw - overlay->width()) / 2; + ox = X = sx + (sw - overlay.surface->width()) / 2; oy = Y = sy + 50; - ow = overlay->width(); - oh = overlay->height(); + ow = overlay.surface->width(); + oh = overlay.surface->height(); fl_clip_box(ox, oy, ow, oh, ox, oy, ow, oh); if ((ow != 0) && (oh != 0)) { if (offscreen) - overlay->blend(offscreen, ox - X, oy - Y, ox, oy, ow, oh, overlayAlpha); + overlay.surface->blend(offscreen, ox - X, oy - Y, + ox, oy, ow, oh, overlay.alpha); else - overlay->blend(ox - X, oy - Y, ox, oy, ow, oh, overlayAlpha); + overlay.surface->blend(ox - X, oy - Y, + ox, oy, ow, oh, overlay.alpha); } } @@ -701,34 +720,55 @@ void DesktopWindow::resize(int x, int y, int w, int h) repositionWidgets(); } - - // Some systems require a grab after the window size has been changed. - // Otherwise they might hold on to displays, resulting in them being unusable. - maybeGrabKeyboard(); } - -void DesktopWindow::menuOverlay(void* data) +void DesktopWindow::addOverlayTip(const char* text, ...) { - DesktopWindow *self; + va_list ap; + char textbuf[1024]; - self = (DesktopWindow*)data; + std::map<std::string, time_t>::iterator iter; + + va_start(ap, text); + vsnprintf(textbuf, sizeof(textbuf), text, ap); + textbuf[sizeof(textbuf)-1] = '\0'; + va_end(ap); - // Empty string means None, for backward compatibility - if ((menuKey != "") && (menuKey != "None")) { - self->setOverlay(_("Press %s to open the context menu"), - menuKey.getValueStr().c_str()); + // Purge all old entries + for (iter = overlayTimes.begin(); iter != overlayTimes.end(); ) { + if ((time(nullptr) - iter->second) >= OVERLAY_REPEAT_TIMEOUT) + overlayTimes.erase(iter++); + else + iter++; } + + // Recently shown? + if (overlayTimes.count(textbuf) > 0) + return; + + overlayTimes[textbuf] = time(nullptr); + + addOverlay(textbuf); } -void DesktopWindow::setOverlay(const char* text, ...) +void DesktopWindow::addOverlayError(const char* text, ...) { - const Fl_Fontsize fontsize = 16; - const int margin = 10; - va_list ap; char textbuf[1024]; + va_start(ap, text); + vsnprintf(textbuf, sizeof(textbuf), text, ap); + textbuf[sizeof(textbuf)-1] = '\0'; + va_end(ap); + + addOverlay(textbuf); +} + +void DesktopWindow::addOverlay(const char *text) +{ + const Fl_Fontsize fontsize = 16; + const int margin = 10; + Fl_Image_Surface *surface; Fl_RGB_Image* imageText; @@ -742,13 +782,7 @@ void DesktopWindow::setOverlay(const char* text, ...) unsigned char* a; const unsigned char* b; - delete overlay; - Fl::remove_timeout(updateOverlay, this); - - va_start(ap, text); - vsnprintf(textbuf, sizeof(textbuf), text, ap); - textbuf[sizeof(textbuf)-1] = '\0'; - va_end(ap); + struct Overlay overlay; #if !defined(WIN32) && !defined(__APPLE__) // FLTK < 1.3.5 crashes if fl_gc is unset @@ -758,7 +792,7 @@ void DesktopWindow::setOverlay(const char* text, ...) fl_font(FL_HELVETICA, fontsize); w = 0; - fl_measure(textbuf, w, h); + fl_measure(text, w, h); // Margins w += margin * 2 * 2; @@ -771,7 +805,7 @@ void DesktopWindow::setOverlay(const char* text, ...) fl_font(FL_HELVETICA, fontsize); fl_color(FL_WHITE); - fl_draw(textbuf, 0, 0, w, h, FL_ALIGN_CENTER); + fl_draw(text, 0, 0, w, h, FL_ALIGN_CENTER); imageText = surface->image(); delete surface; @@ -807,39 +841,53 @@ void DesktopWindow::setOverlay(const char* text, ...) delete imageText; - overlay = new Surface(image); - overlayAlpha = 0; - gettimeofday(&overlayStart, nullptr); + overlay.surface = new Surface(image); + overlay.alpha = 0; + memset(&overlay.start, 0, sizeof(overlay.start)); + overlays.push_back(overlay); delete image; delete [] buffer; - Fl::add_timeout(1.0/60, updateOverlay, this); + if (overlays.size() == 1) + Fl::add_timeout(0.5, updateOverlay, this); } void DesktopWindow::updateOverlay(void *data) { DesktopWindow *self; + struct Overlay* overlay; unsigned elapsed; self = (DesktopWindow*)data; - elapsed = core::msSince(&self->overlayStart); + if (self->overlays.empty()) + return; + + overlay = &self->overlays.front(); + + if (overlay->start.tv_sec == 0) + gettimeofday(&overlay->start, nullptr); + + elapsed = core::msSince(&overlay->start); if (elapsed < 500) { - self->overlayAlpha = (unsigned)255 * elapsed / 500; + overlay->alpha = (unsigned)255 * elapsed / 500; Fl::add_timeout(1.0/60, updateOverlay, self); } else if (elapsed < 3500) { - self->overlayAlpha = 255; + overlay->alpha = 255; Fl::add_timeout(3.0, updateOverlay, self); } else if (elapsed < 4000) { - self->overlayAlpha = (unsigned)255 * (4000 - elapsed) / 500; + overlay->alpha = (unsigned)255 * (4000 - elapsed) / 500; Fl::add_timeout(1.0/60, updateOverlay, self); } else { - delete self->overlay; - self->overlay = nullptr; + delete overlay->surface; + self->overlays.pop_front(); + if (!self->overlays.empty()) + Fl::add_timeout(0.5, updateOverlay, self); } + // FIXME: Only damage relevant area self->damage(FL_DAMAGE_USER1); } @@ -853,10 +901,36 @@ int DesktopWindow::handle(int event) // Update scroll bars repositionWidgets(); - if (fullscreen_active()) - maybeGrabKeyboard(); - else - ungrabKeyboard(); + // Show how to get out of full screen + if (fullscreen_active()) { + unsigned modifierMask; + + modifierMask = 0; + for (core::EnumListEntry key : shortcutModifiers) + modifierMask |= ShortcutHandler::parseModifier(key.getValueStr().c_str()); + + if (modifierMask) + addOverlayTip(_("Press %sEnter to leave full-screen mode"), + ShortcutHandler::modifierPrefix(modifierMask)); + } + +#ifdef __APPLE__ + // Complain to the user if we won't have permission to grab keyboard + if (fullscreenSystemKeys && fullscreen_active()) { + // FIXME: There is some race during initial full screen where we + // fail to give focus to the popup, but we can work around + // it using a timer + Fl::add_timeout(0, [](void*) { cocoa_is_trusted(true); }, nullptr); + } +#endif + + // Automatically toggle keyboard grab? + if (fullscreenSystemKeys) { + if (fullscreen_active()) + grabKeyboard(); + else + ungrabKeyboard(); + } // The window manager respected our full screen request, so stop // waiting and delaying the session resize @@ -925,6 +999,17 @@ int DesktopWindow::fltkDispatch(int event, Fl_Window *win) if ((event == FL_MOVE) && (win == nullptr)) return 0; +#if !defined(WIN32) && !defined(__APPLE__) + // FLTK passes through the fake grab focus events that can cause us + // to end up in an infinite loop + // https://github.com/fltk/fltk/issues/295 + if ((event == FL_FOCUS) || (event == FL_UNFOCUS)) { + const XFocusChangeEvent* xfocus = &fl_xevent->xfocus; + if ((xfocus->mode == NotifyGrab) || (xfocus->mode == NotifyUngrab)) + return 0; + } +#endif + ret = Fl::handle_(event, win); // This is hackish and the result of the dodgy focus handling in FLTK. @@ -937,16 +1022,21 @@ int DesktopWindow::fltkDispatch(int event, Fl_Window *win) if (dw) { switch (event) { // Focus might not stay with us just because we have grabbed the - // keyboard. E.g. we might have sub windows, or we're not using - // all monitors and the user clicked on another application. - // Make sure we update our grabs with the focus changes. + // keyboard. E.g. we might have sub windows, or the user clicked on + // another application. Make sure we update our grabs with the focus + // changes. case FL_FOCUS: - dw->maybeGrabKeyboard(); + if (dw->regrabOnFocus || + (fullscreenSystemKeys && dw->fullscreen_active())) + dw->grabKeyboard(); + dw->regrabOnFocus = false; break; case FL_UNFOCUS: - if (fullscreenSystemKeys) { - dw->ungrabKeyboard(); - } + // If the grab is active when we lose focus, the user likely wants + // the grab to remain once we regain focus + if (dw->keyboardGrabbed) + dw->regrabOnFocus = true; + dw->ungrabKeyboard(); break; case FL_SHOW: @@ -987,14 +1077,6 @@ int DesktopWindow::fltkHandle(int event) // not be resized to cover the new screen. A timer makes sense // also on other systems, to make sure that whatever desktop // environment has a chance to deal with things before we do. - // Please note that when using FullscreenSystemKeys on macOS, the - // display configuration cannot be changed: macOS will not detect - // added or removed screens and there will be no - // FL_SCREEN_CONFIGURATION_CHANGED event. This is by design: - // "When you capture a display, you have exclusive use of the - // display. Other applications and system services are not allowed - // to use the display or change its configuration. In addition, - // they are not notified of display changes" Fl::remove_timeout(reconfigureFullscreen); Fl::add_timeout(0.5, reconfigureFullscreen); } @@ -1070,43 +1152,13 @@ void DesktopWindow::fullscreen_on() } } -#ifdef __APPLE__ - // This is a workaround for a bug in FLTK, see: https://github.com/fltk/fltk/pull/277 - int savedLevel = -1; - if (shown()) - savedLevel = cocoa_get_level(this); -#endif + fullscreen_screens(top, bottom, left, right); -#ifdef __APPLE__ - // This is a workaround for a bug in FLTK, see: https://github.com/fltk/fltk/pull/277 - if (savedLevel != -1) { - if (cocoa_get_level(this) != savedLevel) - cocoa_set_level(this, savedLevel); - } -#endif if (!fullscreen_active()) fullscreen(); } -#if !defined(WIN32) && !defined(__APPLE__) -Bool eventIsFocusWithSerial(Display* /*display*/, XEvent *event, - XPointer arg) -{ - unsigned long serial; - - serial = *(unsigned long*)arg; - - if (event->xany.serial != serial) - return False; - - if ((event->type != FocusIn) && (event->type != FocusOut)) - return False; - - return True; -} -#endif - bool DesktopWindow::hasFocus() { Fl_Widget* focus; @@ -1121,66 +1173,66 @@ bool DesktopWindow::hasFocus() return focus->window() == this; } -void DesktopWindow::maybeGrabKeyboard() -{ - if (fullscreenSystemKeys && fullscreen_active() && hasFocus()) - grabKeyboard(); -} - void DesktopWindow::grabKeyboard() { + unsigned modifierMask; + // Grabbing the keyboard is fairly safe as FLTK reroutes events to the // correct widget regardless of which low level window got the system // event. // FIXME: Push this stuff into FLTK. + if (keyboardGrabbed) + return; + + if (!hasFocus()) + return; + #if defined(WIN32) int ret; ret = win32_enable_lowlevel_keyboard(fl_xid(this)); if (ret != 0) { - vlog.error(_("Failure grabbing keyboard")); + vlog.error(_("Failure grabbing control of the keyboard")); + addOverlayError(_("Failure grabbing control of the keyboard")); return; } #elif defined(__APPLE__) - int ret; - - ret = cocoa_capture_displays(this); - if (ret != 0) { - vlog.error(_("Failure grabbing keyboard")); + bool ret; + + ret = cocoa_tap_keyboard(); + if (!ret) { + vlog.error(_("Failure grabbing control of the keyboard")); + addOverlayError(_("Failure grabbing control of the keyboard")); return; } #else int ret; - XEvent xev; - unsigned long serial; - - serial = XNextRequest(fl_display); - ret = XGrabKeyboard(fl_display, fl_xid(this), True, GrabModeAsync, GrabModeAsync, CurrentTime); if (ret) { if (ret == AlreadyGrabbed) { - // It seems like we can race with the WM in some cases. - // Try again in a bit. - if (!Fl::has_timeout(handleGrab, this)) - Fl::add_timeout(0.500, handleGrab, this); - } else { - vlog.error(_("Failure grabbing keyboard")); + // It seems like we can race with the WM in some cases, e.g. when + // the WM holds the keyboard as part of handling Alt+Tab. + // Repeat the request a few times and see if we get it... + for (int attempt = 0; attempt < 5; attempt++) { + usleep(100000); + // Also throttle based on how busy the X server is + XSync(fl_display, False); + ret = XGrabKeyboard(fl_display, fl_xid(this), True, + GrabModeAsync, GrabModeAsync, CurrentTime); + if (ret != AlreadyGrabbed) + break; + } } - return; - } - // Xorg 1.20+ generates FocusIn/FocusOut even when there is no actual - // change of focus. This causes us to get stuck in an endless loop - // grabbing and ungrabbing the keyboard. Avoid this by filtering out - // any focus events generated by XGrabKeyboard(). - XSync(fl_display, False); - while (XCheckIfEvent(fl_display, &xev, &eventIsFocusWithSerial, - (XPointer)&serial) == True) { - vlog.debug("Ignored synthetic focus event cause by grab change"); + if (ret) { + vlog.error(_("Failure grabbing control of the keyboard")); + addOverlayError(_("Failure grabbing control of the keyboard")); + return; + } } #endif @@ -1188,39 +1240,37 @@ void DesktopWindow::grabKeyboard() if (contains(Fl::belowmouse())) grabPointer(); + + updateCaption(); + + modifierMask = 0; + for (core::EnumListEntry key : shortcutModifiers) + modifierMask |= ShortcutHandler::parseModifier(key.getValueStr().c_str()); + + if (modifierMask) + addOverlayTip(_("Press %s to release keyboard control from the session"), + ShortcutHandler::modifierPrefix(modifierMask, true)); } void DesktopWindow::ungrabKeyboard() { - Fl::remove_timeout(handleGrab, this); - keyboardGrabbed = false; ungrabPointer(); + updateCaption(); + #if defined(WIN32) win32_disable_lowlevel_keyboard(fl_xid(this)); #elif defined(__APPLE__) - cocoa_release_displays(this); + cocoa_untap_keyboard(); #else // FLTK has a grab so lets not mess with it if (Fl::grab()) return; - XEvent xev; - unsigned long serial; - - serial = XNextRequest(fl_display); - XUngrabKeyboard(fl_display, CurrentTime); - - // See grabKeyboard() - XSync(fl_display, False); - while (XCheckIfEvent(fl_display, &xev, &eventIsFocusWithSerial, - (XPointer)&serial) == True) { - vlog.debug("Ignored synthetic focus event cause by grab change"); - } #endif } @@ -1251,16 +1301,6 @@ void DesktopWindow::ungrabPointer() } -void DesktopWindow::handleGrab(void *data) -{ - DesktopWindow *self = (DesktopWindow*)data; - - assert(self); - - self->maybeGrabKeyboard(); -} - - #define _NET_WM_STATE_ADD 1 /* add/set property */ void DesktopWindow::maximizeWindow() { @@ -1584,11 +1624,6 @@ void DesktopWindow::handleOptions(void *data) { DesktopWindow *self = (DesktopWindow*)data; - if (fullscreenSystemKeys) - self->maybeGrabKeyboard(); - else - self->ungrabKeyboard(); - // Call fullscreen_on even if active since it handles // fullScreenMode if (fullScreen) diff --git a/vncviewer/DesktopWindow.h b/vncviewer/DesktopWindow.h index 69729ff8..ca4cf53a 100644 --- a/vncviewer/DesktopWindow.h +++ b/vncviewer/DesktopWindow.h @@ -1,5 +1,5 @@ /* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. - * Copyright 2011 Pierre Ossman <ossman@cendio.se> for Cendio AB + * Copyright 2011-2025 Pierre Ossman <ossman@cendio.se> 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 @@ -20,7 +20,9 @@ #ifndef __DESKTOPWINDOW_H__ #define __DESKTOPWINDOW_H__ +#include <list> #include <map> +#include <string> #include <sys/time.h> @@ -37,8 +39,7 @@ class Fl_Scrollbar; class DesktopWindow : public Fl_Window { public: - DesktopWindow(int w, int h, const char *name, - const rfb::PixelFormat& serverPF, CConn* cc_); + DesktopWindow(int w, int h, CConn* cc_); ~DesktopWindow(); // Most efficient format (from DesktopWindow's point of view) @@ -48,7 +49,7 @@ public: void updateWindow(); // Updated session title - void setName(const char *name); + void updateCaption(); // Resize the current framebuffer, but retain the contents void resizeFramebuffer(int new_w, int new_h); @@ -57,8 +58,7 @@ public: void setDesktopSizeDone(unsigned result); // New image for the locally rendered cursor - void setCursor(int width, int height, const core::Point& hotspot, - const uint8_t* data); + void setCursor(); // Server-provided cursor position void setCursorPos(const core::Point& pos); @@ -80,11 +80,16 @@ public: void fullscreen_on(); -private: - static void menuOverlay(void *data); + // Grab keyboard events from desktop environment + void grabKeyboard(); + void ungrabKeyboard(); - void setOverlay(const char *text, ...) +private: + void addOverlayTip(const char *text, ...) + __attribute__((__format__ (__printf__, 2, 3))); + void addOverlayError(const char *text, ...) __attribute__((__format__ (__printf__, 2, 3))); + void addOverlay(const char *text); static void updateOverlay(void *data); static int fltkDispatch(int event, Fl_Window *win); @@ -92,14 +97,9 @@ private: bool hasFocus(); - void maybeGrabKeyboard(); - void grabKeyboard(); - void ungrabKeyboard(); void grabPointer(); void ungrabPointer(); - static void handleGrab(void *data); - void maximizeWindow(); static void handleResizeTimeout(void *data); @@ -125,9 +125,15 @@ private: Fl_Scrollbar *hscroll, *vscroll; Viewport *viewport; Surface *offscreen; - Surface *overlay; - unsigned char overlayAlpha; - struct timeval overlayStart; + + struct Overlay { + Surface *surface; + unsigned char alpha; + struct timeval start; + }; + + std::list<Overlay> overlays; + std::map<std::string, time_t> overlayTimes; bool firstUpdate; bool delayedFullscreen; @@ -139,6 +145,8 @@ private: bool keyboardGrabbed; bool mouseGrabbed; + bool regrabOnFocus; + struct statsEntry { unsigned ups; unsigned pps; diff --git a/vncviewer/Keyboard.h b/vncviewer/Keyboard.h index 81360252..aeab4e71 100644 --- a/vncviewer/Keyboard.h +++ b/vncviewer/Keyboard.h @@ -21,6 +21,8 @@ #include <stdint.h> +#include <list> + class KeyboardHandler { public: @@ -35,7 +37,10 @@ public: Keyboard(KeyboardHandler* handler_) : handler(handler_) {}; virtual ~Keyboard() {}; + virtual bool isKeyboardReset(const void* event) { (void)event; return false; } + virtual bool handleEvent(const void* event) = 0; + virtual std::list<uint32_t> translateToKeySyms(int systemKeyCode) = 0; virtual void reset() {}; diff --git a/vncviewer/KeyboardMacOS.h b/vncviewer/KeyboardMacOS.h index 0901664b..033c8539 100644 --- a/vncviewer/KeyboardMacOS.h +++ b/vncviewer/KeyboardMacOS.h @@ -23,10 +23,8 @@ #ifdef __OBJC__ @class NSEvent; -@class NSString; #else class NSEvent; -class NSString; #endif class KeyboardMacOS : public Keyboard @@ -35,22 +33,21 @@ public: KeyboardMacOS(KeyboardHandler* handler); virtual ~KeyboardMacOS(); + bool isKeyboardReset(const void* event) override; + bool handleEvent(const void* event) override; + std::list<uint32_t> translateToKeySyms(int systemKeyCode) override; unsigned getLEDState() override; void setLEDState(unsigned state) override; - // Special helper on macOS - static bool isKeyboardSync(const void* event); - protected: bool isKeyboardEvent(const NSEvent* nsevent); bool isKeyPress(const NSEvent* nsevent); uint32_t translateSystemKeyCode(int systemKeyCode); unsigned getSystemKeyCode(const NSEvent* nsevent); - NSString* keyTranslate(unsigned keyCode, unsigned modifierFlags); - uint32_t translateEventKeysym(const NSEvent* nsevent); + uint32_t translateToKeySym(unsigned keyCode, unsigned modifierFlags); int openHID(unsigned int* ioc); int getModifierLockState(int modifier, bool* on); diff --git a/vncviewer/KeyboardMacOS.mm b/vncviewer/KeyboardMacOS.mm index 9ef2fc93..599612ec 100644 --- a/vncviewer/KeyboardMacOS.mm +++ b/vncviewer/KeyboardMacOS.mm @@ -22,6 +22,8 @@ #include <assert.h> +#include <algorithm> + #import <Cocoa/Cocoa.h> #import <Carbon/Carbon.h> @@ -54,7 +56,7 @@ extern const unsigned int code_map_osx_to_qnum_len; static core::LogWriter vlog("KeyboardMacOS"); -static const int kvk_map[][2] = { +static const unsigned kvk_map[][2] = { { kVK_Return, XK_Return }, { kVK_Tab, XK_Tab }, { kVK_Space, XK_space }, @@ -140,6 +142,25 @@ KeyboardMacOS::~KeyboardMacOS() { } +bool KeyboardMacOS::isKeyboardReset(const void* event) +{ + const NSEvent* nsevent = (const NSEvent*)event; + + assert(event); + + // If we get a NSFlagsChanged event with key code 0 then this isn't + // an actual keyboard event but rather the system trying to sync up + // modifier state after it has stolen input for some reason (e.g. + // Cmd+Tab) + + if ([nsevent type] != NSFlagsChanged) + return false; + if ([nsevent keyCode] != 0) + return false; + + return true; +} + bool KeyboardMacOS::handleEvent(const void* event) { const NSEvent* nsevent = (NSEvent*)event; @@ -155,10 +176,25 @@ bool KeyboardMacOS::handleEvent(const void* event) if (isKeyPress(nsevent)) { uint32_t keyCode; uint32_t keySym; + unsigned modifiers; keyCode = translateSystemKeyCode(systemKeyCode); - keySym = translateEventKeysym(nsevent); + // We want a "normal" symbol out of the event, which basically means + // we only respect the shift and alt/altgr modifiers. Cocoa can help + // us if we only wanted shift, but as we also want alt/altgr, we'll + // have to do some lookup ourselves. This matches our behaviour on + // other platforms. + + modifiers = 0; + if ([nsevent modifierFlags] & NSAlphaShiftKeyMask) + modifiers |= alphaLock; + if ([nsevent modifierFlags] & NSShiftKeyMask) + modifiers |= shiftKey; + if ([nsevent modifierFlags] & NSAlternateKeyMask) + modifiers |= optionKey; + + keySym = translateToKeySym([nsevent keyCode], modifiers); if (keySym == NoSymbol) { vlog.error(_("No symbol for key code 0x%02x (in the current state)"), systemKeyCode); @@ -177,6 +213,51 @@ bool KeyboardMacOS::handleEvent(const void* event) return true; } +std::list<uint32_t> KeyboardMacOS::translateToKeySyms(int systemKeyCode) +{ + std::list<uint32_t> keySyms; + unsigned mods; + + uint32_t ks; + + // Start with no modifiers + ks = translateToKeySym(systemKeyCode, 0); + if (ks != NoSymbol) + keySyms.push_back(ks); + + // Next just a single modifier at a time + for (mods = cmdKey; mods <= controlKey; mods <<= 1) { + std::list<uint32_t>::const_iterator iter; + + ks = translateToKeySym(systemKeyCode, mods); + if (ks == NoSymbol) + continue; + + iter = std::find(keySyms.begin(), keySyms.end(), ks); + if (iter != keySyms.end()) + continue; + + keySyms.push_back(ks); + } + + // Finally everything + for (mods = cmdKey; mods < (controlKey << 1); mods += cmdKey) { + std::list<uint32_t>::const_iterator iter; + + ks = translateToKeySym(systemKeyCode, mods); + if (ks == NoSymbol) + continue; + + iter = std::find(keySyms.begin(), keySyms.end(), ks); + if (iter != keySyms.end()) + continue; + + keySyms.push_back(ks); + } + + return keySyms; +} + unsigned KeyboardMacOS::getLEDState() { unsigned state; @@ -225,25 +306,6 @@ void KeyboardMacOS::setLEDState(unsigned state) // No support for Scroll Lock // } -bool KeyboardMacOS::isKeyboardSync(const void* event) -{ - const NSEvent* nsevent = (const NSEvent*)event; - - assert(event); - - // If we get a NSFlagsChanged event with key code 0 then this isn't - // an actual keyboard event but rather the system trying to sync up - // modifier state after it has stolen input for some reason (e.g. - // Cmd+Tab) - - if ([nsevent type] != NSFlagsChanged) - return false; - if ([nsevent keyCode] != 0) - return false; - - return true; -} - bool KeyboardMacOS::isKeyboardEvent(const NSEvent* nsevent) { switch ([nsevent type]) { @@ -251,7 +313,7 @@ bool KeyboardMacOS::isKeyboardEvent(const NSEvent* nsevent) case NSKeyUp: return true; case NSFlagsChanged: - if (isKeyboardSync(nsevent)) + if (isKeyboardReset(nsevent)) return false; return true; default: @@ -339,30 +401,35 @@ uint32_t KeyboardMacOS::translateSystemKeyCode(int systemKeyCode) return code_map_osx_to_qnum[systemKeyCode]; } -NSString* KeyboardMacOS::keyTranslate(unsigned keyCode, - unsigned modifierFlags) +uint32_t KeyboardMacOS::translateToKeySym(unsigned keyCode, + unsigned modifierFlags) { const UCKeyboardLayout *layout; OSStatus err; - layout = nullptr; - TISInputSourceRef keyboard; CFDataRef uchr; + UInt32 dead_state; + UniCharCount max_len, actual_len; + UniChar string[255]; + + // Start with keys that either don't generate a symbol, or + // generate the same symbol as some other key. + for (size_t i = 0;i < sizeof(kvk_map)/sizeof(kvk_map[0]);i++) { + if (keyCode == kvk_map[i][0]) + return kvk_map[i][1]; + } + keyboard = TISCopyCurrentKeyboardLayoutInputSource(); uchr = (CFDataRef)TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); if (uchr == nullptr) - return nil; + return NoSymbol; layout = (const UCKeyboardLayout*)CFDataGetBytePtr(uchr); if (layout == nullptr) - return nil; - - UInt32 dead_state; - UniCharCount max_len, actual_len; - UniChar string[255]; + return NoSymbol; dead_state = 0; max_len = sizeof(string)/sizeof(*string); @@ -373,10 +440,12 @@ NSString* KeyboardMacOS::keyTranslate(unsigned keyCode, LMGetKbdType(), 0, &dead_state, max_len, &actual_len, string); if (err != noErr) - return nil; + return NoSymbol; // Dead key? if (dead_state != 0) { + unsigned combining; + // We have no fool proof way of asking what dead key this is. // Assume we get a spacing equivalent if we press the // same key again, and try to deduce something from that. @@ -384,34 +453,28 @@ NSString* KeyboardMacOS::keyTranslate(unsigned keyCode, LMGetKbdType(), 0, &dead_state, max_len, &actual_len, string); if (err != noErr) - return nil; - } - - return [NSString stringWithCharacters:string length:actual_len]; -} - -uint32_t KeyboardMacOS::translateEventKeysym(const NSEvent* nsevent) -{ - UInt16 key_code; - size_t i; + return NoSymbol; - NSString *chars; - UInt32 modifiers; + // FIXME: Some dead keys are given as NBSP + combining character + if (actual_len != 1) + return NoSymbol; - key_code = [nsevent keyCode]; + combining = ucs2combining(string[0]); + if (combining == (unsigned)-1) + return NoSymbol; - // Start with keys that either don't generate a symbol, or - // generate the same symbol as some other key. - for (i = 0;i < sizeof(kvk_map)/sizeof(kvk_map[0]);i++) { - if (key_code == kvk_map[i][0]) - return kvk_map[i][1]; + return ucs2keysym(combining); } + // Sanity check + if (actual_len != 1) + return NoSymbol; + // OS X always sends the same key code for the decimal key on the // num pad, but X11 wants different keysyms depending on if it should // be a comma or full stop. - if (key_code == 0x41) { - switch ([[nsevent charactersIgnoringModifiers] UTF8String][0]) { + if (keyCode == 0x41) { + switch (string[0]) { case ',': return XK_KP_Separator; case '.': @@ -421,33 +484,7 @@ uint32_t KeyboardMacOS::translateEventKeysym(const NSEvent* nsevent) } } - // We want a "normal" symbol out of the event, which basically means - // we only respect the shift and alt/altgr modifiers. Cocoa can help - // us if we only wanted shift, but as we also want alt/altgr, we'll - // have to do some lookup ourselves. This matches our behaviour on - // other platforms. - - modifiers = 0; - if ([nsevent modifierFlags] & NSAlphaShiftKeyMask) - modifiers |= alphaLock; - if ([nsevent modifierFlags] & NSShiftKeyMask) - modifiers |= shiftKey; - if ([nsevent modifierFlags] & NSAlternateKeyMask) - modifiers |= optionKey; - - chars = keyTranslate(key_code, modifiers); - if (chars == nil) - return NoSymbol; - - // FIXME: Some dead keys are given as NBSP + combining character - if ([chars length] != 1) - return NoSymbol; - - // Dead key? - if ([[nsevent characters] length] == 0) - return ucs2keysym(ucs2combining([chars characterAtIndex:0])); - - return ucs2keysym([chars characterAtIndex:0]); + return ucs2keysym(string[0]); } int KeyboardMacOS::openHID(unsigned int* ioc) diff --git a/vncviewer/KeyboardWin32.cxx b/vncviewer/KeyboardWin32.cxx index 76286217..095927f1 100644 --- a/vncviewer/KeyboardWin32.cxx +++ b/vncviewer/KeyboardWin32.cxx @@ -24,6 +24,8 @@ #include <assert.h> +#include <algorithm> + // Missing in at least some versions of MinGW #ifndef MAPVK_VK_TO_CHAR #define MAPVK_VK_TO_CHAR 2 @@ -139,6 +141,8 @@ static const UINT vkey_map[][3] = { { VK_MEDIA_STOP, NoSymbol, XF86XK_AudioStop }, { VK_MEDIA_PLAY_PAUSE, NoSymbol, XF86XK_AudioPlay }, { VK_LAUNCH_MAIL, NoSymbol, XF86XK_Mail }, + { VK_LAUNCH_MEDIA_SELECT, NoSymbol, XF86XK_AudioMedia }, + { VK_LAUNCH_APP1, NoSymbol, XF86XK_MyComputer }, { VK_LAUNCH_APP2, NoSymbol, XF86XK_Calculator }, }; @@ -203,6 +207,7 @@ bool KeyboardWin32::handleEvent(const void* event) bool isExtended; int systemKeyCode, keyCode; uint32_t keySym; + BYTE state[256]; vKey = msg->wParam; isExtended = (msg->lParam & (1 << 24)) != 0; @@ -257,7 +262,23 @@ bool KeyboardWin32::handleEvent(const void* event) keyCode = translateSystemKeyCode(systemKeyCode); - keySym = translateVKey(vKey, isExtended); + GetKeyboardState(state); + + // Pressing Ctrl wreaks havoc with the symbol lookup, so turn + // that off. But AltGr shows up as Ctrl+Alt in Windows, so keep + // Ctrl if Alt is active. + if (!(state[VK_LCONTROL] & 0x80) || !(state[VK_RMENU] & 0x80)) + state[VK_CONTROL] = state[VK_LCONTROL] = state[VK_RCONTROL] = 0; + + keySym = translateVKey(vKey, isExtended, state); + + if (keySym == NoSymbol) { + // Most Ctrl+Alt combinations will fail to produce a symbol, so + // try it again with Ctrl unconditionally disabled. + state[VK_CONTROL] = state[VK_LCONTROL] = state[VK_RCONTROL] = 0; + keySym = translateVKey(vKey, isExtended, state); + } + if (keySym == NoSymbol) { if (isExtended) vlog.error(_("No symbol for extended virtual key 0x%02x"), (int)vKey); @@ -355,6 +376,114 @@ bool KeyboardWin32::handleEvent(const void* event) return false; } +std::list<uint32_t> KeyboardWin32::translateToKeySyms(int systemKeyCode) +{ + unsigned vkey; + bool extended; + + std::list<uint32_t> keySyms; + unsigned mods; + + BYTE state[256]; + + uint32_t ks; + + UINT ch; + + extended = systemKeyCode & 0x80; + if (extended) + systemKeyCode = 0xe0 | (systemKeyCode & 0x7f); + + vkey = MapVirtualKey(systemKeyCode, MAPVK_VSC_TO_VK_EX); + if (vkey == 0) + return keySyms; + + // Start with no modifiers + memset(state, 0, sizeof(state)); + ks = translateVKey(vkey, extended, state); + if (ks != NoSymbol) + keySyms.push_back(ks); + + // Next just a single modifier at a time + for (mods = 1; mods < 16; mods <<= 1) { + std::list<uint32_t>::const_iterator iter; + + memset(state, 0, sizeof(state)); + if (mods & 0x1) + state[VK_CONTROL] = state[VK_LCONTROL] = 0x80; + if (mods & 0x2) + state[VK_SHIFT] = state[VK_LSHIFT] = 0x80; + if (mods & 0x4) + state[VK_MENU] = state[VK_LMENU] = 0x80; + if (mods & 0x8) { + state[VK_CONTROL] = state[VK_LCONTROL] = 0x80; + state[VK_MENU] = state[VK_RMENU] = 0x80; + } + + ks = translateVKey(vkey, extended, state); + if (ks == NoSymbol) + continue; + + iter = std::find(keySyms.begin(), keySyms.end(), ks); + if (iter != keySyms.end()) + continue; + + keySyms.push_back(ks); + } + + // Finally everything + for (mods = 0; mods < 16; mods++) { + std::list<uint32_t>::const_iterator iter; + + memset(state, 0, sizeof(state)); + if (mods & 0x1) + state[VK_CONTROL] = state[VK_LCONTROL] = 0x80; + if (mods & 0x2) + state[VK_SHIFT] = state[VK_LSHIFT] = 0x80; + if (mods & 0x4) + state[VK_MENU] = state[VK_LMENU] = 0x80; + if (mods & 0x8) { + state[VK_CONTROL] = state[VK_LCONTROL] = 0x80; + state[VK_MENU] = state[VK_RMENU] = 0x80; + } + + ks = translateVKey(vkey, extended, state); + if (ks == NoSymbol) + continue; + + iter = std::find(keySyms.begin(), keySyms.end(), ks); + if (iter != keySyms.end()) + continue; + + keySyms.push_back(ks); + } + + // As a final resort we use MapVirtualKey() as that gives us a Latin + // character even on non-Latin keyboards, which is useful for + // shortcuts + // + // FIXME: Can this give us anything but ASCII? + + ch = MapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR); + if (ch != 0) { + if (ch & 0x80000000) + ch = ucs2combining(ch & 0xffff); + else + ch = ch & 0xffff; + + ks = ucs2keysym(ch); + if (ks != NoSymbol) { + std::list<uint32_t>::const_iterator iter; + + iter = std::find(keySyms.begin(), keySyms.end(), ks); + if (iter == keySyms.end()) + keySyms.push_back(ks); + } + } + + return keySyms; +} + void KeyboardWin32::reset() { altGrArmed = false; @@ -466,12 +595,12 @@ uint32_t KeyboardWin32::lookupVKeyMap(unsigned vkey, bool extended, return NoSymbol; } -uint32_t KeyboardWin32::translateVKey(unsigned vkey, bool extended) +uint32_t KeyboardWin32::translateVKey(unsigned vkey, bool extended, + const unsigned char state[256]) { HKL layout; WORD lang, primary_lang; - BYTE state[256]; int ret; WCHAR wstr[10]; @@ -525,25 +654,10 @@ uint32_t KeyboardWin32::translateVKey(unsigned vkey, bool extended) // does what we want though. Unfortunately it keeps state, so // we have to be careful around dead characters. - GetKeyboardState(state); - - // Pressing Ctrl wreaks havoc with the symbol lookup, so turn - // that off. But AltGr shows up as Ctrl+Alt in Windows, so keep - // Ctrl if Alt is active. - if (!(state[VK_LCONTROL] & 0x80) || !(state[VK_RMENU] & 0x80)) - state[VK_CONTROL] = state[VK_LCONTROL] = state[VK_RCONTROL] = 0; - // FIXME: Multi character results, like U+0644 U+0627 // on Arabic layout ret = ToUnicode(vkey, 0, state, wstr, sizeof(wstr)/sizeof(wstr[0]), 0); - if (ret == 0) { - // Most Ctrl+Alt combinations will fail to produce a symbol, so - // try it again with Ctrl unconditionally disabled. - state[VK_CONTROL] = state[VK_LCONTROL] = state[VK_RCONTROL] = 0; - ret = ToUnicode(vkey, 0, state, wstr, sizeof(wstr)/sizeof(wstr[0]), 0); - } - if (ret == 1) return ucs2keysym(wstr[0]); diff --git a/vncviewer/KeyboardWin32.h b/vncviewer/KeyboardWin32.h index 336fe6da..ecab9268 100644 --- a/vncviewer/KeyboardWin32.h +++ b/vncviewer/KeyboardWin32.h @@ -28,6 +28,7 @@ public: virtual ~KeyboardWin32(); bool handleEvent(const void* event) override; + std::list<uint32_t> translateToKeySyms(int systemKeyCode) override; void reset() override; @@ -38,7 +39,8 @@ protected: uint32_t translateSystemKeyCode(int systemKeyCode); uint32_t lookupVKeyMap(unsigned vkey, bool extended, const UINT map[][3], size_t size); - uint32_t translateVKey(unsigned vkey, bool extended); + uint32_t translateVKey(unsigned vkey, bool extended, + const unsigned char state[256]); bool hasAltGr(); static void handleAltGrTimeout(void *data); diff --git a/vncviewer/KeyboardX11.cxx b/vncviewer/KeyboardX11.cxx index 973278fc..587d9fc5 100644 --- a/vncviewer/KeyboardX11.cxx +++ b/vncviewer/KeyboardX11.cxx @@ -22,6 +22,7 @@ #include <assert.h> +#include <algorithm> #include <stdexcept> #include <X11/XKBlib.h> @@ -87,6 +88,63 @@ KeyboardX11::~KeyboardX11() { } +struct GrabInfo { + Window window; + bool found; +}; + +static Bool is_same_window(Display*, XEvent* event, XPointer arg) +{ + GrabInfo* info = (GrabInfo*)arg; + + assert(info); + + // Focus is returned to our window + if ((event->type == FocusIn) && + (event->xfocus.window == info->window)) { + info->found = true; + } + + // Focus got stolen yet again + if ((event->type == FocusOut) && + (event->xfocus.window == info->window)) { + info->found = false; + } + + return False; +} + +bool KeyboardX11::isKeyboardReset(const void* event) +{ + const XEvent* xevent = (const XEvent*)event; + + assert(event); + + if (xevent->type == FocusOut) { + if (xevent->xfocus.mode == NotifyGrab) { + GrabInfo info; + XEvent dummy; + + // Something grabbed the keyboard, but we don't know if it was to + // ourselves or someone else + + // Make sure we have all the queued events from the X server + XSync(fl_display, False); + + // Check if we'll get the focus back right away + info.window = xevent->xfocus.window; + info.found = false; + XCheckIfEvent(fl_display, &dummy, is_same_window, (XPointer)&info); + if (info.found) + return false; + + return true; + } + } + + return false; +} + bool KeyboardX11::handleEvent(const void* event) { const XEvent *xevent = (const XEvent*)event; @@ -98,6 +156,10 @@ bool KeyboardX11::handleEvent(const void* event) char str; KeySym keysym; + // FLTK likes to use this instead of CurrentTime, so we need to keep + // it updated now that we steal this event + fl_event_time = xevent->xkey.time; + keycode = code_map_keycode_to_qnum[xevent->xkey.keycode]; XLookupString((XKeyEvent*)&xevent->xkey, &str, 1, &keysym, nullptr); @@ -109,6 +171,7 @@ bool KeyboardX11::handleEvent(const void* event) handler->handleKeyPress(xevent->xkey.keycode, keycode, keysym); return true; } else if (xevent->type == KeyRelease) { + fl_event_time = xevent->xkey.time; handler->handleKeyRelease(xevent->xkey.keycode); return true; } @@ -116,6 +179,31 @@ bool KeyboardX11::handleEvent(const void* event) return false; } +std::list<uint32_t> KeyboardX11::translateToKeySyms(int systemKeyCode) +{ + Status status; + XkbStateRec state; + std::list<uint32_t> keySyms; + unsigned char group; + + status = XkbGetState(fl_display, XkbUseCoreKbd, &state); + if (status != Success) + return keySyms; + + // Start with the currently used group + translateToKeySyms(systemKeyCode, state.group, &keySyms); + + // Then all other groups + for (group = 0; group < XkbNumKbdGroups; group++) { + if (group == state.group) + continue; + + translateToKeySyms(systemKeyCode, group, &keySyms); + } + + return keySyms; +} + unsigned KeyboardX11::getLEDState() { unsigned state; @@ -219,3 +307,40 @@ out: return mask; } + +void KeyboardX11::translateToKeySyms(int systemKeyCode, + unsigned char group, + std::list<uint32_t>* keySyms) +{ + unsigned int mods; + + // Start with no modifiers + translateToKeySyms(systemKeyCode, group, 0, keySyms); + + // Next just a single modifier at a time + for (mods = 1; mods < (Mod5Mask+1); mods <<= 1) + translateToKeySyms(systemKeyCode, group, mods, keySyms); + + // Finally everything + for (mods = 0; mods < (Mod5Mask<<1); mods++) + translateToKeySyms(systemKeyCode, group, mods, keySyms); +} + +void KeyboardX11::translateToKeySyms(int systemKeyCode, + unsigned char group, + unsigned char mods, + std::list<uint32_t>* keySyms) +{ + KeySym ks; + std::list<uint32_t>::const_iterator iter; + + ks = XkbKeycodeToKeysym(fl_display, systemKeyCode, group, mods); + if (ks == NoSymbol) + return; + + iter = std::find(keySyms->begin(), keySyms->end(), ks); + if (iter != keySyms->end()) + return; + + keySyms->push_back(ks); +} diff --git a/vncviewer/KeyboardX11.h b/vncviewer/KeyboardX11.h index ba9a88f9..b3b8d0a0 100644 --- a/vncviewer/KeyboardX11.h +++ b/vncviewer/KeyboardX11.h @@ -27,7 +27,10 @@ public: KeyboardX11(KeyboardHandler* handler); virtual ~KeyboardX11(); + bool isKeyboardReset(const void* event) override; + bool handleEvent(const void* event) override; + std::list<uint32_t> translateToKeySyms(int systemKeyCode) override; unsigned getLEDState() override; void setLEDState(unsigned state) override; @@ -36,6 +39,13 @@ protected: unsigned getModifierMask(uint32_t keysym); private: + void translateToKeySyms(int systemKeyCode, unsigned char group, + std::list<uint32_t>* keySyms); + void translateToKeySyms(int systemKeyCode, + unsigned char group, unsigned char mods, + std::list<uint32_t>* keySyms); + +private: int code_map_keycode_to_qnum[256]; }; diff --git a/vncviewer/OptionsDialog.cxx b/vncviewer/OptionsDialog.cxx index 9ff3285c..3ba6fba1 100644 --- a/vncviewer/OptionsDialog.cxx +++ b/vncviewer/OptionsDialog.cxx @@ -1,4 +1,4 @@ -/* Copyright 2011-2021 Pierre Ossman <ossman@cendio.se> for Cendio AB +/* Copyright 2011-2025 Pierre Ossman <ossman@cendio.se> 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 @@ -24,6 +24,8 @@ #include <stdlib.h> #include <list> +#include <core/string.h> + #include <rfb/encodings.h> #if defined(HAVE_GNUTLS) || defined(HAVE_NETTLE) @@ -35,8 +37,8 @@ #endif #include "OptionsDialog.h" +#include "ShortcutHandler.h" #include "i18n.h" -#include "menukey.h" #include "parameters.h" #include "fltk/layout.h" @@ -44,12 +46,18 @@ #include "fltk/Fl_Monitor_Arrangement.h" #include "fltk/Fl_Navigation.h" +#ifdef __APPLE__ +#include "cocoa.h" +#endif + #include <FL/Fl.H> +#include <FL/Fl_Box.H> #include <FL/Fl_Tabs.H> #include <FL/Fl_Button.H> #include <FL/Fl_Check_Button.H> #include <FL/Fl_Return_Button.H> #include <FL/Fl_Round_Button.H> +#include <FL/Fl_Toggle_Button.H> #include <FL/Fl_Int_Input.H> #include <FL/Fl_Choice.H> @@ -82,6 +90,7 @@ OptionsDialog::OptionsDialog() createCompressionPage(tx, ty, tw, th); createSecurityPage(tx, ty, tw, th); createInputPage(tx, ty, tw, th); + createShortcutsPage(tx, ty, tw, th); createDisplayPage(tx, ty, tw, th); createMiscPage(tx, ty, tw, th); } @@ -311,11 +320,19 @@ void OptionsDialog::loadOptions(void) #endif systemKeysCheckbox->value(fullscreenSystemKeys); - menuKeyChoice->value(0); + /* Keyboard shortcuts */ + unsigned modifierMask; + + modifierMask = 0; + for (core::EnumListEntry key : shortcutModifiers) + modifierMask |= ShortcutHandler::parseModifier(key.getValueStr().c_str()); - for (int idx = 0; idx < getMenuKeySymbolCount(); idx++) - if (menuKey == getMenuKeySymbols()[idx].name) - menuKeyChoice->value(idx + 1); + ctrlButton->value(modifierMask & ShortcutHandler::Control); + shiftButton->value(modifierMask & ShortcutHandler::Shift); + altButton->value(modifierMask & ShortcutHandler::Alt); + superButton->value(modifierMask & ShortcutHandler::Super); + + handleModifier(nullptr, this); /* Display */ if (!fullScreen) { @@ -452,11 +469,23 @@ void OptionsDialog::storeOptions(void) #endif fullscreenSystemKeys.setParam(systemKeysCheckbox->value()); - if (menuKeyChoice->value() == 0) - menuKey.setParam("None"); - else { - menuKey.setParam(menuKeyChoice->text()); - } + /* Keyboard shortcuts */ + std::list<std::string> modifierList; + + if (ctrlButton->value()) + modifierList.push_back( + ShortcutHandler::modifierString(ShortcutHandler::Control)); + if (shiftButton->value()) + modifierList.push_back( + ShortcutHandler::modifierString(ShortcutHandler::Shift)); + if (altButton->value()) + modifierList.push_back( + ShortcutHandler::modifierString(ShortcutHandler::Alt)); + if (superButton->value()) + modifierList.push_back( + ShortcutHandler::modifierString(ShortcutHandler::Super)); + + shortcutModifiers.setParam(modifierList); /* Display */ if (windowedButton->value()) { @@ -879,21 +908,11 @@ void OptionsDialog::createInputPage(int tx, int ty, int tw, int th) tx += INDENT; ty += TIGHT_MARGIN; - systemKeysCheckbox = new Fl_Check_Button(LBLRIGHT(tx, ty, - CHECK_MIN_WIDTH, - CHECK_HEIGHT, - _("Pass system keys directly to server (full screen)"))); + systemKeysCheckbox = new Fl_Check_Button( + LBLRIGHT(tx, ty, CHECK_MIN_WIDTH, CHECK_HEIGHT, + _("Always send all keyboard input in full screen"))); + systemKeysCheckbox->callback(handleSystemKeys, this); ty += CHECK_HEIGHT + TIGHT_MARGIN; - - menuKeyChoice = new Fl_Choice(LBLLEFT(tx, ty, 150, CHOICE_HEIGHT, _("Menu key"))); - - fltk_menu_add(menuKeyChoice, _("None"), 0, nullptr, nullptr, FL_MENU_DIVIDER); - for (int idx = 0; idx < getMenuKeySymbolCount(); idx++) - fltk_menu_add(menuKeyChoice, getMenuKeySymbols()[idx].name, 0, nullptr, nullptr, 0); - - fltk_adjust_choice(menuKeyChoice); - - ty += CHOICE_HEIGHT + TIGHT_MARGIN; } ty -= TIGHT_MARGIN; @@ -962,6 +981,76 @@ void OptionsDialog::createInputPage(int tx, int ty, int tw, int th) } +void OptionsDialog::createShortcutsPage(int tx, int ty, int tw, int th) +{ + Fl_Group *group = new Fl_Group(tx, ty, tw, th, _("Keyboard shortcuts")); + + tx += OUTER_MARGIN; + ty += OUTER_MARGIN; + + Fl_Box *intro = new Fl_Box(tx, ty, tw - OUTER_MARGIN * 2, INPUT_HEIGHT); + intro->align(FL_ALIGN_TOP_LEFT|FL_ALIGN_INSIDE); + intro->label(_("Modifier keys for keyboard shortcuts:")); + + ty += INPUT_HEIGHT + INNER_MARGIN; + + int width; + + width = (tw - OUTER_MARGIN * 2 - INNER_MARGIN * 3) / 4; + + ctrlButton = new Fl_Toggle_Button(tx, ty, + /* + * TRANSLATORS: This refers to the + * keyboard key + * */ + width, BUTTON_HEIGHT, _("Ctrl")); + ctrlButton->selection_color(FL_SELECTION_COLOR); + ctrlButton->callback(handleModifier, this); + shiftButton = new Fl_Toggle_Button(tx + width + INNER_MARGIN, ty, + /* + * TRANSLATORS: This refers to the + * keyboard key + * */ + width, BUTTON_HEIGHT, _("Shift")); + shiftButton->selection_color(FL_SELECTION_COLOR); + shiftButton->callback(handleModifier, this); + altButton = new Fl_Toggle_Button(tx + width * 2 + INNER_MARGIN * 2, ty, + /* + * TRANSLATORS: This refers to the + * keyboard key + * */ + width, BUTTON_HEIGHT, _("Alt")); + altButton->selection_color(FL_SELECTION_COLOR); + altButton->callback(handleModifier, this); + superButton = new Fl_Toggle_Button(tx + width * 3 + INNER_MARGIN * 3, ty, + /* + * TRANSLATORS: This refers to the + * keyboard key + * */ + width, BUTTON_HEIGHT, _("Win")); + superButton->selection_color(FL_SELECTION_COLOR); + superButton->callback(handleModifier, this); + +#ifdef __APPLE__ + /* TRANSLATORS: This refers to the keyboard key */ + ctrlButton->label(_("⌃ Ctrl")); + /* TRANSLATORS: This refers to the keyboard key */ + shiftButton->label(_("⇧ Shift")); + /* TRANSLATORS: This refers to the keyboard key */ + altButton->label(_("⌥ Option")); + /* TRANSLATORS: This refers to the keyboard key */ + superButton->label(_("⌘ Cmd")); +#endif + + ty += BUTTON_HEIGHT + INNER_MARGIN; + + shortcutsText = new Fl_Box(tx, ty, tw - OUTER_MARGIN * 2, th - ty - OUTER_MARGIN); + shortcutsText->align(FL_ALIGN_TOP_LEFT|FL_ALIGN_INSIDE|FL_ALIGN_WRAP); + + group->end(); +} + + void OptionsDialog::createDisplayPage(int tx, int ty, int tw, int th) { Fl_Group *group = new Fl_Group(tx, ty, tw, th, _("Display")); @@ -1130,6 +1219,20 @@ void OptionsDialog::handleRSAAES(Fl_Widget* /*widget*/, void *data) } +void OptionsDialog::handleSystemKeys(Fl_Widget* /*widget*/, void* data) +{ +#ifdef __APPLE__ + OptionsDialog* dialog = (OptionsDialog*)data; + + // Pop up the access dialog if needed + if (dialog->systemKeysCheckbox->value()) + cocoa_is_trusted(true); +#else + (void)data; +#endif +} + + void OptionsDialog::handleClipboard(Fl_Widget* /*widget*/, void *data) { (void)data; @@ -1147,6 +1250,61 @@ void OptionsDialog::handleClipboard(Fl_Widget* /*widget*/, void *data) #endif } +void OptionsDialog::handleModifier(Fl_Widget* /*widget*/, void *data) +{ + OptionsDialog *dialog = (OptionsDialog*)data; + unsigned mask; + + mask = 0; + if (dialog->ctrlButton->value()) + mask |= ShortcutHandler::Control; + if (dialog->shiftButton->value()) + mask |= ShortcutHandler::Shift; + if (dialog->altButton->value()) + mask |= ShortcutHandler::Alt; + if (dialog->superButton->value()) + mask |= ShortcutHandler::Super; + + if (mask == 0) { + dialog->shortcutsText->copy_label( + _("All keyboard shortcuts are disabled.")); + } else { + char prefix[256]; + char prefix_noplus[256]; + + std::string label; + + strcpy(prefix, ShortcutHandler::modifierPrefix(mask)); + strcpy(prefix_noplus, ShortcutHandler::modifierPrefix(mask, true)); + + label += core::format( + _("To release keyboard control from the session, press %s."), + prefix_noplus); + label += "\n\n"; + + label += core::format( + _("To pass all keyboard input to the session, press %sG."), + prefix); + label += "\n\n"; + + label += core::format( + _("To toggle full-screen mode, press %sEnter."), prefix); + label += "\n\n"; + + label += core::format( + _("To open the session context menu, press %sM."), prefix); + label += "\n\n"; + + label += core::format( + _("To send a key combination that includes %s directly to the " + "session, press %sSpace, release the space bar without " + "releasing %s, and press the desired key."), + prefix_noplus, prefix, prefix_noplus); + + dialog->shortcutsText->copy_label(label.c_str()); + } +} + void OptionsDialog::handleFullScreenMode(Fl_Widget* /*widget*/, void *data) { OptionsDialog *dialog = (OptionsDialog*)data; diff --git a/vncviewer/OptionsDialog.h b/vncviewer/OptionsDialog.h index 86a1423a..daa9f3e8 100644 --- a/vncviewer/OptionsDialog.h +++ b/vncviewer/OptionsDialog.h @@ -24,9 +24,11 @@ #include <FL/Fl_Window.H> class Fl_Widget; +class Fl_Box; class Fl_Group; class Fl_Check_Button; class Fl_Round_Button; +class Fl_Toggle_Button; class Fl_Input; class Fl_Int_Input; class Fl_Choice; @@ -54,6 +56,7 @@ protected: void createCompressionPage(int tx, int ty, int tw, int th); void createSecurityPage(int tx, int ty, int tw, int th); void createInputPage(int tx, int ty, int tw, int th); + void createShortcutsPage(int tx, int ty, int tw, int th); void createDisplayPage(int tx, int ty, int tw, int th); void createMiscPage(int tx, int ty, int tw, int th); @@ -65,8 +68,12 @@ protected: static void handleX509(Fl_Widget *widget, void *data); static void handleRSAAES(Fl_Widget *widget, void *data); + static void handleSystemKeys(Fl_Widget *widget, void *data); + static void handleClipboard(Fl_Widget *widget, void *data); + static void handleModifier(Fl_Widget *widget, void *data); + static void handleFullScreenMode(Fl_Widget *widget, void *data); static void handleCancel(Fl_Widget *widget, void *data); @@ -120,7 +127,6 @@ protected: Fl_Choice *cursorTypeChoice; Fl_Group *keyboardGroup; Fl_Check_Button *systemKeysCheckbox; - Fl_Choice *menuKeyChoice; Fl_Group *clipboardGroup; Fl_Check_Button *acceptClipboardCheckbox; #if !defined(WIN32) && !defined(__APPLE__) @@ -131,6 +137,14 @@ protected: Fl_Check_Button *sendPrimaryCheckbox; #endif + /* Keyboard shortcuts */ + Fl_Toggle_Button *ctrlButton; + Fl_Toggle_Button *altButton; + Fl_Toggle_Button *shiftButton; + Fl_Toggle_Button *superButton; + + Fl_Box *shortcutsText; + /* Display */ Fl_Group *displayModeGroup; Fl_Round_Button *windowedButton; diff --git a/vncviewer/ServerDialog.cxx b/vncviewer/ServerDialog.cxx index b7adabe7..3011e948 100644 --- a/vncviewer/ServerDialog.cxx +++ b/vncviewer/ServerDialog.cxx @@ -60,7 +60,7 @@ static core::LogWriter vlog("ServerDialog"); const char* SERVER_HISTORY="tigervnc.history"; ServerDialog::ServerDialog() - : Fl_Window(450, 0, _("VNC viewer: Connection details")) + : Fl_Window(450, 0, "TigerVNC") { int x, y, x2; Fl_Button *button; diff --git a/vncviewer/ShortcutHandler.cxx b/vncviewer/ShortcutHandler.cxx new file mode 100644 index 00000000..aa17a6d1 --- /dev/null +++ b/vncviewer/ShortcutHandler.cxx @@ -0,0 +1,275 @@ +/* Copyright 2021-2025 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 + +#define XK_MISCELLANY +#include <rfb/keysymdef.h> + +#include "ShortcutHandler.h" +#include "i18n.h" + +ShortcutHandler::ShortcutHandler() : + modifierMask(0), state(Idle) +{ +} + +void ShortcutHandler::setModifiers(unsigned mask) +{ + modifierMask = mask; + reset(); +} + +ShortcutHandler::KeyAction ShortcutHandler::handleKeyPress(int keyCode, + uint32_t keySym) +{ + unsigned modifier, pressedMask; + std::map<int, uint32_t>::const_iterator iter; + + pressedKeys[keyCode] = keySym; + + if (modifierMask == 0) + return KeyNormal; + + modifier = keySymToModifier(keySym); + + pressedMask = 0; + for (iter = pressedKeys.begin(); iter != pressedKeys.end(); ++iter) + pressedMask |= keySymToModifier(iter->second); + + switch (state) { + case Idle: + case Arming: + case Rearming: + if (pressedMask == modifierMask) { + // All triggering modifier keys are pressed + state = Armed; + } if (modifier && ((modifier & modifierMask) == modifier)) { + // The new key is part of the triggering set + if (state == Idle) + state = Arming; + } else { + // The new key was something else + state = Wedged; + } + return KeyNormal; + case Armed: + if (modifier && ((modifier & modifierMask) == modifier)) { + // The new key is part of the triggering set + return KeyNormal; + } else if (modifier) { + // The new key is some other modifier + state = Wedged; + return KeyNormal; + } else { + // The new key was something else + state = Firing; + firedKeys.insert(keyCode); + return KeyShortcut; + } + break; + case Firing: + if (modifier) { + // The new key is a modifier (may or may not be part of the + // triggering set) + return KeyIgnore; + } else { + // The new key was something else + firedKeys.insert(keyCode); + return KeyShortcut; + } + default: + break; + } + + return KeyNormal; +} + +ShortcutHandler::KeyAction ShortcutHandler::handleKeyRelease(int keyCode) +{ + bool firedKey; + unsigned pressedMask; + std::map<int, uint32_t>::const_iterator iter; + KeyAction action; + + firedKey = firedKeys.count(keyCode) != 0; + + firedKeys.erase(keyCode); + pressedKeys.erase(keyCode); + + pressedMask = 0; + for (iter = pressedKeys.begin(); iter != pressedKeys.end(); ++iter) + pressedMask |= keySymToModifier(iter->second); + + switch (state) { + case Arming: + action = KeyNormal; + break; + case Armed: + if (pressedKeys.empty()) + action = KeyUnarm; + else if (pressedMask == modifierMask) + action = KeyNormal; + else { + action = KeyNormal; + state = Rearming; + } + break; + case Rearming: + if (pressedKeys.empty()) + action = KeyUnarm; + else + action = KeyNormal; + break; + case Firing: + if (firedKey) + action = KeyShortcut; + else + action = KeyIgnore; + break; + default: + action = KeyNormal; + } + + if (pressedKeys.empty()) + state = Idle; + + return action; +} + +void ShortcutHandler::reset() +{ + state = Idle; + firedKeys.clear(); + pressedKeys.clear(); +} + +// Keep list of valid values in sync with shortcutModifiers +unsigned ShortcutHandler::parseModifier(const char* key) +{ + if (strcasecmp(key, "Ctrl") == 0) + return Control; + else if (strcasecmp(key, "Shift") == 0) + return Shift; + else if (strcasecmp(key, "Alt") == 0) + return Alt; + else if (strcasecmp(key, "Win") == 0) + return Super; + else if (strcasecmp(key, "Super") == 0) + return Super; + else if (strcasecmp(key, "Option") == 0) + return Alt; + else if (strcasecmp(key, "Cmd") == 0) + return Super; + else + return 0; +} + +const char* ShortcutHandler::modifierString(unsigned key) +{ + if (key == Control) + return "Ctrl"; + if (key == Shift) + return "Shift"; + if (key == Alt) + return "Alt"; + if (key == Super) + return "Super"; + + return ""; +} + +const char* ShortcutHandler::modifierPrefix(unsigned mask, + bool justPrefix) +{ + static char prefix[256]; + + prefix[0] = '\0'; + if (mask & Control) { +#ifdef __APPLE__ + strcat(prefix, "⌃"); +#else + strcat(prefix, _("Ctrl")); + strcat(prefix, "+"); +#endif + } + if (mask & Shift) { +#ifdef __APPLE__ + strcat(prefix, "⇧"); +#else + strcat(prefix, _("Shift")); + strcat(prefix, "+"); +#endif + } + if (mask & Alt) { +#ifdef __APPLE__ + strcat(prefix, "⌥"); +#else + strcat(prefix, _("Alt")); + strcat(prefix, "+"); +#endif + } + if (mask & Super) { +#ifdef __APPLE__ + strcat(prefix, "⌘"); +#else + strcat(prefix, _("Win")); + strcat(prefix, "+"); +#endif + } + + if (prefix[0] == '\0') + return ""; + + if (justPrefix) { +#ifndef __APPLE__ + prefix[strlen(prefix)-1] = '\0'; +#endif + return prefix; + } + +#ifdef __APPLE__ + strcat(prefix, "\xc2\xa0"); // U+00A0 NO-BREAK SPACE +#endif + + return prefix; +} + +unsigned ShortcutHandler::keySymToModifier(uint32_t keySym) +{ + switch (keySym) { + case XK_Control_L: + case XK_Control_R: + return Control; + case XK_Shift_L: + case XK_Shift_R: + return Shift; + case XK_Alt_L: + case XK_Alt_R: + return Alt; + case XK_Super_L: + case XK_Super_R: + case XK_Hyper_L: + case XK_Hyper_R: + return Super; + } + + return 0; +} diff --git a/vncviewer/ShortcutHandler.h b/vncviewer/ShortcutHandler.h new file mode 100644 index 00000000..bb6497a9 --- /dev/null +++ b/vncviewer/ShortcutHandler.h @@ -0,0 +1,79 @@ +/* Copyright 2021-2025 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. + */ + +#ifndef __SHORTCUTHANDLER__ +#define __SHORTCUTHANDLER__ + +#include <set> +#include <map> + +#include <stdint.h> + +class ShortcutHandler { +public: + ShortcutHandler(); + + void setModifiers(unsigned mask); + + enum KeyAction { + KeyNormal, + KeyUnarm, + KeyShortcut, + KeyIgnore, + }; + + KeyAction handleKeyPress(int keyCode, uint32_t keySym); + KeyAction handleKeyRelease(int keyCode); + + void reset(); + +public: + enum Modifier { + Control = (1<<0), + Shift = (1<<1), + Alt = (1<<2), + Super = (1<<3), + }; + + static unsigned parseModifier(const char* key); + static const char* modifierString(unsigned key); + + static const char* modifierPrefix(unsigned mask, + bool justPrefix=false); + +private: + unsigned keySymToModifier(uint32_t keySym); + +private: + unsigned modifierMask; + + enum State { + Idle, + Arming, + Armed, + Rearming, + Firing, + Wedged, + }; + State state; + + std::set<int> firedKeys; + std::map<int, uint32_t> pressedKeys; +}; + +#endif diff --git a/vncviewer/Viewport.cxx b/vncviewer/Viewport.cxx index 175be172..03e6fb09 100644 --- a/vncviewer/Viewport.cxx +++ b/vncviewer/Viewport.cxx @@ -1,5 +1,5 @@ /* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. - * Copyright 2011-2021 Pierre Ossman for Cendio AB + * Copyright 2011-2025 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 @@ -31,14 +31,21 @@ #include <core/string.h> #include <rfb/CMsgWriter.h> +#include <rfb/Cursor.h> +#include <rfb/KeysymStr.h> #include <rfb/ledStates.h> // FLTK can pull in the X11 headers on some systems #ifndef XK_VoidSymbol +#define XK_LATIN1 #define XK_MISCELLANY #include <rfb/keysymdef.h> #endif +#ifndef NoSymbol +#define NoSymbol 0 +#endif + #include "fltk/layout.h" #include "fltk/util.h" #include "Viewport.h" @@ -47,7 +54,6 @@ #include "DesktopWindow.h" #include "i18n.h" #include "parameters.h" -#include "menukey.h" #include "vncviewer.h" #include "PlatformPixelBuffer.h" @@ -76,7 +82,7 @@ static core::LogWriter vlog("Viewport"); // Menu constants enum { ID_DISCONNECT, ID_FULLSCREEN, ID_MINIMIZE, ID_RESIZE, - ID_CTRL, ID_ALT, ID_MENUKEY, ID_CTRLALTDEL, + ID_CTRL, ID_ALT, ID_CTRLALTDEL, ID_REFRESH, ID_OPTIONS, ID_INFO, ID_ABOUT }; // Used for fake key presses from the menu @@ -87,10 +93,10 @@ static const int FAKE_DEL_KEY_CODE = 0x10003; // Used for fake key presses for lock key sync static const int FAKE_KEY_CODE = 0xffff; -Viewport::Viewport(int w, int h, const rfb::PixelFormat& /*serverPF*/, CConn* cc_) +Viewport::Viewport(int w, int h, CConn* cc_) : Fl_Widget(0, 0, w, h), cc(cc_), frameBuffer(nullptr), lastPointerPos(0, 0), lastButtonMask(0), - keyboard(nullptr), + keyboard(nullptr), shortcutBypass(false), shortcutActive(false), firstLEDState(true), pendingClientClipboard(false), menuCtrlKey(false), menuAltKey(false), cursor(nullptr), cursorIsBlank(false) @@ -129,12 +135,16 @@ Viewport::Viewport(int w, int h, const rfb::PixelFormat& /*serverPF*/, CConn* cc // reparenting to the current window works for most cases. window()->add(contextMenu); - setMenuKey(); + unsigned modifierMask = 0; + for (core::EnumListEntry key : shortcutModifiers) + modifierMask |= ShortcutHandler::parseModifier(key.getValueStr().c_str()); + + shortcutHandler.setModifiers(modifierMask); OptionsDialog::addCallback(handleOptions, this); // Make sure we have an initial blank cursor set - setCursor(0, 0, {0, 0}, nullptr); + setCursor(); } @@ -191,10 +201,12 @@ static const char * dotcursor_xpm[] = { " ... ", " "}; -void Viewport::setCursor(int width, int height, - const core::Point& hotspot, - const uint8_t* data) +void Viewport::setCursor() { + int width, height; + core::Point hotspot; + const uint8_t* data; + int i; if (cursor) { @@ -203,6 +215,11 @@ void Viewport::setCursor(int width, int height, delete cursor; } + width = cc->server.cursor().width(); + height = cc->server.cursor().height(); + hotspot = cc->server.cursor().hotspot(); + data = cc->server.cursor().getBuffer(); + for (i = 0; i < width*height; i++) if (data[i*4 + 3] != 0) break; @@ -665,23 +682,122 @@ void Viewport::resetKeyboard() } keyboard->reset(); + + shortcutHandler.reset(); + shortcutBypass = false; + shortcutActive = false; + pressedKeys.clear(); } void Viewport::handleKeyPress(int systemKeyCode, uint32_t keyCode, uint32_t 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; + pressedKeys.insert(systemKeyCode); + + // Possible keyboard shortcut? + + if (!shortcutBypass) { + ShortcutHandler::KeyAction action; + + action = shortcutHandler.handleKeyPress(systemKeyCode, keySym); + + if (action == ShortcutHandler::KeyIgnore) { + vlog.debug("Ignoring key press %d => 0x%02x / XK_%s (0x%04x)", + systemKeyCode, keyCode, KeySymName(keySym), keySym); + return; + } + + if (action == ShortcutHandler::KeyShortcut) { + std::list<uint32_t> keySyms; + std::list<uint32_t>::const_iterator iter; + + // Modifiers can change the KeySym that's been resolved, so we + // need to check all possible KeySyms for this physical key, not + // just the current one + keySyms = keyboard->translateToKeySyms(systemKeyCode); + + // Then we pick the one that matches first + keySym = NoSymbol; + for (iter = keySyms.begin(); iter != keySyms.end(); iter++) { + bool found; + + switch (*iter) { + case XK_space: + case XK_G: + case XK_g: + case XK_M: + case XK_m: + case XK_KP_Enter: + case XK_Return: + keySym = *iter; + found = true; + break; + default: + found = false; + break; + } + + if (found) + break; + } + + vlog.debug("Detected shortcut %d => 0x%02x / XK_%s (0x%04x)", + systemKeyCode, keyCode, KeySymName(keySym), keySym); + + // Special case which we need to handle first + if (keySym == XK_space) { + // If another shortcut has already fired, then we're too late as + // we've already released the modifier keys + if (!shortcutActive) { + shortcutBypass = true; + shortcutHandler.reset(); + } + return; + } + + shortcutActive = true; + + // The remote session won't see any more keys, so release the ones + // currently down + try { + cc->releaseAllKeys(); + } catch (std::exception& e) { + vlog.error("%s", e.what()); + abort_connection(_("An unexpected error occurred when communicating " + "with the server:\n\n%s"), e.what()); + } + + switch (keySym) { + case XK_G: + case XK_g: + ((DesktopWindow*)window())->grabKeyboard(); + break; + case XK_M: + case XK_m: + popupContextMenu(); + break; + case XK_KP_Enter: + case XK_Return: + if (window()->fullscreen_active()) { + fullScreen.setParam(false); + window()->fullscreen_off(); + } else { + fullScreen.setParam(true); + ((DesktopWindow*)window())->fullscreen_on(); + } + break; + default: + // Unknown/Unused keyboard shortcut + break; + } + + return; + } } + // Normal key, so send to server... + if (viewOnly) return; @@ -696,6 +812,54 @@ void Viewport::handleKeyPress(int systemKeyCode, void Viewport::handleKeyRelease(int systemKeyCode) { + pressedKeys.erase(systemKeyCode); + + if (pressedKeys.empty()) + shortcutActive = false; + + // Possible keyboard shortcut? + + if (!shortcutBypass) { + ShortcutHandler::KeyAction action; + + action = shortcutHandler.handleKeyRelease(systemKeyCode); + + if (action == ShortcutHandler::KeyIgnore) { + vlog.debug("Ignoring key release %d", systemKeyCode); + return; + } + + if (action == ShortcutHandler::KeyShortcut) { + vlog.debug("Shortcut release %d", systemKeyCode); + return; + } + + if (action == ShortcutHandler::KeyUnarm) { + DesktopWindow *win; + + vlog.debug("Detected shortcut to release grab"); + + try { + cc->releaseAllKeys(); + } catch (std::exception& e) { + vlog.error("%s", e.what()); + abort_connection(_("An unexpected error occurred when communicating " + "with the server:\n\n%s"), e.what()); + } + + win = dynamic_cast<DesktopWindow*>(window()); + assert(win); + win->ungrabKeyboard(); + + return; + } + } + + if (pressedKeys.empty()) + shortcutBypass = false; + + // Normal key, so send to server... + if (viewOnly) return; @@ -718,13 +882,11 @@ int Viewport::handleSystemEvent(void *event, void *data) if (!self->hasFocus()) return 0; -#ifdef __APPLE__ // Special event that means we temporarily lost some input - if (KeyboardMacOS::isKeyboardSync(event)) { + if (self->keyboard->isKeyboardReset(event)) { self->resetKeyboard(); return 1; } -#endif consumed = self->keyboard->handleEvent(event); if (consumed) @@ -760,16 +922,6 @@ void Viewport::initContextMenu() 0, nullptr, (void*)ID_ALT, FL_MENU_TOGGLE | (menuAltKey?FL_MENU_VALUE:0)); - if (menuKeySym) { - char sendMenuKey[64]; - snprintf(sendMenuKey, 64, p_("ContextMenu|", "Send %s"), - menuKey.getValueStr().c_str()); - fltk_menu_add(contextMenu, sendMenuKey, 0, nullptr, (void*)ID_MENUKEY, 0); - fltk_menu_add(contextMenu, "Secret shortcut menu key", - menuKeyFLTK, nullptr, - (void*)ID_MENUKEY, FL_MENU_INVISIBLE); - } - fltk_menu_add(contextMenu, p_("ContextMenu|", "Send Ctrl-Alt-&Del"), 0, nullptr, (void*)ID_CTRLALTDEL, FL_MENU_DIVIDER); @@ -780,7 +932,7 @@ void Viewport::initContextMenu() 0, nullptr, (void*)ID_OPTIONS, 0); fltk_menu_add(contextMenu, p_("ContextMenu|", "Connection &info..."), 0, nullptr, (void*)ID_INFO, 0); - fltk_menu_add(contextMenu, p_("ContextMenu|", "About &TigerVNC viewer..."), + fltk_menu_add(contextMenu, p_("ContextMenu|", "About &TigerVNC..."), 0, nullptr, (void*)ID_ABOUT, 0); } #pragma GCC diagnostic pop @@ -803,11 +955,11 @@ void Viewport::popupContextMenu() window()->cursor(FL_CURSOR_DEFAULT); // FLTK also doesn't switch focus properly for menus - handle(FL_UNFOCUS); + Fl::handle(FL_UNFOCUS, window()); m = contextMenu->popup(); - handle(FL_FOCUS); + Fl::handle(FL_FOCUS, window()); // Back to our proper mouse pointer. if (Fl::belowmouse() == this) @@ -854,10 +1006,6 @@ void Viewport::popupContextMenu() handleKeyRelease(FAKE_ALT_KEY_CODE); menuAltKey = !menuAltKey; break; - case ID_MENUKEY: - handleKeyPress(FAKE_KEY_CODE, menuKeyCode, menuKeySym); - handleKeyRelease(FAKE_KEY_CODE); - break; case ID_CTRLALTDEL: handleKeyPress(FAKE_CTRL_KEY_CODE, 0x1d, XK_Control_L); handleKeyPress(FAKE_ALT_KEY_CODE, 0x38, XK_Alt_L); @@ -886,18 +1034,17 @@ void Viewport::popupContextMenu() } } - -void Viewport::setMenuKey() -{ - getMenuKey(&menuKeyFLTK, &menuKeyCode, &menuKeySym); -} - - void Viewport::handleOptions(void *data) { Viewport *self = (Viewport*)data; + unsigned modifierMask; + + modifierMask = 0; + for (core::EnumListEntry key : shortcutModifiers) + modifierMask |= ShortcutHandler::parseModifier(key.getValueStr().c_str()); + + self->shortcutHandler.setModifiers(modifierMask); - self->setMenuKey(); if (Fl::belowmouse() == self) self->showCursor(); } diff --git a/vncviewer/Viewport.h b/vncviewer/Viewport.h index 0f606089..8e3f473e 100644 --- a/vncviewer/Viewport.h +++ b/vncviewer/Viewport.h @@ -26,6 +26,7 @@ #include "EmulateMB.h" #include "Keyboard.h" +#include "ShortcutHandler.h" class Fl_Menu_Button; class Fl_RGB_Image; @@ -39,7 +40,7 @@ class Viewport : public Fl_Widget, protected EmulateMB, protected KeyboardHandler { public: - Viewport(int w, int h, const rfb::PixelFormat& serverPF, CConn* cc_); + Viewport(int w, int h, CConn* cc_); ~Viewport(); // Most efficient format (from Viewport's point of view) @@ -49,8 +50,7 @@ public: void updateWindow(); // New image for the locally rendered cursor - void setCursor(int width, int height, const core::Point& hotspot, - const uint8_t* data); + void setCursor(); // Change client LED state void setLEDState(unsigned int state); @@ -100,8 +100,6 @@ private: void initContextMenu(); void popupContextMenu(); - void setMenuKey(); - static void handleOptions(void *data); private: @@ -113,6 +111,10 @@ private: uint16_t lastButtonMask; Keyboard* keyboard; + ShortcutHandler shortcutHandler; + bool shortcutBypass; + bool shortcutActive; + std::set<int> pressedKeys; bool firstLEDState; @@ -120,8 +122,6 @@ private: int clipboardSource; - uint32_t menuKeySym; - int menuKeyCode, menuKeyFLTK; Fl_Menu_Button *contextMenu; bool menuCtrlKey; diff --git a/vncviewer/Win32TouchHandler.cxx b/vncviewer/Win32TouchHandler.cxx index d1d81ae1..2777cca2 100644 --- a/vncviewer/Win32TouchHandler.cxx +++ b/vncviewer/Win32TouchHandler.cxx @@ -310,6 +310,9 @@ void Win32TouchHandler::fakeButtonEvent(bool press, int button, LPARAM lParam; int delta; + // Needed to silence false positive that this is used uninitialized + delta = 0; + switch (button) { case 1: // left mousebutton diff --git a/vncviewer/cocoa.h b/vncviewer/cocoa.h index b3a8326c..09db9a45 100644 --- a/vncviewer/cocoa.h +++ b/vncviewer/cocoa.h @@ -23,11 +23,10 @@ class Fl_Window; void cocoa_prevent_native_fullscreen(Fl_Window *win); -int cocoa_get_level(Fl_Window *win); -void cocoa_set_level(Fl_Window *win, int level); +bool cocoa_is_trusted(bool prompt=false); -int cocoa_capture_displays(Fl_Window *win); -void cocoa_release_displays(Fl_Window *win); +bool cocoa_tap_keyboard(); +void cocoa_untap_keyboard(); typedef struct CGColorSpace *CGColorSpaceRef; diff --git a/vncviewer/cocoa.mm b/vncviewer/cocoa.mm index 0675c429..4d9908dd 100644 --- a/vncviewer/cocoa.mm +++ b/vncviewer/cocoa.mm @@ -20,15 +20,19 @@ #include <config.h> #endif -#include <FL/Fl.H> +#include <assert.h> +#include <dlfcn.h> + #include <FL/Fl_Window.H> #include <FL/x.H> #import <Cocoa/Cocoa.h> +#import <ApplicationServices/ApplicationServices.h> -#include <core/Rect.h> +#include "cocoa.h" -static bool captured = false; +static CFMachPortRef event_tap; +static CFRunLoopSourceRef tap_source; void cocoa_prevent_native_fullscreen(Fl_Window *win) { @@ -40,102 +44,183 @@ void cocoa_prevent_native_fullscreen(Fl_Window *win) #endif } -int cocoa_get_level(Fl_Window *win) +bool cocoa_is_trusted(bool prompt) { - NSWindow *nsw; - nsw = (NSWindow*)fl_xid(win); - assert(nsw); - return [nsw level]; -} + CFStringRef keys[1]; + CFBooleanRef values[1]; + CFDictionaryRef options; -void cocoa_set_level(Fl_Window *win, int level) -{ - NSWindow *nsw; - nsw = (NSWindow*)fl_xid(win); - assert(nsw); - [nsw setLevel:level]; -} + Boolean trusted; -int cocoa_capture_displays(Fl_Window *win) -{ - NSWindow *nsw; - - nsw = (NSWindow*)fl_xid(win); - assert(nsw); +#if !defined(MAC_OS_X_VERSION_10_9) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9 + // FIXME: Raise system requirements so this isn't needed + void *lib; + typedef Boolean (*AXIsProcessTrustedWithOptionsRef)(CFDictionaryRef); + AXIsProcessTrustedWithOptionsRef AXIsProcessTrustedWithOptions; + CFStringRef kAXTrustedCheckOptionPrompt; - CGDisplayCount count; - CGDirectDisplayID displays[16]; + lib = dlopen(nullptr, 0); + if (lib == nullptr) + return false; - int sx, sy, sw, sh; - core::Rect windows_rect, screen_rect; + AXIsProcessTrustedWithOptions = + (AXIsProcessTrustedWithOptionsRef)dlsym(lib, "AXIsProcessTrustedWithOptions"); - windows_rect.setXYWH(win->x(), win->y(), win->w(), win->h()); + dlclose(lib); - if (CGGetActiveDisplayList(16, displays, &count) != kCGErrorSuccess) - return 1; + if (AXIsProcessTrustedWithOptions == nullptr) + return false; - if (count != (unsigned)Fl::screen_count()) - return 1; + kAXTrustedCheckOptionPrompt = CFSTR("AXTrustedCheckOptionPrompt"); +#endif - for (int i = 0; i < Fl::screen_count(); i++) { - Fl::screen_xywh(sx, sy, sw, sh, i); + keys[0] = kAXTrustedCheckOptionPrompt; + values[0] = prompt ? kCFBooleanTrue : kCFBooleanFalse; + options = CFDictionaryCreate(kCFAllocatorDefault, + (const void**)keys, + (const void**)values, 1, + &kCFCopyStringDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + if (options == nullptr) + return false; + + trusted = AXIsProcessTrustedWithOptions(options); + CFRelease(options); + + // For some reason, the authentication popups isn't set as active and + // is hidden behind our window(s). Try to find it and manually switch + // to it. + if (!trusted && prompt) { + long long pid; + + pid = 0; + for (int attempt = 0; attempt < 5; attempt++) { + CFArrayRef windowList; + + windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, + kCGNullWindowID); + for (int i = 0; i < CFArrayGetCount(windowList); i++) { + CFDictionaryRef window; + CFStringRef owner; + CFNumberRef cfpid; + + window = (CFDictionaryRef)CFArrayGetValueAtIndex(windowList, i); + assert(window != nullptr); + owner = (CFStringRef)CFDictionaryGetValue(window, + kCGWindowOwnerName); + if (owner == nullptr) + continue; + + // FIXME: Unknown how stable this identifier is + CFStringRef authOwner = CFSTR("universalAccessAuthWarn"); + if (CFStringCompare(owner, authOwner, 0) != kCFCompareEqualTo) + continue; + + cfpid = (CFNumberRef)CFDictionaryGetValue(window, + kCGWindowOwnerPID); + if (cfpid == nullptr) + continue; + + CFNumberGetValue(cfpid, kCFNumberLongLongType, &pid); + break; + } + + CFRelease(windowList); + + if (pid != 0) + break; + + usleep(100000); + } - screen_rect.setXYWH(sx, sy, sw, sh); - if (screen_rect.enclosed_by(windows_rect)) { - if (CGDisplayCapture(displays[i]) != kCGErrorSuccess) - return 1; + if (pid != 0) { + NSRunningApplication* authApp; - } else { - // A display might have been captured with the previous - // monitor selection. In that case we don't want to keep - // it when its no longer inside the window_rect. - CGDisplayRelease(displays[i]); + authApp = [NSRunningApplication runningApplicationWithProcessIdentifier:pid]; + if (authApp != nil) { + // Seems to work fine even without yieldActivationToApplication, + // or NSApplicationActivateIgnoringOtherApps + [authApp activateWithOptions:0]; + } } } - captured = true; + return trusted; +} - if ([nsw level] == CGShieldingWindowLevel()) - return 0; +static CGEventRef cocoa_event_tap(CGEventTapProxy /*proxy*/, + CGEventType type, CGEventRef event, + void* /*refcon*/) +{ + ProcessSerialNumber psn; + OSErr err; + + // We should just be getting these events, but just in case + if ((type != kCGEventKeyDown) && + (type != kCGEventKeyUp) && + (type != kCGEventFlagsChanged)) + return event; + + // Redirect the event to us, no matter the original target + // (note that this will loop if kCGAnnotatedSessionEventTap is used) + err = GetCurrentProcess(&psn); + if (err != noErr) + return event; + + // FIXME: CGEventPostToPid() in macOS 10.11+ + CGEventPostToPSN(&psn, event); + + // Stop delivery to original target + return nullptr; +} - [nsw setLevel:CGShieldingWindowLevel()]; +bool cocoa_tap_keyboard() +{ + CGEventMask mask; - // We're not getting put in front of the shielding window in many - // cases on macOS 13, despite setLevel: being documented as also - // pushing the window to the front. So let's explicitly move it. - [nsw orderFront:nsw]; + if (event_tap != nullptr) + return true; - return 0; -} + if (!cocoa_is_trusted()) + return false; -void cocoa_release_displays(Fl_Window *win) -{ - NSWindow *nsw; - int newlevel; + mask = CGEventMaskBit(kCGEventKeyDown) | + CGEventMaskBit(kCGEventKeyUp) | + CGEventMaskBit(kCGEventFlagsChanged); - if (captured) - CGReleaseAllDisplays(); + // Cannot be kCGAnnotatedSessionEventTap as window manager intercepts + // before that (e.g. Ctrl+Up) + event_tap = CGEventTapCreate(kCGSessionEventTap, + kCGHeadInsertEventTap, + kCGEventTapOptionDefault, + mask, cocoa_event_tap, nullptr); + if (event_tap == nullptr) + return false; - captured = false; + tap_source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, + event_tap, 0); + CFRunLoopAddSource(CFRunLoopGetCurrent(), tap_source, + kCFRunLoopCommonModes); - nsw = (NSWindow*)fl_xid(win); - assert(nsw); + return true; +} - // Someone else has already changed the level of this window - if ([nsw level] != CGShieldingWindowLevel()) +void cocoa_untap_keyboard() +{ + if (event_tap == nullptr) return; - // FIXME: Store the previous level somewhere so we don't have to hard - // code a level here. - if (win->fullscreen_active() && win->contains(Fl::focus())) - newlevel = NSStatusWindowLevel; - else - newlevel = NSNormalWindowLevel; - - // Only change if different as the level change also moves the window - // to the top of that level. - if ([nsw level] != newlevel) - [nsw setLevel:newlevel]; + // Need to explicitly disable the tap first, or we get a short delay + // where all events are dropped + CGEventTapEnable(event_tap, false); + + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), tap_source, + kCFRunLoopCommonModes); + CFRelease(tap_source); + tap_source = nullptr; + + CFRelease(event_tap); + event_tap = nullptr; } CGColorSpaceRef cocoa_win_color_space(Fl_Window *win) diff --git a/vncviewer/fltk/Fl_Navigation.cxx b/vncviewer/fltk/Fl_Navigation.cxx index d3117aae..be258ce5 100644 --- a/vncviewer/fltk/Fl_Navigation.cxx +++ b/vncviewer/fltk/Fl_Navigation.cxx @@ -31,6 +31,7 @@ #include <FL/Fl_Button.H> #include <FL/Fl_Scroll.H> +#include <FL/fl_draw.H> #include "Fl_Navigation.h" @@ -154,14 +155,21 @@ void Fl_Navigation::update_labels() for (i = 0;i < pages->children();i++) { Fl_Widget *page; Fl_Button *btn; + int w, h; page = pages->child(i); + w = labels->w() - page->labelsize() * 2; + fl_font(page->labelfont(), page->labelsize()); + fl_measure(page->label(), w, h); + h += page->labelsize() * 2; + btn = new Fl_Button(labels->x(), labels->y() + offset, - labels->w(), page->labelsize() * 3, + labels->w(), h, page->label()); btn->box(FL_FLAT_BOX); btn->type(FL_RADIO_BUTTON); + btn->align(btn->align() | FL_ALIGN_WRAP); btn->color(FL_BACKGROUND2_COLOR); btn->selection_color(FL_SELECTION_COLOR); btn->labelsize(page->labelsize()); @@ -171,7 +179,7 @@ void Fl_Navigation::update_labels() btn->callback(label_pressed, this); labels->add(btn); - offset += page->labelsize() * 3; + offset += h; } labels->size(labels->w(), offset); diff --git a/vncviewer/menukey.cxx b/vncviewer/menukey.cxx deleted file mode 100644 index 59e1daa1..00000000 --- a/vncviewer/menukey.cxx +++ /dev/null @@ -1,83 +0,0 @@ -/* Copyright 2011 Martin Koegler <mkoegler@auto.tuwien.ac.at> - * Copyright 2011 Pierre Ossman <ossman@cendio.se> 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 <string.h> -#include <FL/Fl.H> - -// FLTK can pull in the X11 headers on some systems -#ifndef XK_VoidSymbol -#define XK_MISCELLANY -#include <rfb/keysymdef.h> -#endif - -#include "menukey.h" -#include "parameters.h" - -static const MenuKeySymbol menuSymbols[] = { - {"F1", FL_F + 1, 0x3b, XK_F1}, - {"F2", FL_F + 2, 0x3c, XK_F2}, - {"F3", FL_F + 3, 0x3d, XK_F3}, - {"F4", FL_F + 4, 0x3e, XK_F4}, - {"F5", FL_F + 5, 0x3f, XK_F5}, - {"F6", FL_F + 6, 0x40, XK_F6}, - {"F7", FL_F + 7, 0x41, XK_F7}, - {"F8", FL_F + 8, 0x42, XK_F8}, - {"F9", FL_F + 9, 0x43, XK_F9}, - {"F10", FL_F + 10, 0x44, XK_F10}, - {"F11", FL_F + 11, 0x57, XK_F11}, - {"F12", FL_F + 12, 0x58, XK_F12}, - {"Pause", FL_Pause, 0xc6, XK_Pause}, - {"Scroll_Lock", FL_Scroll_Lock, 0x46, XK_Scroll_Lock}, - {"Escape", FL_Escape, 0x01, XK_Escape}, - {"Insert", FL_Insert, 0xd2, XK_Insert}, - {"Delete", FL_Delete, 0xd3, XK_Delete}, - {"Home", FL_Home, 0xc7, XK_Home}, - {"Page_Up", FL_Page_Up, 0xc9, XK_Page_Up}, - {"Page_Down", FL_Page_Down, 0xd1, XK_Page_Down}, -}; - -int getMenuKeySymbolCount() -{ - return sizeof(menuSymbols)/sizeof(menuSymbols[0]); -} - -const MenuKeySymbol* getMenuKeySymbols() -{ - return menuSymbols; -} - -void getMenuKey(int *fltkcode, int *keycode, uint32_t *keysym) -{ - for(int i = 0; i < getMenuKeySymbolCount(); i++) { - if (menuKey == menuSymbols[i].name) { - *fltkcode = menuSymbols[i].fltkcode; - *keycode = menuSymbols[i].keycode; - *keysym = menuSymbols[i].keysym; - return; - } - } - - *fltkcode = 0; - *keycode = 0; - *keysym = 0; -} diff --git a/vncviewer/menukey.h b/vncviewer/menukey.h deleted file mode 100644 index 50106955..00000000 --- a/vncviewer/menukey.h +++ /dev/null @@ -1,34 +0,0 @@ -/* Copyright 2011 Martin Koegler <mkoegler@auto.tuwien.ac.at> - * - * 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. - */ -#ifndef __KEYSYM_H__ -#define __KEYSYM_H__ - -#include <stdint.h> - -typedef struct { - const char* name; - int fltkcode; - int keycode; - uint32_t keysym; -} MenuKeySymbol; - -void getMenuKey(int *fltkcode, int *keycode, uint32_t *keysym); -int getMenuKeySymbolCount(); -const MenuKeySymbol* getMenuKeySymbols(); - -#endif diff --git a/vncviewer/org.tigervnc.vncviewer.metainfo.xml.in b/vncviewer/org.tigervnc.vncviewer.metainfo.xml.in index 363c12fa..207f9707 100644 --- a/vncviewer/org.tigervnc.vncviewer.metainfo.xml.in +++ b/vncviewer/org.tigervnc.vncviewer.metainfo.xml.in @@ -10,7 +10,7 @@ <id>org.tigervnc.vncviewer</id> <metadata_license>FSFAP</metadata_license> <project_license>GPL-2.0-or-later</project_license> - <name>TigerVNC Viewer</name> + <name>TigerVNC</name> <summary>Connect to VNC server and display remote desktop</summary> <content_rating type="oars-1.1"/> <description> @@ -30,15 +30,15 @@ <launchable type="desktop-id">vncviewer.desktop</launchable> <screenshots> <screenshot type="default"> - <caption>TigerVNC viewer connection to a CentOS machine</caption> + <caption>TigerVNC connection to a CentOS machine</caption> <image>https://raw.githubusercontent.com/TigerVNC/tigervnc/741d3edbfab65eda6f033078bc06347fe244ea6a/vncviewer/metainfo/tigervnc-connection-linux.jpg</image> </screenshot> <screenshot> - <caption>TigerVNC viewer connection to a macOS machine</caption> + <caption>TigerVNC connection to a macOS machine</caption> <image>https://raw.githubusercontent.com/TigerVNC/tigervnc/741d3edbfab65eda6f033078bc06347fe244ea6a/vncviewer/metainfo/tigervnc-connection-macos.jpg</image> </screenshot> <screenshot> - <caption>TigerVNC viewer connection to a Windows machine</caption> + <caption>TigerVNC connection to a Windows machine</caption> <image>https://raw.githubusercontent.com/TigerVNC/tigervnc/741d3edbfab65eda6f033078bc06347fe244ea6a/vncviewer/metainfo/tigervnc-connection-windows.jpg</image> </screenshot> </screenshots> diff --git a/vncviewer/parameters.cxx b/vncviewer/parameters.cxx index 957838a7..d2000181 100644 --- a/vncviewer/parameters.cxx +++ b/vncviewer/parameters.cxx @@ -217,19 +217,21 @@ core::BoolParameter true); core::StringParameter display("display", - "Specifies the X display on which the VNC viewer window " + "Specifies the X display on which the TigerVNC window " "should appear.", ""); #endif -// Empty string means None, for backward compatibility -core::EnumParameter - menuKey("MenuKey", - "The key which brings up the popup menu", - {"", "None", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", - "F9", "F10", "F11", "F12", "Pause", "Scroll_Lock", "Escape", - "Insert", "Delete", "Home", "Page_Up", "Page_Down"}, - "F8"); +// Keep list of valid values in sync with ShortcutHandler +core::EnumListParameter + shortcutModifiers("ShortcutModifiers", + "The combination of modifier keys that triggers " + "special actions in the viewer instead of being " + "sent to the remote session. Possible values are a " + "combination of Ctrl, Shift, Alt, and Super.", + {"Ctrl", "Shift", "Alt", "Super", + "Win", "Option", "Cmd"}, + {"Ctrl", "Alt"}); core::BoolParameter fullscreenSystemKeys("FullscreenSystemKeys", @@ -282,8 +284,9 @@ static core::VoidParameter* parameterArray[] = { &sendPrimary, &setPrimary, #endif - &menuKey, - &fullscreenSystemKeys + &fullscreenSystemKeys, + /* Keyboard shortcuts */ + &shortcutModifiers, }; static core::VoidParameter* readOnlyParameterArray[] = { diff --git a/vncviewer/parameters.h b/vncviewer/parameters.h index 4dafdaa0..4dc30db6 100644 --- a/vncviewer/parameters.h +++ b/vncviewer/parameters.h @@ -73,7 +73,7 @@ extern core::BoolParameter sendPrimary; extern core::StringParameter display; #endif -extern core::EnumParameter menuKey; +extern core::EnumListParameter shortcutModifiers; extern core::BoolParameter fullscreenSystemKeys; extern core::BoolParameter alertOnFatalError; diff --git a/vncviewer/vncviewer.cxx b/vncviewer/vncviewer.cxx index 29cf4ca0..382119b8 100644 --- a/vncviewer/vncviewer.cxx +++ b/vncviewer/vncviewer.cxx @@ -100,7 +100,7 @@ static const char *about_text() // encodings, so we need to make sure we get a fresh string every // time. snprintf(buffer, sizeof(buffer), - _("TigerVNC viewer v%s\n" + _("TigerVNC v%s\n" "Built on: %s\n" "Copyright (C) 1999-%d TigerVNC team and many others (see README.rst)\n" "See https://www.tigervnc.org for information on TigerVNC."), @@ -170,7 +170,7 @@ bool should_disconnect() void about_vncviewer() { - fl_message_title(_("About TigerVNC Viewer")); + fl_message_title(_("About TigerVNC")); fl_message("%s", about_text()); } @@ -241,7 +241,7 @@ static void new_connection_cb(Fl_Widget* /*widget*/, void* /*data*/) pid = fork(); if (pid == -1) { - vlog.error(_("Error starting new TigerVNC Viewer: %s"), strerror(errno)); + vlog.error(_("Error starting new connection: %s"), strerror(errno)); return; } @@ -253,7 +253,7 @@ static void new_connection_cb(Fl_Widget* /*widget*/, void* /*data*/) execvp(argv[0], (char * const *)argv); - vlog.error(_("Error starting new TigerVNC Viewer: %s"), strerror(errno)); + vlog.error(_("Error starting new connection: %s"), strerror(errno)); _exit(1); } #endif @@ -262,7 +262,7 @@ static void CleanupSignalHandler(int sig) { // CleanupSignalHandler allows C++ object cleanup to happen because it calls // exit() rather than the default which is to abort. - vlog.info(_("Termination signal %d has been received. TigerVNC viewer will now exit."), sig); + vlog.info(_("Termination signal %d has been received. TigerVNC will now exit."), sig); exit(1); } @@ -387,7 +387,7 @@ static void init_fltk() fl_message_hotspot(false); // Avoid empty titles for popups - fl_message_title_default(_("TigerVNC viewer")); + fl_message_title_default("TigerVNC"); // FLTK exposes these so that we can translate them. fl_no = _("No"); @@ -464,7 +464,7 @@ static void usage(const char *programName) fprintf(stderr, _("\n" "Options:\n\n" " -display Xdisplay - Specifies the X display for the viewer window\n" - " -geometry geometry - Initial position of the main VNC viewer window. See the\n" + " -geometry geometry - Initial position of the main TigerVNC window. See the\n" " man page for details.\n")); #endif @@ -607,7 +607,9 @@ createTunnel(const char *gatewayHost, const char *remoteHost, cmd2 = strdup(cmd); while ((percent = strchr(cmd2, '%')) != nullptr) *percent = '$'; - system(cmd2); + int res = system(cmd2); + if (res != 0) + fprintf(stderr, "Failed to create tunnel: '%s' returned %d\n", cmd2, res); free(cmd2); } diff --git a/vncviewer/vncviewer.desktop.in.in b/vncviewer/vncviewer.desktop.in.in index 1a91755c..705845d9 100644 --- a/vncviewer/vncviewer.desktop.in.in +++ b/vncviewer/vncviewer.desktop.in.in @@ -1,5 +1,5 @@ [Desktop Entry] -Name=TigerVNC viewer +Name=TigerVNC GenericName=Remote desktop viewer Comment=Connect to VNC server and display remote desktop Exec=@CMAKE_INSTALL_FULL_BINDIR@/vncviewer diff --git a/vncviewer/vncviewer.man b/vncviewer/vncviewer.man index 208858f9..7597c1a6 100644 --- a/vncviewer/vncviewer.man +++ b/vncviewer/vncviewer.man @@ -77,24 +77,40 @@ safely. Automatic selection can be turned off by setting the \fBAutoSelect\fP parameter to false, or from the options dialog. -.SH POPUP MENU -The viewer has a popup menu containing entries which perform various actions. -It is usually brought up by pressing F8, but this can be configured with the -MenuKey parameter. Actions which the popup menu can perform include: -.RS 2 -.IP * 2 -switching in and out of full-screen mode -.IP * -quitting the viewer -.IP * -generating key events, e.g. sending ctrl-alt-del -.IP * -accessing the options dialog and various other dialogs -.RE -.PP -By default, key presses in the popup menu get sent to the VNC server and -dismiss the popup. So to get an F8 through to the VNC server simply press it -twice. +.SH KEYBOARD SHORTCUTS + +The viewer can be controlled using certain key combinations, invoking +special actions instead of passing the keyboard events on to the remote +session. By default pressing Ctrl+Alt and something else will be +interpreted as a keyboard shortcut for the viewer, but this can be +changed witht the \fBShortcutModifiers\fP parameter. + +The possible keyboard shortcuts are: + +.TP +Ctrl+Alt +Releases control of the keyboard and allows system keys to be used +locally. +. +.TP +Ctrl+Alt+G +Grabs control of the keyboard and allows system keys (like Alt+Tab) to +be sent to the remote session. +. +.TP +Ctrl+Alt+Enter +Toggles full-screen mode. +. +.TP +Ctrl+Alt+M +Opens a popup menu that can perform various extra actions, such as +quitting the viewer or opening the options dialog. +. +.TP +Ctrl+Alt+Space +Temporarily bypasses the keyboard shortcuts, allowing the same key +combinations to be sent to the remote session. +. .SH FULL-SCREEN MODE A full-screen mode is supported. This is particularly useful when connecting @@ -154,7 +170,7 @@ the SetDesktopSize message then the screen will retain the original size. . .TP .B \-display \fIXdisplay\fP -Specifies the X display on which the VNC viewer window should appear. +Specifies the X display on which the TigerVNC window should appear. . .TP .B \-DotWhenNoCursor (DEPRECATED) @@ -196,12 +212,12 @@ The default is "1". . .TP .B \-FullscreenSystemKeys -Pass special keys (like Alt+Tab) directly to the server when in full-screen -mode. +Automatically grab all input from the keyboard when entering full-screen +and pass special keys (like Alt+Tab) directly to the server. . .TP .B \-geometry \fIgeometry\fP -Initial position of the main VNC viewer window. The format is +Initial position of the main TigerVNC window. The format is .B \fIwidth\fPx\fIheight\fP+\fIxoffset\fP+\fIyoffset\fP , where `+' signs can be replaced with `\-' signs to specify offsets from the right and/or from the bottom of the screen. Offsets are optional and the @@ -246,13 +262,6 @@ Default is \fB262144\fP. Maximize viewer window. . .TP -.B \-MenuKey \fIkeysym-name\fP -This option specifies the key which brings up the popup menu, or None to -disable the menu. The currently supported list is: F1, F2, F3, F4, F5, -F6, F7, F8, F9, F10, F11, F12, Pause, Scroll_Lock, Escape, Insert, -Delete, Home, Page_Up, Page_Down). Default is F8. -. -.TP .B \-NoJpeg Disable lossy JPEG compression in Tight encoding. Default is off. . @@ -319,6 +328,13 @@ normally closed. This option requests that they be left open, allowing you to share the desktop with someone already using it. . .TP +.B \-ShortcutModifiers \fIkeys\fP +The combination of modifier keys that triggers special actions in the +viewer instead of being sent to the remote session. Possible values are +a combination of \fBCtrl\fP, \fBShift\fP, \fBAlt\fP, and \fBSuper\fP. +Default is \fBCtrl,Alt\fP. +. +.TP .B \-UseIPv4 Use IPv4 for incoming and outgoing connections. Default is on. . diff --git a/vncviewer/vncviewer.rc.in b/vncviewer/vncviewer.rc.in index 43e44da3..375da7af 100644 --- a/vncviewer/vncviewer.rc.in +++ b/vncviewer/vncviewer.rc.in @@ -42,9 +42,9 @@ BEGIN BLOCK "080904b0" BEGIN VALUE "Comments", "\0" - VALUE "CompanyName", "TigerVNC project\0" - VALUE "FileDescription", "TigerVNC client\0" - VALUE "ProductName", "TigerVNC client\0" + VALUE "CompanyName", "TigerVNC team\0" + VALUE "FileDescription", "TigerVNC\0" + VALUE "ProductName", "TigerVNC\0" VALUE "FileVersion", "@RCVERSION@\0" VALUE "InternalName", "vncviewer\0" VALUE "LegalCopyright", "Copyright (C) 1999-2025 TigerVNC team and many others (see README.rst)\0" diff --git a/vncviewer/win32.c b/vncviewer/win32.c index b0a3813c..c649f783 100644 --- a/vncviewer/win32.c +++ b/vncviewer/win32.c @@ -27,39 +27,50 @@ static HANDLE thread; static DWORD thread_id; static HHOOK hook = 0; +static BYTE kbd_state[256]; static HWND target_wnd = 0; -static int is_system_hotkey(int vkCode) { - switch (vkCode) { - case VK_LWIN: - case VK_RWIN: - case VK_SNAPSHOT: - return 1; - case VK_TAB: - if (GetAsyncKeyState(VK_MENU) & 0x8000) - return 1; - break; - case VK_ESCAPE: - if (GetAsyncKeyState(VK_MENU) & 0x8000) - return 1; - if (GetAsyncKeyState(VK_CONTROL) & 0x8000) - return 1; - break; - } - return 0; -} - static LRESULT CALLBACK keyboard_hook(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { KBDLLHOOKSTRUCT* msgInfo = (KBDLLHOOKSTRUCT*)lParam; - // Grabbing everything seems to mess up some keyboard state that - // FLTK relies on, so just grab the keys that we normally cannot. - if (is_system_hotkey(msgInfo->vkCode)) { - PostMessage(target_wnd, wParam, msgInfo->vkCode, - (msgInfo->scanCode & 0xff) << 16 | - (msgInfo->flags & 0xff) << 24); + BYTE vkey; + BYTE scanCode; + BYTE flags; + + vkey = msgInfo->vkCode; + scanCode = msgInfo->scanCode; + flags = msgInfo->flags; + + // We get the low level vkeys here, but the application code + // expects this to have been translated to the generic ones + switch (vkey) { + case VK_LSHIFT: + case VK_RSHIFT: + vkey = VK_SHIFT; + // The extended bit is also always missing for right shift + flags &= ~0x01; + break; + case VK_LCONTROL: + case VK_RCONTROL: + vkey = VK_CONTROL; + break; + case VK_LMENU: + case VK_RMENU: + vkey = VK_MENU; + break; + } + + // If the key was pressed before the grab was activated, then we + // need to avoid intercepting the release event or Windows will get + // confused about the state of the key + if (((wParam == WM_KEYUP) || (wParam == WM_SYSKEYUP)) && + (kbd_state[msgInfo->vkCode] & 0x80)) { + kbd_state[msgInfo->vkCode] &= ~0x80; + } else { + PostMessage(target_wnd, wParam, vkey, + scanCode << 16 | flags << 24); return 1; } } @@ -76,6 +87,9 @@ static DWORD WINAPI keyboard_thread(LPVOID data) // Make sure a message queue is created PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE | PM_NOYIELD); + // We need to know which keys are currently pressed + GetKeyboardState(kbd_state); + hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_hook, GetModuleHandle(0), 0); // If something goes wrong then there is not much we can do. // Just sit around and wait for WM_QUIT... |