diff options
Diffstat (limited to 'vncviewer')
48 files changed, 2983 insertions, 1723 deletions
diff --git a/vncviewer/BaseTouchHandler.cxx b/vncviewer/BaseTouchHandler.cxx index 6552634b..3100f86b 100644 --- a/vncviewer/BaseTouchHandler.cxx +++ b/vncviewer/BaseTouchHandler.cxx @@ -24,9 +24,10 @@ #include <stdlib.h> #include <math.h> +#include <core/time.h> + #define XK_MISCELLANY #include <rfb/keysymdef.h> -#include <rfb/util.h> #include "GestureHandler.h" #include "BaseTouchHandler.h" @@ -172,7 +173,7 @@ void BaseTouchHandler::handleTapEvent(const GestureEvent& ev, // If the user quickly taps multiple times we assume they meant to // hit the same spot, so slightly adjust coordinates - if ((rfb::msSince(&lastTapTime) < DOUBLE_TAP_TIMEOUT) && + if ((core::msSince(&lastTapTime) < DOUBLE_TAP_TIMEOUT) && (firstDoubleTapEvent.type == ev.type)) { double dx = firstDoubleTapEvent.eventX - ev.eventX; diff --git a/vncviewer/CConn.cxx b/vncviewer/CConn.cxx index f8e80429..da99c8f1 100644 --- a/vncviewer/CConn.cxx +++ b/vncviewer/CConn.cxx @@ -27,17 +27,21 @@ #include <unistd.h> #endif -#include <rdr/Exception.h> +#include <core/LogWriter.h> +#include <core/Timer.h> +#include <core/string.h> +#include <core/time.h> + +#include <rdr/FdInStream.h> +#include <rdr/FdOutStream.h> #include <rfb/CMsgWriter.h> #include <rfb/CSecurity.h> #include <rfb/Exception.h> -#include <rfb/Hostname.h> -#include <rfb/LogWriter.h> #include <rfb/Security.h> -#include <rfb/screenTypes.h> #include <rfb/fenceTypes.h> -#include <rfb/Timer.h> +#include <rfb/screenTypes.h> + #include <network/TcpSocket.h> #ifndef WIN32 #include <network/UnixSocket.h> @@ -58,29 +62,27 @@ #include "win32.h" #endif -using namespace rfb; - -static rfb::LogWriter vlog("CConn"); +static core::LogWriter vlog("CConn"); // 8 colours (1 bit per component) -static const PixelFormat verylowColourPF(8, 3,false, true, - 1, 1, 1, 2, 1, 0); +static const rfb::PixelFormat verylowColourPF(8, 3,false, true, + 1, 1, 1, 2, 1, 0); // 64 colours (2 bits per component) -static const PixelFormat lowColourPF(8, 6, false, true, - 3, 3, 3, 4, 2, 0); +static const rfb::PixelFormat lowColourPF(8, 6, false, true, + 3, 3, 3, 4, 2, 0); // 256 colours (2-3 bits per component) -static const PixelFormat mediumColourPF(8, 8, false, true, - 7, 7, 3, 5, 2, 0); +static const rfb::PixelFormat mediumColourPF(8, 8, false, true, + 7, 7, 3, 5, 2, 0); // Time new bandwidth estimates are weighted against (in ms) static const unsigned bpsEstimateWindow = 1000; -CConn::CConn(const char* vncServerName, network::Socket* socket=nullptr) - : serverPort(0), desktop(nullptr), updateCount(0), pixelCount(0), +CConn::CConn() + : serverPort(0), sock(nullptr), desktop(nullptr), + updateCount(0), pixelCount(0), lastServerEncoding((unsigned int)-1), bpsEstimate(20000000) { setShared(::shared); - sock = socket; supportsLocalCursor = true; supportsCursorPosition = true; @@ -93,6 +95,61 @@ CConn::CConn(const char* vncServerName, network::Socket* socket=nullptr) if (!noJpeg) setQualityLevel(::qualityLevel); + OptionsDialog::addCallback(handleOptions, this); +} + +CConn::~CConn() +{ + close(); + + OptionsDialog::removeCallback(handleOptions); + Fl::remove_timeout(handleUpdateTimeout, this); + + if (desktop) + delete desktop; + + if (sock) { + struct timeval now; + + 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 + } + + Fl::remove_fd(sock->getFd()); + + delete sock; + } +} + +void CConn::connect(const char* vncServerName, network::Socket* socket) +{ + sock = socket; if(sock == nullptr) { try { #ifndef WIN32 @@ -103,7 +160,7 @@ CConn::CConn(const char* vncServerName, network::Socket* socket=nullptr) } else #endif { - getHostAndPort(vncServerName, &serverHost, &serverPort); + network::getHostAndPort(vncServerName, &serverHost, &serverPort); sock = new network::TcpSocket(serverHost.c_str(), serverPort); vlog.info(_("Connected to host %s port %d"), @@ -123,23 +180,6 @@ CConn::CConn(const char* vncServerName, network::Socket* socket=nullptr) setStreams(&sock->inStream(), &sock->outStream()); initialiseProtocol(); - - OptionsDialog::addCallback(handleOptions, this); -} - -CConn::~CConn() -{ - close(); - - OptionsDialog::removeCallback(handleOptions); - Fl::remove_timeout(handleUpdateTimeout, this); - - if (desktop) - delete desktop; - - if (sock) - Fl::remove_fd(sock->getFd()); - delete sock; } std::string CConn::connectionInfo() @@ -148,46 +188,41 @@ std::string CConn::connectionInfo() char pfStr[100]; - infoText += format(_("Desktop name: %.80s"), server.name()); + infoText += core::format(_("Desktop name: %.80s"), server.name()); infoText += "\n"; - infoText += format(_("Host: %.80s port: %d"), - serverHost.c_str(), serverPort); + infoText += core::format(_("Host: %.80s port: %d"), + serverHost.c_str(), serverPort); infoText += "\n"; - infoText += format(_("Size: %d x %d"), - server.width(), server.height()); + infoText += core::format(_("Size: %d x %d"), + server.width(), server.height()); infoText += "\n"; // TRANSLATORS: Will be filled in with a string describing the // protocol pixel format in a fairly language neutral way server.pf().print(pfStr, 100); - infoText += format(_("Pixel format: %s"), pfStr); + infoText += core::format(_("Pixel format: %s"), pfStr); infoText += "\n"; - // TRANSLATORS: Similar to the earlier "Pixel format" string - serverPF.print(pfStr, 100); - infoText += format(_("(server default %s)"), pfStr); + infoText += core::format(_("Requested encoding: %s"), + rfb::encodingName(getPreferredEncoding())); infoText += "\n"; - infoText += format(_("Requested encoding: %s"), - encodingName(getPreferredEncoding())); + infoText += core::format(_("Last used encoding: %s"), + rfb::encodingName(lastServerEncoding)); infoText += "\n"; - infoText += format(_("Last used encoding: %s"), - encodingName(lastServerEncoding)); + infoText += core::format(_("Line speed estimate: %d kbit/s"), + (int)(bpsEstimate / 1000)); infoText += "\n"; - infoText += format(_("Line speed estimate: %d kbit/s"), - (int)(bpsEstimate / 1000)); + infoText += core::format(_("Protocol version: %d.%d"), + server.majorVersion, server.minorVersion); infoText += "\n"; - infoText += format(_("Protocol version: %d.%d"), - server.majorVersion, server.minorVersion); - infoText += "\n"; - - infoText += format(_("Security method: %s"), - secTypeName(csecurity->getType())); + infoText += core::format(_("Security method: %s"), + rfb::secTypeName(csecurity->getType())); infoText += "\n"; return infoText; @@ -236,7 +271,7 @@ void CConn::socketEvent(FL_SOCKET fd, void *data) // Make sure that the FLTK handling and the timers gets some CPU // time in case of back to back messages Fl::check(); - Timer::checkTimeouts(); + core::Timer::checkTimeouts(); // Also check if we need to stop reading and terminate if (should_disconnect()) @@ -282,7 +317,7 @@ void CConn::resetPassword() ////////////////////// CConnection callback methods ////////////////////// -bool CConn::showMsgBox(MsgBoxFlags flags, const char *title, +bool CConn::showMsgBox(rfb::MsgBoxFlags flags, const char *title, const char *text) { return dlg.showMsgBox(flags, title, text); @@ -304,46 +339,29 @@ 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 = encodingNum(::preferredEncoding); - if (encNum != -1) - setPreferredEncoding(encNum); -} - -// setDesktopSize() is called when the desktop size changes (including when -// it is set initially). -void CConn::setDesktopSize(int w, int h) -{ - CConnection::setDesktopSize(w,h); - resizeFramebuffer(); } -// setExtendedDesktopSize() is a more advanced version of setDesktopSize() void CConn::setExtendedDesktopSize(unsigned reason, unsigned result, - int w, int h, const rfb::ScreenSet& layout) + int w, int h, + const rfb::ScreenSet& layout) { CConnection::setExtendedDesktopSize(reason, result, w, h, layout); - if ((reason == reasonClient) && (result != resultSuccess)) { - vlog.error(_("SetDesktopSize failed: %d"), result); - return; - } - - resizeFramebuffer(); + if (reason == rfb::reasonClient) + desktop->setDesktopSizeDone(result); } // setName() is called when the desktop name changes void CConn::setName(const char* name) { CConnection::setName(name); - desktop->setName(name); + desktop->updateCaption(); } // framebufferUpdateStart() is called at the beginning of an update. @@ -396,28 +414,25 @@ 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(); } -bool CConn::dataRect(const Rect& r, int encoding) +bool CConn::dataRect(const core::Rect& r, int encoding) { bool ret; - if (encoding != encodingCopyRect) + if (encoding != rfb::encodingCopyRect) lastServerEncoding = encoding; ret = CConnection::dataRect(r, encoding); @@ -428,28 +443,17 @@ bool CConn::dataRect(const Rect& r, int encoding) return ret; } -void CConn::setCursor(int width, int height, const Point& hotspot, +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); -void CConn::setCursorPos(const Point& pos) -{ - desktop->setCursorPos(pos); + desktop->setCursor(); } -void CConn::fence(uint32_t flags, unsigned len, const uint8_t data[]) +void CConn::setCursorPos(const core::Point& pos) { - CMsgHandler::fence(flags, len, data); - - if (flags & fenceFlagRequest) { - // We handle everything synchronously so we trivially honor these modes - flags = flags & (fenceFlagBlockBefore | fenceFlagBlockAfter); - - writer()->writeFence(flags, len, data); - return; - } + desktop->setCursorPos(pos); } void CConn::setLEDState(unsigned int state) @@ -482,44 +486,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() { - bool newFullColour = fullColour; - int newQualityLevel = ::qualityLevel; + if (customCompressLevel) + setCompressLevel(::compressLevel); + else + setCompressLevel(-1); +} - // Always use Tight - setPreferredEncoding(encodingTight); +void CConn::updateQualityLevel() +{ + int newQualityLevel; + + 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 @@ -530,28 +549,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() -{ - 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) @@ -562,37 +576,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 = encodingNum(::preferredEncoding); - - 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 a7b9afda..bc30d9b7 100644 --- a/vncviewer/CConn.h +++ b/vncviewer/CConn.h @@ -23,7 +23,7 @@ #include <FL/Fl.H> #include <rfb/CConnection.h> -#include <rdr/FdInStream.h> + #include "UserDialog.h" namespace network { class Socket; } @@ -33,15 +33,19 @@ class DesktopWindow; class CConn : public rfb::CConnection { public: - CConn(const char* vncServerName, network::Socket* sock); + CConn(); ~CConn(); + void connect(const char* vncServerName, network::Socket* sock=nullptr); + std::string connectionInfo(); unsigned getUpdateCount(); unsigned getPixelCount(); unsigned getPosition(); +protected: + // Callback when socket is ready (or broken) static void socketEvent(FL_SOCKET fd, void *data); @@ -57,28 +61,21 @@ public: void initDone() override; - void setDesktopSize(int w, int h) override; void setExtendedDesktopSize(unsigned reason, unsigned result, int w, int h, const rfb::ScreenSet& layout) override; void setName(const char* name) override; - void setColourMapEntries(int firstColour, int nColours, - uint16_t* rgbs) override; - void bell() override; void framebufferUpdateStart() override; void framebufferUpdateEnd() override; - bool dataRect(const rfb::Rect& r, int encoding) override; + bool dataRect(const core::Rect& r, int encoding) override; - void setCursor(int width, int height, const rfb::Point& hotspot, + void setCursor(int width, int height, const core::Point& hotspot, const uint8_t* data) override; - void setCursorPos(const rfb::Point& pos) override; - - void fence(uint32_t flags, unsigned len, - const uint8_t data[]) override; + void setCursorPos(const core::Point& pos) override; void setLEDState(unsigned int state) override; @@ -90,7 +87,9 @@ private: void resizeFramebuffer() override; - void autoSelectFormatAndEncoding(); + void updateEncoding(); + void updateCompressLevel(); + void updateQualityLevel(); void updatePixelFormat(); static void handleOptions(void *data); @@ -107,7 +106,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 72904b25..1ca6675d 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 @@ -49,9 +49,10 @@ else() endif() target_include_directories(vncviewer SYSTEM PUBLIC ${FLTK_INCLUDE_DIR}) -target_include_directories(vncviewer SYSTEM PUBLIC ${GETTEXT_INCLUDE_DIR}) +target_include_directories(vncviewer SYSTEM PUBLIC ${Intl_INCLUDE_DIR}) target_include_directories(vncviewer PUBLIC ${CMAKE_SOURCE_DIR}/common) -target_link_libraries(vncviewer rfb network rdr os ${FLTK_LIBRARIES} ${GETTEXT_LIBRARIES}) +target_link_libraries(vncviewer core rfb network rdr) +target_link_libraries(vncviewer ${FLTK_LIBRARIES} ${Intl_LIBRARIES}) if(WIN32) target_link_libraries(vncviewer msimg32) diff --git a/vncviewer/DesktopWindow.cxx b/vncviewer/DesktopWindow.cxx index 6dbd7d5f..831bb107 100644 --- a/vncviewer/DesktopWindow.cxx +++ b/vncviewer/DesktopWindow.cxx @@ -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 @@ -26,11 +26,16 @@ #include <assert.h> #include <stdio.h> #include <string.h> +#include <time.h> +#include <unistd.h> #include <sys/time.h> -#include <rfb/LogWriter.h> +#include <core/LogWriter.h> +#include <core/string.h> +#include <core/time.h> + #include <rfb/CMsgWriter.h> -#include <rfb/util.h> +#include <rfb/ScreenSet.h> #include "DesktopWindow.h" #include "OptionsDialog.h" @@ -70,21 +75,21 @@ static int edge_scroll_size_y = 96; // default: roughly 60 fps for smooth motion #define EDGE_SCROLL_SECONDS_PER_FRAME 0.016666 -using namespace rfb; +// Time before we show an overlay tip again +const time_t OVERLAY_REPEAT_TIMEOUT = 600; -static rfb::LogWriter vlog("DesktopWindow"); +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), delayedDesktopSize(false), - keyboardGrabbed(false), mouseGrabbed(false), + delayedFullscreen(false), sentDesktopSize(false), + pendingRemoteResize(false), lastResize({0, 0}), + keyboardGrabbed(false), mouseGrabbed(false), regrabOnFocus(false), statsLastUpdates(0), statsLastPixels(0), statsLastPosition(0), statsGraph(nullptr) { @@ -95,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); @@ -108,7 +113,7 @@ DesktopWindow::DesktopWindow(int w, int h, const char *name, callback(handleClose, this); - setName(name); + updateCaption(); OptionsDialog::addCallback(handleOptions, this); @@ -174,7 +179,7 @@ DesktopWindow::DesktopWindow(int w, int h, const char *name, #ifdef __APPLE__ // On OS X we can do the maximize thing properly before the // window is showned. Other platforms handled further down... - if (maximize) { + if (::maximize) { int dummy; Fl::screen_work_area(dummy, dummy, w, h, geom_x, geom_y); } @@ -199,6 +204,11 @@ DesktopWindow::DesktopWindow(int w, int h, const char *name, show(); +#ifdef __APPLE__ + // FLTK does its own full screen, so disable the system one + cocoa_prevent_native_fullscreen(this); +#endif + // Full screen events are not sent out for a hidden window, // so send a fake one here to set up things properly. if (fullscreen_active()) @@ -208,7 +218,7 @@ DesktopWindow::DesktopWindow(int w, int h, const char *name, // maximized property on Windows and X11 before showing the window. // See STR #2083 and STR #2178 #ifndef __APPLE__ - if (maximize) { + if (::maximize) { maximizeWindow(); } #endif @@ -216,22 +226,22 @@ DesktopWindow::DesktopWindow(int w, int h, const char *name, // Adjust layout now that we're visible and know our final size repositionWidgets(); - if (delayedFullscreen) { - // Hack: Fullscreen requests may be ignored, so we need a timeout for - // when we should stop waiting. We also really need to wait for the - // resize, which can come after the fullscreen event. - Fl::add_timeout(0.5, handleFullscreenTimeout, this); - fullscreen_on(); - } - // Throughput graph for debugging - if (vlog.getLevel() >= LogWriter::LEVEL_DEBUG) { + if (vlog.getLevel() >= core::LogWriter::LEVEL_DEBUG) { memset(&stats, 0, sizeof(stats)); 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 @@ -252,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; @@ -285,13 +296,50 @@ const rfb::PixelFormat &DesktopWindow::getPreferredPF() } -void DesktopWindow::setName(const char *name) +void DesktopWindow::updateCaption() { - char windowNameStr[256]; + const size_t maxLen = 100; + std::string windowName; + const char *labelFormat; + size_t maxNameSize; + std::string name; + + // 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 = maxLen - strlen(labelFormat) + 2; + + name = cc->server.name(); + + 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" + + windowName = core::format(labelFormat, name.c_str()); - snprintf(windowNameStr, 256, "%.240s - TigerVNC", name); +#pragma GCC diagnostic pop - copy_label(windowNameStr); + copy_label(windowName.c_str()); } @@ -301,15 +349,8 @@ void DesktopWindow::setName(const char *name) void DesktopWindow::updateWindow() { if (firstUpdate) { - if (cc->server.supportsSetDesktopSize) { - // Hack: Wait until we're in the proper mode and position until - // resizing things, otherwise we might send the wrong thing. - if (delayedFullscreen) - delayedDesktopSize = true; - else - handleDesktopSize(); - } firstUpdate = false; + remoteResize(); } viewport->updateWindow(); @@ -372,15 +413,26 @@ void DesktopWindow::resizeFramebuffer(int new_w, int new_h) } -void DesktopWindow::setCursor(int width, int height, - const rfb::Point& hotspot, - const uint8_t* data) +void DesktopWindow::setDesktopSizeDone(unsigned result) +{ + pendingRemoteResize = false; + + if (result != 0) + return; + + // We might have resized again whilst waiting for the previous + // request, so check if we are in sync + remoteResize(); +} + + +void DesktopWindow::setCursor() { - viewport->setCursor(width, height, hotspot, data); + viewport->setCursor(); } -void DesktopWindow::setCursorPos(const rfb::Point& pos) +void DesktopWindow::setCursorPos(const core::Point& pos) { if (!mouseGrabbed) { // Do nothing if we do not have the mouse captured. @@ -498,16 +550,19 @@ 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 if (fullscreen_active()) { assert(Fl::screen_count() >= 1); - rfb::Rect windowRect, screenRect; + core::Rect windowRect, screenRect; windowRect.setXYWH(x(), y(), w(), h()); bool foundEnclosedScreen = false; @@ -538,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); } } @@ -659,51 +716,59 @@ void DesktopWindow::resize(int x, int y, int w, int h) Fl_Window::resize(x, y, w, h); if (resizing) { - // Try to get the remote size to match our window size, provided - // the following conditions are true: - // - // a) The user has this feature turned on - // b) The server supports it - // c) We're not still waiting for a chance to handle DesktopSize - // d) We're not still waiting for startup fullscreen to kick in - // - if (not firstUpdate and not delayedFullscreen and - ::remoteResize and cc->server.supportsSetDesktopSize) { - // We delay updating the remote desktop as we tend to get a flood - // of resize events as the user is dragging the window. - Fl::remove_timeout(handleResizeTimeout, this); - Fl::add_timeout(0.5, handleResizeTimeout, this); - } + remoteResize(); 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); - if (strcmp((const char*)menuKey, "") != 0) { - self->setOverlay(_("Press %s to open the context menu"), - (const char*)menuKey); + // 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; @@ -717,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 @@ -733,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; @@ -746,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; @@ -782,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 = 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); } @@ -828,10 +901,44 @@ 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 + if (delayedFullscreen && fullscreen_active()) { + Fl::remove_timeout(handleFullscreenTimeout, this); + delayedFullscreen = false; + remoteResize(); + } break; @@ -892,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. @@ -904,15 +1022,33 @@ 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: + // In this particular place, FL_SHOW means an actual MapNotify, + // which means we can continue enabling initial fullscreen. + if (dw->delayedFullscreen) { + // Hack: Fullscreen requests may be ignored, so we need a + // timeout for when we should stop waiting. We also really need + // to wait for the resize, which can come after the fullscreen + // event. + Fl::add_timeout(0.5, handleFullscreenTimeout, dw); + dw->fullscreen_on(); } break; @@ -941,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); } @@ -958,8 +1086,8 @@ int DesktopWindow::fltkHandle(int event) void DesktopWindow::fullscreen_on() { - bool allMonitors = !strcasecmp(fullScreenMode, "all"); - bool selectedMonitors = !strcasecmp(fullScreenMode, "selected"); + bool allMonitors = fullScreenMode == "all"; + bool selectedMonitors = fullScreenMode == "selected"; int top, bottom, left, right; if (not selectedMonitors and not allMonitors) { @@ -972,7 +1100,7 @@ void DesktopWindow::fullscreen_on() std::set<int> monitors; if (selectedMonitors and not allMonitors) { - std::set<int> selected = fullScreenSelectedMonitors.getParam(); + std::set<int> selected = fullScreenSelectedMonitors.getMonitors(); monitors.insert(selected.begin(), selected.end()); } else { for (int idx = 0; idx < Fl::screen_count(); idx++) @@ -1024,40 +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; - 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 (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; @@ -1072,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 @@ -1139,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 } @@ -1202,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() { @@ -1252,32 +1341,13 @@ void DesktopWindow::maximizeWindow() } -void DesktopWindow::handleDesktopSize() -{ - if (strcmp(desktopSize, "") != 0) { - int width, height; - - // An explicit size has been requested - - if (sscanf(desktopSize, "%dx%d", &width, &height) != 2) - return; - - remoteResize(width, height); - } else if (::remoteResize) { - // No explicit size, but remote resizing is on so make sure it - // matches whatever size the window ended up being - remoteResize(w(), h()); - } -} - - void DesktopWindow::handleResizeTimeout(void *data) { DesktopWindow *self = (DesktopWindow *)data; assert(self); - self->remoteResize(self->w(), self->h()); + self->remoteResize(); } @@ -1292,10 +1362,47 @@ void DesktopWindow::reconfigureFullscreen(void* /*data*/) } -void DesktopWindow::remoteResize(int width, int height) +void DesktopWindow::remoteResize() { - ScreenSet layout; - ScreenSet::const_iterator iter; + int width, height; + rfb::ScreenSet layout; + rfb::ScreenSet::const_iterator iter; + + if (viewOnly) + return; + + if (!::remoteResize) + return; + if (!cc->server.supportsSetDesktopSize) + return; + + // Don't pester the server with a resize until we have our final size + if (delayedFullscreen) + return; + + // Rate limit to one pending resize at a time + if (pendingRemoteResize) + return; + + // And no more than once every 100ms + if (core::msSince(&lastResize) < 100) { + Fl::remove_timeout(handleResizeTimeout, this); + Fl::add_timeout((100.0 - core::msSince(&lastResize)) / 1000.0, + handleResizeTimeout, this); + return; + } + + width = w(); + height = h(); + + if (!sentDesktopSize && (strcmp(desktopSize, "") != 0)) { + // An explicit size has been requested + + if (sscanf(desktopSize, "%dx%d", &width, &height) != 2) + return; + + sentDesktopSize = true; + } if (!fullscreen_active() || (width > w()) || (height > h())) { // In windowed mode (or the framebuffer is so large that we need @@ -1331,7 +1438,7 @@ void DesktopWindow::remoteResize(int width, int height) } else { uint32_t id; int sx, sy, sw, sh; - rfb::Rect viewport_rect, screen_rect; + core::Rect viewport_rect, screen_rect; // In full screen we report all screens that are fully covered. @@ -1415,6 +1522,8 @@ void DesktopWindow::remoteResize(int width, int height) vlog.debug("%s", buffer); } + pendingRemoteResize = true; + gettimeofday(&lastResize, nullptr); cc->writer()->writeSetDesktopSize(width, height, layout); } @@ -1515,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) @@ -1535,11 +1639,7 @@ void DesktopWindow::handleFullscreenTimeout(void *data) assert(self); self->delayedFullscreen = false; - - if (self->delayedDesktopSize) { - self->handleDesktopSize(); - self->delayedDesktopSize = false; - } + self->remoteResize(); } void DesktopWindow::scrollTo(int x, int y) @@ -1644,7 +1744,7 @@ void DesktopWindow::handleStatsTimeout(void *data) updates = self->cc->getUpdateCount(); pixels = self->cc->getPixelCount(); pos = self->cc->getPosition(); - elapsed = msSince(&self->statsLastTime); + elapsed = core::msSince(&self->statsLastTime); if (elapsed < 1) elapsed = 1; @@ -1719,11 +1819,11 @@ void DesktopWindow::handleStatsTimeout(void *data) fl_draw(buffer, 5, statsHeight - 5); fl_color(FL_YELLOW); - fl_draw(siPrefix(self->stats[statsCount-1].pps, "pix/s").c_str(), + fl_draw(core::siPrefix(self->stats[statsCount-1].pps, "pix/s").c_str(), 5 + (statsWidth-10)/3, statsHeight - 5); fl_color(FL_RED); - fl_draw(siPrefix(self->stats[statsCount-1].bps * 8, "bps").c_str(), + fl_draw(core::siPrefix(self->stats[statsCount-1].bps * 8, "bps").c_str(), 5 + (statsWidth-10)*2/3, statsHeight - 5); image = surface->image(); diff --git a/vncviewer/DesktopWindow.h b/vncviewer/DesktopWindow.h index ce7960ce..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,12 +20,12 @@ #ifndef __DESKTOPWINDOW_H__ #define __DESKTOPWINDOW_H__ +#include <list> #include <map> +#include <string> #include <sys/time.h> -#include <rfb/Rect.h> - #include <FL/Fl_Window.H> namespace rfb { class ModifiablePixelBuffer; } @@ -39,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) @@ -50,17 +49,19 @@ 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); + // A previous call to writeSetDesktopSize() has completed + void setDesktopSizeDone(unsigned result); + // New image for the locally rendered cursor - void setCursor(int width, int height, const rfb::Point& hotspot, - const uint8_t* data); + void setCursor(); // Server-provided cursor position - void setCursorPos(const rfb::Point& pos); + void setCursorPos(const core::Point& pos); // Change client LED state void setLEDState(unsigned int state); @@ -79,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); @@ -91,20 +97,14 @@ private: bool hasFocus(); - void maybeGrabKeyboard(); - void grabKeyboard(); - void ungrabKeyboard(); void grabPointer(); void ungrabPointer(); - static void handleGrab(void *data); - void maximizeWindow(); - void handleDesktopSize(); static void handleResizeTimeout(void *data); static void reconfigureFullscreen(void *data); - void remoteResize(int width, int height); + void remoteResize(); void repositionWidgets(); @@ -125,17 +125,28 @@ 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; - bool delayedDesktopSize; + bool sentDesktopSize; + + bool pendingRemoteResize; + struct timeval lastResize; bool keyboardGrabbed; bool mouseGrabbed; + bool regrabOnFocus; + struct statsEntry { unsigned ups; unsigned pps; diff --git a/vncviewer/EmulateMB.cxx b/vncviewer/EmulateMB.cxx index 44d92752..89470274 100644 --- a/vncviewer/EmulateMB.cxx +++ b/vncviewer/EmulateMB.cxx @@ -199,7 +199,8 @@ EmulateMB::EmulateMB() { } -void EmulateMB::filterPointerEvent(const rfb::Point& pos, uint16_t buttonMask) +void EmulateMB::filterPointerEvent(const core::Point& pos, + uint16_t buttonMask) { int btstate; int action1, action2; @@ -277,7 +278,7 @@ void EmulateMB::filterPointerEvent(const rfb::Point& pos, uint16_t buttonMask) } } -void EmulateMB::handleTimeout(rfb::Timer *t) +void EmulateMB::handleTimeout(core::Timer* t) { int action1, action2; uint16_t buttonMask; @@ -312,7 +313,8 @@ void EmulateMB::handleTimeout(rfb::Timer *t) state = stateTab[state][4][2]; } -void EmulateMB::sendAction(const rfb::Point& pos, uint16_t buttonMask, int action) +void EmulateMB::sendAction(const core::Point& pos, + uint16_t buttonMask, int action) { assert(action != 0); @@ -332,4 +334,4 @@ int EmulateMB::createButtonMask(uint16_t buttonMask) // Set the left and right buttons according to the action return buttonMask |= emulatedButtonMask; -}
\ No newline at end of file +} diff --git a/vncviewer/EmulateMB.h b/vncviewer/EmulateMB.h index 127c34a4..393655e4 100644 --- a/vncviewer/EmulateMB.h +++ b/vncviewer/EmulateMB.h @@ -19,22 +19,24 @@ #ifndef __EMULATEMB__ #define __EMULATEMB__ -#include <rfb/Timer.h> -#include <rfb/Rect.h> +#include <core/Rect.h> +#include <core/Timer.h> -class EmulateMB : public rfb::Timer::Callback { +class EmulateMB : public core::Timer::Callback { public: EmulateMB(); - void filterPointerEvent(const rfb::Point& pos, uint16_t buttonMask); + void filterPointerEvent(const core::Point& pos, uint16_t buttonMask); protected: - virtual void sendPointerEvent(const rfb::Point& pos, uint16_t buttonMask)=0; + virtual void sendPointerEvent(const core::Point& pos, + uint16_t buttonMask)=0; - void handleTimeout(rfb::Timer *t) override; + void handleTimeout(core::Timer* t) override; private: - void sendAction(const rfb::Point& pos, uint16_t buttonMask, int action); + void sendAction(const core::Point& pos, uint16_t buttonMask, + int action); int createButtonMask(uint16_t buttonMask); @@ -42,8 +44,8 @@ private: int state; uint16_t emulatedButtonMask; uint16_t lastButtonMask; - rfb::Point lastPos, origPos; - rfb::Timer timer; + core::Point lastPos, origPos; + core::Timer timer; }; #endif diff --git a/vncviewer/GestureHandler.cxx b/vncviewer/GestureHandler.cxx index 121ceecf..2ede991b 100644 --- a/vncviewer/GestureHandler.cxx +++ b/vncviewer/GestureHandler.cxx @@ -24,12 +24,12 @@ #include <assert.h> #include <math.h> -#include <rfb/util.h> -#include <rfb/LogWriter.h> +#include <core/LogWriter.h> +#include <core/time.h> #include "GestureHandler.h" -static rfb::LogWriter vlog("GestureHandler"); +static core::LogWriter vlog("GestureHandler"); static const unsigned char GH_NOGESTURE = 0; static const unsigned char GH_ONETAP = 1; @@ -81,7 +81,7 @@ void GestureHandler::handleTouchBegin(int id, double x, double y) // Did it take too long between touches that we should no longer // consider this a single gesture? if ((tracked.size() > 0) && - (rfb::msSince(&tracked.begin()->second.started) > GH_MULTITOUCH_TIMEOUT)) { + (core::msSince(&tracked.begin()->second.started) > GH_MULTITOUCH_TIMEOUT)) { state = GH_NOGESTURE; ignored.insert(id); return; @@ -257,12 +257,12 @@ void GestureHandler::handleTouchEnd(int id) // Waiting for all touches to release? (i.e. some tap) if (waitingRelease) { // Were all touches released at roughly the same time? - if (rfb::msSince(&releaseStart) > GH_MULTITOUCH_TIMEOUT) + if (core::msSince(&releaseStart) > GH_MULTITOUCH_TIMEOUT) state = GH_NOGESTURE; // Did too long time pass between press and release? for (iter = tracked.begin(); iter != tracked.end(); ++iter) { - if (rfb::msSince(&iter->second.started) > GH_TAP_TIMEOUT) { + if (core::msSince(&iter->second.started) > GH_TAP_TIMEOUT) { state = GH_NOGESTURE; break; } @@ -323,7 +323,7 @@ bool GestureHandler::hasDetectedGesture() return true; } -void GestureHandler::handleTimeout(rfb::Timer* t) +void GestureHandler::handleTimeout(core::Timer* t) { if (t == &longpressTimer) longpressTimeout(); diff --git a/vncviewer/GestureHandler.h b/vncviewer/GestureHandler.h index 2b31703a..1c2134c0 100644 --- a/vncviewer/GestureHandler.h +++ b/vncviewer/GestureHandler.h @@ -23,11 +23,11 @@ #include <set> #include <map> -#include <rfb/Timer.h> +#include <core/Timer.h> #include "GestureEvent.h" -class GestureHandler : public rfb::Timer::Callback { +class GestureHandler : public core::Timer::Callback { public: GestureHandler(); virtual ~GestureHandler(); @@ -42,7 +42,7 @@ class GestureHandler : public rfb::Timer::Callback { private: bool hasDetectedGesture(); - void handleTimeout(rfb::Timer* t) override; + void handleTimeout(core::Timer* t) override; void longpressTimeout(); void twoTouchTimeout(); @@ -74,8 +74,8 @@ class GestureHandler : public rfb::Timer::Callback { bool waitingRelease; struct timeval releaseStart; - rfb::Timer longpressTimer; - rfb::Timer twoTouchTimer; + core::Timer longpressTimer; + core::Timer twoTouchTimer; }; #endif // __GESTUREHANDLER_H__ 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 e0a10dc8..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> @@ -35,11 +37,12 @@ const int kVK_RightCommand = 0x36; // And this is still missing const int kVK_Menu = 0x6E; +#include <core/LogWriter.h> + #define XK_LATIN1 #define XK_MISCELLANY #include <rfb/keysymdef.h> #include <rfb/XF86keysym.h> -#include <rfb/LogWriter.h> #include <rfb/ledStates.h> #define NoSymbol 0 @@ -51,9 +54,9 @@ const int kVK_Menu = 0x6E; extern const unsigned short code_map_osx_to_qnum[]; extern const unsigned int code_map_osx_to_qnum_len; -static rfb::LogWriter vlog("KeyboardMacOS"); +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 }, @@ -139,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; @@ -154,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); @@ -176,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; @@ -224,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]) { @@ -250,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: @@ -338,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); @@ -372,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. @@ -383,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 '.': @@ -420,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 1caf4863..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 @@ -31,12 +33,13 @@ #include <FL/Fl.H> +#include <core/LogWriter.h> + #define XK_MISCELLANY #define XK_XKB_KEYS #define XK_KOREAN #include <rfb/keysymdef.h> #include <rfb/XF86keysym.h> -#include <rfb/LogWriter.h> #include <rfb/ledStates.h> #define NoSymbol 0 @@ -50,7 +53,7 @@ // Used to detect fake input (0xaa is not a real key) static const WORD SCAN_FAKE = 0xaa; -static rfb::LogWriter vlog("KeyboardWin32"); +static core::LogWriter vlog("KeyboardWin32"); // Layout independent keys static const UINT vkey_map[][3] = { @@ -138,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 }, }; @@ -202,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; @@ -256,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); @@ -354,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; @@ -465,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]; @@ -524,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 0ee1d4f6..587d9fc5 100644 --- a/vncviewer/KeyboardX11.cxx +++ b/vncviewer/KeyboardX11.cxx @@ -22,12 +22,14 @@ #include <assert.h> +#include <algorithm> #include <stdexcept> #include <X11/XKBlib.h> #include <FL/x.H> -#include <rfb/LogWriter.h> +#include <core/LogWriter.h> + #include <rfb/ledStates.h> #include "i18n.h" @@ -39,7 +41,7 @@ extern const struct _code_map_xkb_to_qnum { } code_map_xkb_to_qnum[]; extern const unsigned int code_map_xkb_to_qnum_len; -static rfb::LogWriter vlog("KeyboardX11"); +static core::LogWriter vlog("KeyboardX11"); KeyboardX11::KeyboardX11(KeyboardHandler* handler_) : Keyboard(handler_) @@ -86,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; @@ -97,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); @@ -108,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; } @@ -115,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; @@ -218,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/MonitorIndicesParameter.cxx b/vncviewer/MonitorIndicesParameter.cxx index 957ade06..7f8d6a29 100644 --- a/vncviewer/MonitorIndicesParameter.cxx +++ b/vncviewer/MonitorIndicesParameter.cxx @@ -30,22 +30,23 @@ #include <stdexcept> #include "i18n.h" + #include <FL/Fl.H> -#include <rfb/LogWriter.h> + +#include <core/LogWriter.h> #include "MonitorIndicesParameter.h" -using namespace rfb; -static LogWriter vlog("MonitorIndicesParameter"); +static core::LogWriter vlog("MonitorIndicesParameter"); -MonitorIndicesParameter::MonitorIndicesParameter(const char* name_, const char* desc_, const char* v) -: StringParameter(name_, desc_, v) {} +MonitorIndicesParameter::MonitorIndicesParameter(const char* name_, + const char* desc_, + const ListType& v) +: IntListParameter(name_, desc_, v, 1, INT_MAX) {} -std::set<int> MonitorIndicesParameter::getParam() +std::set<int> MonitorIndicesParameter::getMonitors() const { - bool valid = false; std::set<int> indices; - std::set<int> configIndices; std::vector<MonitorIndicesParameter::Monitor> monitors = fetchMonitors(); if (monitors.size() <= 0) { @@ -53,18 +54,9 @@ std::set<int> MonitorIndicesParameter::getParam() return indices; } - valid = parseIndices(value.c_str(), &configIndices); - if (!valid) { - return indices; - } - - if (configIndices.size() <= 0) { - return indices; - } - // Go through the monitors and see what indices are present in the config. for (int i = 0; i < ((int) monitors.size()); i++) { - if (std::find(configIndices.begin(), configIndices.end(), i) != configIndices.end()) + if (std::find(begin(), end(), i+1) != end()) indices.insert(monitors[i].fltkIndex); } @@ -73,27 +65,20 @@ std::set<int> MonitorIndicesParameter::getParam() bool MonitorIndicesParameter::setParam(const char* v) { - std::set<int> indices; - - if (!parseIndices(v, &indices, true)) { - vlog.error(_("Invalid configuration specified for %s"), name); + if (!IntListParameter::setParam(v)) return false; - } - for (int index : indices) { - index += 1; + for (int index : value) { if (index <= 0 || index > Fl::screen_count()) vlog.error(_("Monitor index %d does not exist"), index); } - return StringParameter::setParam(v); + return true; } -bool MonitorIndicesParameter::setParam(std::set<int> indices) +void MonitorIndicesParameter::setMonitors(const std::set<int>& indices) { - static const int BUF_MAX_LEN = 1024; - char buf[BUF_MAX_LEN] = {0}; - std::set<int> configIndices; + std::list<int> configIndices; std::vector<MonitorIndicesParameter::Monitor> monitors = fetchMonitors(); if (monitors.size() <= 0) { @@ -103,88 +88,10 @@ bool MonitorIndicesParameter::setParam(std::set<int> indices) for (int i = 0; i < ((int) monitors.size()); i++) { if (std::find(indices.begin(), indices.end(), monitors[i].fltkIndex) != indices.end()) - configIndices.insert(i); - } - - int bytesWritten = 0; - char const * separator = ""; - - for (int configIndex : configIndices) - { - bytesWritten += snprintf( - buf+bytesWritten, - BUF_MAX_LEN-bytesWritten, - "%s%u", - separator, - configIndex+1 - ); - - separator = ","; + configIndices.push_back(i+1); } - return setParam(buf); -} - -static bool parseNumber(std::string number, std::set<int> *indices) -{ - if (number.size() <= 0) - return false; - - int v = strtol(number.c_str(), nullptr, 0); - - if (v <= 0) - return false; - - if (v > INT_MAX) - return false; - - indices->insert(v-1); - return true; -} - -bool MonitorIndicesParameter::parseIndices(const char* value, - std::set<int> *indices, - bool complain) -{ - char d; - std::string current; - - for (size_t i = 0; i < strlen(value); i++) { - d = value[i]; - - if (d == ' ') - continue; - else if (d >= '0' && d <= '9') - current.push_back(d); - else if (d == ',') { - if (!parseNumber(current, indices)) { - if (complain) - vlog.error(_("Invalid monitor index '%s'"), - current.c_str()); - return false; - } - - current.clear(); - } else { - if (complain) - vlog.error(_("Unexpected character '%c'"), d); - return false; - } - } - - // If we have nothing left to parse we are in a valid state. - if (current.size() == 0) - return true; - - // Parsing anything we have left. - if (!parseNumber(current, indices)) { - if (complain) - vlog.error(_("Invalid monitor index '%s'"), - current.c_str()); - return false; - } - - return true; + IntListParameter::setParam(configIndices); } std::vector<MonitorIndicesParameter::Monitor> MonitorIndicesParameter::fetchMonitors() diff --git a/vncviewer/MonitorIndicesParameter.h b/vncviewer/MonitorIndicesParameter.h index d91c84fe..a4f7171d 100644 --- a/vncviewer/MonitorIndicesParameter.h +++ b/vncviewer/MonitorIndicesParameter.h @@ -22,23 +22,22 @@ #include <set> #include <vector> -#include <rfb/Configuration.h> +#include <core/Configuration.h> -class MonitorIndicesParameter: public rfb::StringParameter { +class MonitorIndicesParameter: public core::IntListParameter { public: - MonitorIndicesParameter(const char* name_, const char* desc_, const char* v); - std::set<int> getParam(); - bool setParam(std::set<int> indices); - bool setParam(const char* v) override; + MonitorIndicesParameter(const char* name_, const char* desc_, + const ListType& v); + std::set<int> getMonitors() const; + bool setParam(const char* value) override; + void setMonitors(const std::set<int>& v); private: typedef struct { int x, y, w, h; int fltkIndex; } Monitor; - static bool parseIndices(const char* value, std::set<int> *indices, - bool complain=false); - std::vector<MonitorIndicesParameter::Monitor> fetchMonitors(); + static std::vector<MonitorIndicesParameter::Monitor> fetchMonitors(); static int compare(const void*, const void*); }; diff --git a/vncviewer/OptionsDialog.cxx b/vncviewer/OptionsDialog.cxx index c5f21b24..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,18 +46,21 @@ #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> -using namespace std; -using namespace rfb; - std::map<OptionsCallback*, void*> OptionsDialog::callbacks; static std::set<OptionsDialog *> instances; @@ -85,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); } @@ -162,27 +168,18 @@ void OptionsDialog::loadOptions(void) /* Compression */ autoselectCheckbox->value(autoSelect); - int encNum = encodingNum(preferredEncoding); - - switch (encNum) { - case encodingTight: + if (preferredEncoding == "Tight") tightButton->setonly(); - break; - case encodingZRLE: + else if (preferredEncoding == "ZRLE") zrleButton->setonly(); - break; - case encodingHextile: + else if (preferredEncoding == "Hextile") hextileButton->setonly(); - break; #ifdef HAVE_H264 - case encodingH264: + else if (preferredEncoding == "H.264") h264Button->setonly(); - break; #endif - case encodingRaw: + else if (preferredEncoding == "Raw") rawButton->setonly(); - break; - } if (fullColour) fullcolorCheckbox->setonly(); @@ -215,11 +212,11 @@ void OptionsDialog::loadOptions(void) #if defined(HAVE_GNUTLS) || defined(HAVE_NETTLE) /* Security */ - Security security(SecurityClient::secTypes); + rfb::Security security(rfb::SecurityClient::secTypes); - list<uint8_t> secTypes; + std::list<uint8_t> secTypes; - list<uint32_t> secTypesExt; + std::list<uint32_t> secTypesExt; encNoneCheckbox->value(false); #ifdef HAVE_GNUTLS @@ -237,11 +234,11 @@ void OptionsDialog::loadOptions(void) secTypes = security.GetEnabledSecTypes(); for (uint8_t type : secTypes) { switch (type) { - case secTypeNone: + case rfb::secTypeNone: encNoneCheckbox->value(true); authNoneCheckbox->value(true); break; - case secTypeVncAuth: + case rfb::secTypeVncAuth: encNoneCheckbox->value(true); authVncCheckbox->value(true); break; @@ -251,49 +248,49 @@ void OptionsDialog::loadOptions(void) secTypesExt = security.GetEnabledExtSecTypes(); for (uint32_t type : secTypesExt) { switch (type) { - case secTypePlain: + case rfb::secTypePlain: encNoneCheckbox->value(true); authPlainCheckbox->value(true); break; #ifdef HAVE_GNUTLS - case secTypeTLSNone: + case rfb::secTypeTLSNone: encTLSCheckbox->value(true); authNoneCheckbox->value(true); break; - case secTypeTLSVnc: + case rfb::secTypeTLSVnc: encTLSCheckbox->value(true); authVncCheckbox->value(true); break; - case secTypeTLSPlain: + case rfb::secTypeTLSPlain: encTLSCheckbox->value(true); authPlainCheckbox->value(true); break; - case secTypeX509None: + case rfb::secTypeX509None: encX509Checkbox->value(true); authNoneCheckbox->value(true); break; - case secTypeX509Vnc: + case rfb::secTypeX509Vnc: encX509Checkbox->value(true); authVncCheckbox->value(true); break; - case secTypeX509Plain: + case rfb::secTypeX509Plain: encX509Checkbox->value(true); authPlainCheckbox->value(true); break; #endif #ifdef HAVE_NETTLE - case secTypeRA2: - case secTypeRA256: + case rfb::secTypeRA2: + case rfb::secTypeRA256: encRSAAESCheckbox->value(true); authVncCheckbox->value(true); authPlainCheckbox->value(true); break; - case secTypeRA2ne: - case secTypeRAne256: + case rfb::secTypeRA2ne: + case rfb::secTypeRAne256: authVncCheckbox->value(true); /* fall through */ - case secTypeDH: - case secTypeMSLogonII: + case rfb::secTypeDH: + case rfb::secTypeMSLogonII: encNoneCheckbox->value(true); authPlainCheckbox->value(true); break; @@ -303,16 +300,14 @@ void OptionsDialog::loadOptions(void) } #ifdef HAVE_GNUTLS - caInput->value(CSecurityTLS::X509CA); - crlInput->value(CSecurityTLS::X509CRL); + caInput->value(rfb::CSecurityTLS::X509CA); + crlInput->value(rfb::CSecurityTLS::X509CRL); handleX509(encX509Checkbox, this); #endif #endif /* Input */ - const char *menuKeyBuf; - viewOnlyCheckbox->value(viewOnly); emulateMBCheckbox->value(emulateMiddleButton); acceptClipboardCheckbox->value(acceptClipboard); @@ -325,34 +320,48 @@ 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()); + + ctrlButton->value(modifierMask & ShortcutHandler::Control); + shiftButton->value(modifierMask & ShortcutHandler::Shift); + altButton->value(modifierMask & ShortcutHandler::Alt); + superButton->value(modifierMask & ShortcutHandler::Super); - menuKeyBuf = menuKey; - for (int idx = 0; idx < getMenuKeySymbolCount(); idx++) - if (!strcmp(getMenuKeySymbols()[idx].name, menuKeyBuf)) - menuKeyChoice->value(idx + 1); + handleModifier(nullptr, this); /* Display */ if (!fullScreen) { windowedButton->setonly(); } else { - if (!strcasecmp(fullScreenMode, "all")) { + if (fullScreenMode == "all") { allMonitorsButton->setonly(); - } else if (!strcasecmp(fullScreenMode, "selected")) { + } else if (fullScreenMode == "selected") { selectedMonitorsButton->setonly(); } else { currentMonitorButton->setonly(); } } - monitorArrangement->value(fullScreenSelectedMonitors.getParam()); + monitorArrangement->value(fullScreenSelectedMonitors.getMonitors()); handleFullScreenMode(selectedMonitorsButton, this); /* Misc. */ sharedCheckbox->value(shared); reconnectCheckbox->value(reconnectOnError); - dotCursorCheckbox->value(dotWhenNoCursor); + alwaysCursorCheckbox->value(alwaysCursor); + if (cursorType == "system") { + cursorTypeChoice->value(1); + } else { + // Default + cursorTypeChoice->value(0); + } + handleAlwaysCursor(alwaysCursorCheckbox, this); } @@ -362,17 +371,17 @@ void OptionsDialog::storeOptions(void) autoSelect.setParam(autoselectCheckbox->value()); if (tightButton->value()) - preferredEncoding.setParam(encodingName(encodingTight)); + preferredEncoding.setParam(rfb::encodingName(rfb::encodingTight)); else if (zrleButton->value()) - preferredEncoding.setParam(encodingName(encodingZRLE)); + preferredEncoding.setParam(rfb::encodingName(rfb::encodingZRLE)); else if (hextileButton->value()) - preferredEncoding.setParam(encodingName(encodingHextile)); + preferredEncoding.setParam(rfb::encodingName(rfb::encodingHextile)); #ifdef HAVE_H264 else if (h264Button->value()) - preferredEncoding.setParam(encodingName(encodingH264)); + preferredEncoding.setParam(rfb::encodingName(rfb::encodingH264)); #endif else if (rawButton->value()) - preferredEncoding.setParam(encodingName(encodingRaw)); + preferredEncoding.setParam(rfb::encodingName(rfb::encodingRaw)); fullColour.setParam(fullcolorCheckbox->value()); if (verylowcolorCheckbox->value()) @@ -389,26 +398,26 @@ void OptionsDialog::storeOptions(void) #if defined(HAVE_GNUTLS) || defined(HAVE_NETTLE) /* Security */ - Security security; + rfb::Security security; /* Process security types which don't use encryption */ if (encNoneCheckbox->value()) { if (authNoneCheckbox->value()) - security.EnableSecType(secTypeNone); + security.EnableSecType(rfb::secTypeNone); if (authVncCheckbox->value()) { - security.EnableSecType(secTypeVncAuth); + security.EnableSecType(rfb::secTypeVncAuth); #ifdef HAVE_NETTLE - security.EnableSecType(secTypeRA2ne); - security.EnableSecType(secTypeRAne256); + security.EnableSecType(rfb::secTypeRA2ne); + security.EnableSecType(rfb::secTypeRAne256); #endif } if (authPlainCheckbox->value()) { - security.EnableSecType(secTypePlain); + security.EnableSecType(rfb::secTypePlain); #ifdef HAVE_NETTLE - security.EnableSecType(secTypeRA2ne); - security.EnableSecType(secTypeRAne256); - security.EnableSecType(secTypeDH); - security.EnableSecType(secTypeMSLogonII); + security.EnableSecType(rfb::secTypeRA2ne); + security.EnableSecType(rfb::secTypeRAne256); + security.EnableSecType(rfb::secTypeDH); + security.EnableSecType(rfb::secTypeMSLogonII); #endif } } @@ -417,34 +426,34 @@ void OptionsDialog::storeOptions(void) /* Process security types which use TLS encryption */ if (encTLSCheckbox->value()) { if (authNoneCheckbox->value()) - security.EnableSecType(secTypeTLSNone); + security.EnableSecType(rfb::secTypeTLSNone); if (authVncCheckbox->value()) - security.EnableSecType(secTypeTLSVnc); + security.EnableSecType(rfb::secTypeTLSVnc); if (authPlainCheckbox->value()) - security.EnableSecType(secTypeTLSPlain); + security.EnableSecType(rfb::secTypeTLSPlain); } /* Process security types which use X509 encryption */ if (encX509Checkbox->value()) { if (authNoneCheckbox->value()) - security.EnableSecType(secTypeX509None); + security.EnableSecType(rfb::secTypeX509None); if (authVncCheckbox->value()) - security.EnableSecType(secTypeX509Vnc); + security.EnableSecType(rfb::secTypeX509Vnc); if (authPlainCheckbox->value()) - security.EnableSecType(secTypeX509Plain); + security.EnableSecType(rfb::secTypeX509Plain); } - CSecurityTLS::X509CA.setParam(caInput->value()); - CSecurityTLS::X509CRL.setParam(crlInput->value()); + rfb::CSecurityTLS::X509CA.setParam(caInput->value()); + rfb::CSecurityTLS::X509CRL.setParam(crlInput->value()); #endif #ifdef HAVE_NETTLE if (encRSAAESCheckbox->value()) { - security.EnableSecType(secTypeRA2); - security.EnableSecType(secTypeRA256); + security.EnableSecType(rfb::secTypeRA2); + security.EnableSecType(rfb::secTypeRA256); } #endif - SecurityClient::secTypes.setParam(security.ToString()); + rfb::SecurityClient::secTypes.setParam(security.ToString()); #endif /* Input */ @@ -460,11 +469,23 @@ void OptionsDialog::storeOptions(void) #endif fullscreenSystemKeys.setParam(systemKeysCheckbox->value()); - if (menuKeyChoice->value() == 0) - menuKey.setParam(""); - 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()) { @@ -481,12 +502,19 @@ void OptionsDialog::storeOptions(void) } } - fullScreenSelectedMonitors.setParam(monitorArrangement->value()); + fullScreenSelectedMonitors.setMonitors(monitorArrangement->value()); /* Misc. */ shared.setParam(sharedCheckbox->value()); reconnectOnError.setParam(reconnectCheckbox->value()); - dotWhenNoCursor.setParam(dotCursorCheckbox->value()); + alwaysCursor.setParam(alwaysCursorCheckbox->value()); + + if (cursorTypeChoice->value() == 1) { + cursorType.setParam("System"); + } else { + // Default + cursorType.setParam("Dot"); + } std::map<OptionsCallback*, void*>::const_iterator iter; @@ -840,11 +868,23 @@ void OptionsDialog::createInputPage(int tx, int ty, int tw, int th) _("Emulate middle mouse button"))); ty += CHECK_HEIGHT + TIGHT_MARGIN; - dotCursorCheckbox = new Fl_Check_Button(LBLRIGHT(tx, ty, - CHECK_MIN_WIDTH, - CHECK_HEIGHT, - _("Show dot when no cursor"))); + alwaysCursorCheckbox = new Fl_Check_Button(LBLRIGHT(tx, ty, + CHECK_MIN_WIDTH, + CHECK_HEIGHT, + _("Show local cursor when not provided by server"))); + alwaysCursorCheckbox->callback(handleAlwaysCursor, this); ty += CHECK_HEIGHT + TIGHT_MARGIN; + + /* Cursor type */ + cursorTypeChoice = new Fl_Choice(LBLLEFT(tx, ty, 150, CHOICE_HEIGHT, _("Cursor type"))); + + fltk_menu_add(cursorTypeChoice, _("Dot"), 0, nullptr, nullptr, 0); + fltk_menu_add(cursorTypeChoice, _("System"), 0, nullptr, nullptr, 0); + + fltk_adjust_choice(cursorTypeChoice); + + ty += CHOICE_HEIGHT + TIGHT_MARGIN; + } ty -= TIGHT_MARGIN; @@ -868,19 +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); - - ty += CHOICE_HEIGHT + TIGHT_MARGIN; } ty -= TIGHT_MARGIN; @@ -949,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")); @@ -1117,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; @@ -1134,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; @@ -1184,5 +1355,16 @@ void OptionsDialog::handleScreenConfigTimeout(void *data) assert(self); - self->monitorArrangement->value(fullScreenSelectedMonitors.getParam()); + self->monitorArrangement->value(fullScreenSelectedMonitors.getMonitors()); +} + +void OptionsDialog::handleAlwaysCursor(Fl_Widget* /*widget*/, void *data) +{ + OptionsDialog *dialog = (OptionsDialog*)data; + + if (dialog->alwaysCursorCheckbox->value()) { + dialog->cursorTypeChoice->activate(); + } else { + dialog->cursorTypeChoice->deactivate(); + } } diff --git a/vncviewer/OptionsDialog.h b/vncviewer/OptionsDialog.h index f6ca89b1..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,18 +56,24 @@ 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); static void handleAutoselect(Fl_Widget *widget, void *data); static void handleCompression(Fl_Widget *widget, void *data); static void handleJpeg(Fl_Widget *widget, void *data); + static void handleAlwaysCursor(Fl_Widget *widget, void *data); 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); @@ -115,10 +123,10 @@ protected: Fl_Check_Button *viewOnlyCheckbox; Fl_Group *mouseGroup; Fl_Check_Button *emulateMBCheckbox; - Fl_Check_Button *dotCursorCheckbox; + Fl_Check_Button *alwaysCursorCheckbox; + 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__) @@ -129,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/PlatformPixelBuffer.cxx b/vncviewer/PlatformPixelBuffer.cxx index 0f152e11..6a6d22f1 100644 --- a/vncviewer/PlatformPixelBuffer.cxx +++ b/vncviewer/PlatformPixelBuffer.cxx @@ -33,11 +33,11 @@ #include <FL/Fl.H> #include <FL/x.H> -#include <rfb/LogWriter.h> +#include <core/LogWriter.h> #include "PlatformPixelBuffer.h" -static rfb::LogWriter vlog("PlatformPixelBuffer"); +static core::LogWriter vlog("PlatformPixelBuffer"); PlatformPixelBuffer::PlatformPixelBuffer(int width, int height) : FullFramePixelBuffer(rfb::PixelFormat(32, 24, false, true, @@ -91,17 +91,17 @@ PlatformPixelBuffer::~PlatformPixelBuffer() #endif } -void PlatformPixelBuffer::commitBufferRW(const rfb::Rect& r) +void PlatformPixelBuffer::commitBufferRW(const core::Rect& r) { FullFramePixelBuffer::commitBufferRW(r); mutex.lock(); - damage.assign_union(rfb::Region(r)); + damage.assign_union(r); mutex.unlock(); } -rfb::Rect PlatformPixelBuffer::getDamage(void) +core::Rect PlatformPixelBuffer::getDamage(void) { - rfb::Rect r; + core::Rect r; mutex.lock(); r = damage.get_bounding_rect(); diff --git a/vncviewer/PlatformPixelBuffer.h b/vncviewer/PlatformPixelBuffer.h index 24763d46..2c8d3957 100644 --- a/vncviewer/PlatformPixelBuffer.h +++ b/vncviewer/PlatformPixelBuffer.h @@ -27,11 +27,11 @@ #endif #include <list> +#include <mutex> -#include <os/Mutex.h> +#include <core/Region.h> #include <rfb/PixelBuffer.h> -#include <rfb/Region.h> #include "Surface.h" @@ -40,16 +40,16 @@ public: PlatformPixelBuffer(int width, int height); ~PlatformPixelBuffer(); - void commitBufferRW(const rfb::Rect& r) override; + void commitBufferRW(const core::Rect& r) override; - rfb::Rect getDamage(void); + core::Rect getDamage(void); using rfb::FullFramePixelBuffer::width; using rfb::FullFramePixelBuffer::height; protected: - os::Mutex mutex; - rfb::Region damage; + std::mutex mutex; + core::Region damage; #if !defined(WIN32) && !defined(__APPLE__) protected: diff --git a/vncviewer/ServerDialog.cxx b/vncviewer/ServerDialog.cxx index e9d4e75b..3011e948 100644 --- a/vncviewer/ServerDialog.cxx +++ b/vncviewer/ServerDialog.cxx @@ -25,6 +25,11 @@ #include <algorithm> #include <libgen.h> +// FIXME: Workaround for FLTK including windows.h +#ifdef WIN32 +#include <winsock2.h> +#endif + #include <FL/Fl.H> #include <FL/Fl_Input.H> #include <FL/Fl_Input_Choice.H> @@ -35,13 +40,12 @@ #include <FL/Fl_Box.H> #include <FL/Fl_File_Chooser.H> -#include <os/os.h> - -#include <rdr/Exception.h> +#include <core/Exception.h> +#include <core/LogWriter.h> +#include <core/string.h> +#include <core/xdgdirs.h> -#include <rfb/Hostname.h> -#include <rfb/LogWriter.h> -#include <rfb/util.h> +#include <network/TcpSocket.h> #include "fltk/layout.h" #include "fltk/util.h" @@ -51,16 +55,12 @@ #include "vncviewer.h" #include "parameters.h" - -using namespace std; -using namespace rfb; - -static LogWriter vlog("ServerDialog"); +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; @@ -139,7 +139,7 @@ void ServerDialog::run(const char* servername, char *newservername) dialog.loadServerHistory(); dialog.serverName->clear(); - for (const string& entry : dialog.serverHistory) + for (const std::string& entry : dialog.serverHistory) fltk_menu_add(dialog.serverName->menubutton(), entry.c_str(), 0, nullptr); } catch (std::exception& e) { @@ -170,7 +170,7 @@ void ServerDialog::handleLoad(Fl_Widget* /*widget*/, void* data) ServerDialog *dialog = (ServerDialog*)data; if (dialog->usedDir.empty()) - dialog->usedDir = os::getuserhomedir(); + dialog->usedDir = core::getuserhomedir(); Fl_File_Chooser* file_chooser = new Fl_File_Chooser(dialog->usedDir.c_str(), _("TigerVNC configuration (*.tigervnc)"), @@ -210,7 +210,7 @@ void ServerDialog::handleSaveAs(Fl_Widget* /*widget*/, void* data) const char* servername = dialog->serverName->value(); const char* filename; if (dialog->usedDir.empty()) - dialog->usedDir = os::getuserhomedir(); + dialog->usedDir = core::getuserhomedir(); Fl_File_Chooser* file_chooser = new Fl_File_Chooser(dialog->usedDir.c_str(), _("TigerVNC configuration (*.tigervnc)"), @@ -309,19 +309,20 @@ void ServerDialog::handleConnect(Fl_Widget* /*widget*/, void *data) } -static bool same_server(const string& a, const string& b) +static bool same_server(const std::string& a, const std::string& b) { - string hostA, hostB; + std::string hostA, hostB; int portA, portB; #ifndef WIN32 - if ((a.find("/") != string::npos) || (b.find("/") != string::npos)) + if ((a.find("/") != std::string::npos) || + (b.find("/") != std::string::npos)) return a == b; #endif try { - getHostAndPort(a.c_str(), &hostA, &portA); - getHostAndPort(b.c_str(), &hostB, &portB); + network::getHostAndPort(a.c_str(), &hostA, &portA); + network::getHostAndPort(b.c_str(), &hostB, &portB); } catch (std::exception& e) { return false; } @@ -338,7 +339,7 @@ static bool same_server(const string& a, const string& b) void ServerDialog::loadServerHistory() { - list<string> rawHistory; + std::list<std::string> rawHistory; serverHistory.clear(); @@ -346,7 +347,7 @@ void ServerDialog::loadServerHistory() rawHistory = loadHistoryFromRegKey(); #else - const char* stateDir = os::getvncstatedir(); + const char* stateDir = core::getvncstatedir(); if (stateDir == nullptr) throw std::runtime_error(_("Could not determine VNC state directory path")); @@ -360,8 +361,8 @@ void ServerDialog::loadServerHistory() // no history file return; } - std::string msg = format(_("Could not open \"%s\""), filepath); - throw rdr::posix_error(msg.c_str(), errno); + throw core::posix_error( + core::format(_("Could not open \"%s\""), filepath), errno); } int lineNr = 0; @@ -375,20 +376,21 @@ void ServerDialog::loadServerHistory() break; fclose(f); - std::string msg = format(_("Failed to read line %d in " - "file \"%s\""), lineNr, filepath); - throw rdr::posix_error(msg.c_str(), errno); + throw core::posix_error( + core::format(_("Failed to read line %d in file \"%s\""), + lineNr, filepath), + errno); } int len = strlen(line); if (len == (sizeof(line) - 1)) { fclose(f); - throw std::runtime_error(format("%s: %s", - format(_("Failed to read line %d " - "in file %s"), - lineNr, filepath).c_str(), - _("Line too long"))); + std::string msg = core::format(_("Failed to read line %d in " + "file \"%s\""), + lineNr, filepath); + throw std::runtime_error( + core::format("%s: %s", msg.c_str(), _("Line too long"))); } if ((len > 0) && (line[len-1] == '\n')) { @@ -410,9 +412,11 @@ void ServerDialog::loadServerHistory() #endif // Filter out duplicates, even if they have different formats - for (const string& entry : rawHistory) { + for (const std::string& entry : rawHistory) { if (std::find_if(serverHistory.begin(), serverHistory.end(), - [&entry](const string& s) { return same_server(s, entry); }) != serverHistory.end()) + [&entry](const std::string& s) { + return same_server(s, entry); + }) != serverHistory.end()) continue; serverHistory.push_back(entry); } @@ -425,7 +429,7 @@ void ServerDialog::saveServerHistory() return; #endif - const char* stateDir = os::getvncstatedir(); + const char* stateDir = core::getvncstatedir(); if (stateDir == nullptr) throw std::runtime_error(_("Could not determine VNC state directory path")); @@ -435,13 +439,13 @@ void ServerDialog::saveServerHistory() /* Write server history to file */ FILE* f = fopen(filepath, "w+"); if (!f) { - std::string msg = format(_("Could not open \"%s\""), filepath); - throw rdr::posix_error(msg.c_str(), errno); + std::string msg = core::format(_("Could not open \"%s\""), filepath); + throw core::posix_error(msg.c_str(), errno); } // Save the last X elements to the config file. size_t count = 0; - for (const string& entry : serverHistory) { + for (const std::string& entry : serverHistory) { if (++count > SERVER_HISTORY_SIZE) break; fprintf(f, "%s\n", entry.c_str()); 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/Surface_Win32.cxx b/vncviewer/Surface_Win32.cxx index c992dbea..c00cbc06 100644 --- a/vncviewer/Surface_Win32.cxx +++ b/vncviewer/Surface_Win32.cxx @@ -25,7 +25,7 @@ #include <FL/Fl_RGB_Image.H> #include <FL/x.H> -#include <rdr/Exception.h> +#include <core/Exception.h> #include "Surface.h" @@ -57,10 +57,10 @@ void Surface::draw(int src_x, int src_y, int dst_x, int dst_y, dc = CreateCompatibleDC(fl_gc); if (!dc) - throw rdr::win32_error("CreateCompatibleDC", GetLastError()); + throw core::win32_error("CreateCompatibleDC", GetLastError()); if (!SelectObject(dc, bitmap)) - throw rdr::win32_error("SelectObject", GetLastError()); + throw core::win32_error("SelectObject", GetLastError()); if (!BitBlt(fl_gc, dst_x, dst_y, dst_w, dst_h, dc, src_x, src_y, SRCCOPY)) { @@ -70,7 +70,7 @@ void Surface::draw(int src_x, int src_y, int dst_x, int dst_y, // with it. For now, we've only seen this error and for this function // so only ignore this combination. if (GetLastError() != ERROR_INVALID_HANDLE) - throw rdr::win32_error("BitBlt", GetLastError()); + throw core::win32_error("BitBlt", GetLastError()); } DeleteDC(dc); @@ -83,10 +83,10 @@ void Surface::draw(Surface* dst, int src_x, int src_y, dstdc = CreateCompatibleDC(nullptr); if (!dstdc) - throw rdr::win32_error("CreateCompatibleDC", GetLastError()); + throw core::win32_error("CreateCompatibleDC", GetLastError()); if (!SelectObject(dstdc, dst->bitmap)) - throw rdr::win32_error("SelectObject", GetLastError()); + throw core::win32_error("SelectObject", GetLastError()); origdc = fl_gc; fl_gc = dstdc; @@ -113,15 +113,15 @@ void Surface::blend(Surface* dst, int src_x, int src_y, dstdc = CreateCompatibleDC(nullptr); if (!dstdc) - throw rdr::win32_error("CreateCompatibleDC", GetLastError()); + throw core::win32_error("CreateCompatibleDC", GetLastError()); srcdc = CreateCompatibleDC(nullptr); if (!srcdc) - throw rdr::win32_error("CreateCompatibleDC", GetLastError()); + throw core::win32_error("CreateCompatibleDC", GetLastError()); if (!SelectObject(dstdc, dst->bitmap)) - throw rdr::win32_error("SelectObject", GetLastError()); + throw core::win32_error("SelectObject", GetLastError()); if (!SelectObject(srcdc, bitmap)) - throw rdr::win32_error("SelectObject", GetLastError()); + throw core::win32_error("SelectObject", GetLastError()); blend.BlendOp = AC_SRC_OVER; blend.BlendFlags = 0; @@ -136,7 +136,7 @@ void Surface::blend(Surface* dst, int src_x, int src_y, // with it. For now, we've only seen this error and for this function // so only ignore this combination. if (GetLastError() != ERROR_INVALID_HANDLE) - throw rdr::win32_error("BitBlt", GetLastError()); + throw core::win32_error("BitBlt", GetLastError()); } DeleteDC(srcdc); @@ -161,7 +161,7 @@ void Surface::alloc() bitmap = CreateDIBSection(nullptr, (BITMAPINFO*)&bih, DIB_RGB_COLORS, (void**)&data, nullptr, 0); if (!bitmap) - throw rdr::win32_error("CreateDIBSection", GetLastError()); + throw core::win32_error("CreateDIBSection", GetLastError()); } void Surface::dealloc() diff --git a/vncviewer/UserDialog.cxx b/vncviewer/UserDialog.cxx index c6cc02f7..9b7c0b93 100644 --- a/vncviewer/UserDialog.cxx +++ b/vncviewer/UserDialog.cxx @@ -36,8 +36,9 @@ #include <FL/Fl_Return_Button.H> #include <FL/Fl_Pixmap.H> -#include <rdr/Exception.h> +#include <core/Exception.h> +#include <rfb/CConnection.h> #include <rfb/Exception.h> #include <rfb/obfuscate.h> @@ -54,8 +55,6 @@ #include "../media/insecure.xpm" #pragma GCC diagnostic pop -using namespace rfb; - static Fl_Pixmap secure_icon(secure); static Fl_Pixmap insecure_icon(insecure); @@ -65,8 +64,16 @@ std::string UserDialog::savedPassword; static long ret_val = 0; static void button_cb(Fl_Widget *widget, long val) { + Fl_Window* win; + ret_val = val; - widget->window()->hide(); + + assert(widget != nullptr); + win = dynamic_cast<Fl_Window*>(widget); + if (win == nullptr) + win = widget->window(); + assert(win != nullptr); + win->hide(); } UserDialog::UserDialog() @@ -120,12 +127,12 @@ void UserDialog::getUserPasswd(bool secure_, std::string* user, fp = fopen(passwordFileName, "rb"); if (!fp) - throw rdr::posix_error(_("Opening password file failed"), errno); + throw core::posix_error(_("Opening password file failed"), errno); obfPwd.resize(fread(obfPwd.data(), 1, obfPwd.size(), fp)); fclose(fp); - *password = deobfuscate(obfPwd.data(), obfPwd.size()); + *password = rfb::deobfuscate(obfPwd.data(), obfPwd.size()); return; } @@ -141,7 +148,7 @@ void UserDialog::getUserPasswd(bool secure_, std::string* user, int x, y; win = new Fl_Window(410, 0, _("VNC authentication")); - win->callback(button_cb, 0); + win->callback(button_cb, 1); banner = new Fl_Box(0, 0, win->w(), 20); banner->align(FL_ALIGN_CENTER|FL_ALIGN_INSIDE|FL_ALIGN_IMAGE_NEXT_TO_TEXT); @@ -254,7 +261,8 @@ void UserDialog::getUserPasswd(bool secure_, std::string* user, throw rfb::auth_cancelled(); } -bool UserDialog::showMsgBox(MsgBoxFlags flags, const char* title, const char* text) +bool UserDialog::showMsgBox(rfb::MsgBoxFlags flags, + const char* title, const char* text) { char buffer[1024]; @@ -267,14 +275,14 @@ bool UserDialog::showMsgBox(MsgBoxFlags flags, const char* title, const char* te fl_message_title(title); switch (flags & 0xf) { - case M_OKCANCEL: + case rfb::M_OKCANCEL: return fl_choice("%s", nullptr, fl_ok, fl_cancel, buffer) == 1; - case M_YESNO: + case rfb::M_YESNO: return fl_choice("%s", nullptr, fl_yes, fl_no, buffer) == 1; - case M_OK: + case rfb::M_OK: default: - if (((flags & 0xf0) == M_ICONERROR) || - ((flags & 0xf0) == M_ICONWARNING)) + if (((flags & 0xf0) == rfb::M_ICONERROR) || + ((flags & 0xf0) == rfb::M_ICONWARNING)) fl_alert("%s", buffer); else fl_message("%s", buffer); diff --git a/vncviewer/UserDialog.h b/vncviewer/UserDialog.h index ddafbc3c..22799fb9 100644 --- a/vncviewer/UserDialog.h +++ b/vncviewer/UserDialog.h @@ -19,8 +19,6 @@ #ifndef __USERDIALOG_H__ #define __USERDIALOG_H__ -#include <rfb/CConnection.h> - class UserDialog { public: diff --git a/vncviewer/Viewport.cxx b/vncviewer/Viewport.cxx index 8c3b5dc5..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 @@ -25,17 +25,27 @@ #include <stdio.h> #include <string.h> +#include <stdexcept> + +#include <core/LogWriter.h> +#include <core/string.h> + #include <rfb/CMsgWriter.h> -#include <rfb/LogWriter.h> +#include <rfb/Cursor.h> +#include <rfb/KeysymStr.h> #include <rfb/ledStates.h> -#include <rfb/util.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" @@ -44,7 +54,6 @@ #include "DesktopWindow.h" #include "i18n.h" #include "parameters.h" -#include "menukey.h" #include "vncviewer.h" #include "PlatformPixelBuffer.h" @@ -68,14 +77,12 @@ #include "cocoa.h" #endif -using namespace rfb; - -static rfb::LogWriter vlog("Viewport"); +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 @@ -86,12 +93,13 @@ 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) + menuCtrlKey(false), menuAltKey(false), cursor(nullptr), + cursorIsBlank(false) { #if defined(WIN32) keyboard = new KeyboardWin32(this); @@ -106,6 +114,12 @@ Viewport::Viewport(int w, int h, const rfb::PixelFormat& /*serverPF*/, CConn* cc // We need to intercept keyboard events early Fl::add_system_handler(handleSystemEvent, this); + // FIXME: We should only disable this whilst we have keyboard focus, + // but we also need to keep it disabled when we lose focus to + // any layout selector so it can properly filter out the + // layouts we don't support + Fl::disable_im(); + frameBuffer = new PlatformPixelBuffer(w, h); assert(frameBuffer); cc->setFramebuffer(frameBuffer); @@ -121,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, rfb::Point(0, 0), nullptr); + setCursor(); } @@ -149,6 +167,7 @@ Viewport::~Viewport() } delete keyboard; + Fl::enable_im(); // FLTK automatically deletes all child widgets, so we shouldn't touch // them ourselves here @@ -166,7 +185,7 @@ const rfb::PixelFormat &Viewport::getPreferredPF() void Viewport::updateWindow() { - Rect r; + core::Rect r; r = frameBuffer->getDamage(); damage(FL_DAMAGE_USER1, r.tl.x + x(), r.tl.y + y(), r.width(), r.height()); @@ -182,9 +201,12 @@ static const char * dotcursor_xpm[] = { " ... ", " "}; -void Viewport::setCursor(int width, int height, const Point& hotspot, - const uint8_t* data) +void Viewport::setCursor() { + int width, height; + core::Point hotspot; + const uint8_t* data; + int i; if (cursor) { @@ -193,10 +215,20 @@ void Viewport::setCursor(int width, int height, const Point& hotspot, 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; - if ((i == width*height) && dotWhenNoCursor) { + cursorIsBlank = i == width*height; + + if (cursorIsBlank && alwaysCursor) { + // This is the default in case the local cursor should be displayed yet cursorType is invalid. + // Since the cursor variable isn't used if the cursorType is system, we can do this without checking the current + // type which helps handle changing the type while the viewer is running. vlog.debug("Cursor is empty, using dot"); Fl_Pixmap pxm(dotcursor_xpm); @@ -217,16 +249,36 @@ void Viewport::setCursor(int width, int height, const Point& hotspot, } if (Fl::belowmouse() == this) + showCursor(); +} + +void Viewport::showCursor() +{ + if (viewOnly) { + window()->cursor(FL_CURSOR_DEFAULT); + return; + } + + if (cursorIsBlank && alwaysCursor && (cursorType == "system")) { + window()->cursor(FL_CURSOR_DEFAULT); + } else { window()->cursor(cursor, cursorHotspot.x, cursorHotspot.y); + } } void Viewport::handleClipboardRequest() { + if (viewOnly) + return; + Fl::paste(*this, clipboardSource); } void Viewport::handleClipboardAnnounce(bool available) { + if (viewOnly) + return; + if (!acceptClipboard) return; @@ -281,6 +333,9 @@ void Viewport::setLEDState(unsigned int ledState) return; } + if (viewOnly) + return; + if (!hasFocus()) return; @@ -291,30 +346,36 @@ void Viewport::pushLEDState() { unsigned int ledState; + if (viewOnly) + return; + // Server support? - if (cc->server.ledState() == ledUnknown) + if (cc->server.ledState() == rfb::ledUnknown) return; ledState = keyboard->getLEDState(); - if (ledState == ledUnknown) + if (ledState == rfb::ledUnknown) return; #if defined(__APPLE__) // No support for Scroll Lock // - ledState |= (cc->server.ledState() & ledScrollLock); + ledState |= (cc->server.ledState() & rfb::ledScrollLock); #endif - if ((ledState & ledCapsLock) != (cc->server.ledState() & ledCapsLock)) { + if ((ledState & rfb::ledCapsLock) != + (cc->server.ledState() & rfb::ledCapsLock)) { vlog.debug("Inserting fake CapsLock to get in sync with server"); handleKeyPress(FAKE_KEY_CODE, 0x3a, XK_Caps_Lock); handleKeyRelease(FAKE_KEY_CODE); } - if ((ledState & ledNumLock) != (cc->server.ledState() & ledNumLock)) { + if ((ledState & rfb::ledNumLock) != + (cc->server.ledState() & rfb::ledNumLock)) { vlog.debug("Inserting fake NumLock to get in sync with server"); handleKeyPress(FAKE_KEY_CODE, 0x45, XK_Num_Lock); handleKeyRelease(FAKE_KEY_CODE); } - if ((ledState & ledScrollLock) != (cc->server.ledState() & ledScrollLock)) { + if ((ledState & rfb::ledScrollLock) != + (cc->server.ledState() & rfb::ledScrollLock)) { vlog.debug("Inserting fake ScrollLock to get in sync with server"); handleKeyPress(FAKE_KEY_CODE, 0x46, XK_Scroll_Lock); handleKeyRelease(FAKE_KEY_CODE); @@ -370,12 +431,20 @@ int Viewport::handle(int event) switch (event) { case FL_PASTE: - if (!isValidUTF8(Fl::event_text(), Fl::event_length())) { + if (!core::isValidUTF8(Fl::event_text(), Fl::event_length())) { vlog.error("Invalid UTF-8 sequence in system clipboard"); + // Reset the state as if we don't have any clipboard data at all + this->pendingClientClipboard = false; + try { + this->cc->announceClipboard(false); + } catch (std::exception& e) { + vlog.error("%s", e.what()); + abort_connection_with_unexpected_error(e); + } return 1; } - filtered = convertLF(Fl::event_text(), Fl::event_length()); + filtered = core::convertLF(Fl::event_text(), Fl::event_length()); vlog.debug("Sending clipboard data (%d bytes)", (int)filtered.size()); @@ -389,14 +458,14 @@ int Viewport::handle(int event) return 1; case FL_ENTER: - window()->cursor(cursor, cursorHotspot.x, cursorHotspot.y); + showCursor(); // Yes, we would like some pointer events please! return 1; case FL_LEAVE: window()->cursor(FL_CURSOR_DEFAULT); // We want a last move event to help trigger edge stuff - handlePointerEvent(Point(Fl::event_x() - x(), Fl::event_y() - y()), 0); + handlePointerEvent({Fl::event_x() - x(), Fl::event_y() - y()}, 0); return 1; case FL_PUSH: @@ -439,16 +508,14 @@ int Viewport::handle(int event) // A quick press of the wheel "button", followed by a immediate // release below - handlePointerEvent(Point(Fl::event_x() - x(), Fl::event_y() - y()), + handlePointerEvent({Fl::event_x() - x(), Fl::event_y() - y()}, buttonMask | wheelMask); } - handlePointerEvent(Point(Fl::event_x() - x(), Fl::event_y() - y()), buttonMask); + handlePointerEvent({Fl::event_x() - x(), Fl::event_y() - y()}, buttonMask); return 1; case FL_FOCUS: - Fl::disable_im(); - flushPendingClipboard(); // We may have gotten our lock keys out of sync with the server @@ -467,8 +534,6 @@ int Viewport::handle(int event) case FL_UNFOCUS: // We won't get more key events, so reset our knowledge about keys resetKeyboard(); - - Fl::enable_im(); return 1; case FL_KEYDOWN: @@ -480,7 +545,8 @@ int Viewport::handle(int event) return Fl_Widget::handle(event); } -void Viewport::sendPointerEvent(const rfb::Point& pos, uint16_t buttonMask) +void Viewport::sendPointerEvent(const core::Point& pos, + uint16_t buttonMask) { if (viewOnly) return; @@ -518,6 +584,9 @@ void Viewport::handleClipboardChange(int source, void *data) assert(self); + if (viewOnly) + return; + if (!sendClipboard) return; @@ -530,7 +599,12 @@ void Viewport::handleClipboardChange(int source, void *data) vlog.debug("Got non-plain text in local clipboard, ignoring."); // Reset the state as if we don't have any clipboard data at all self->pendingClientClipboard = false; - self->cc->announceClipboard(false); + try { + self->cc->announceClipboard(false); + } catch (std::exception& e) { + vlog.error("%s", e.what()); + abort_connection_with_unexpected_error(e); + } return; } @@ -540,7 +614,12 @@ void Viewport::handleClipboardChange(int source, void *data) vlog.debug("Local clipboard changed whilst not focused, will notify server later"); self->pendingClientClipboard = true; // Clear any older client clipboard from the server - self->cc->announceClipboard(false); + try { + self->cc->announceClipboard(false); + } catch (std::exception& e) { + vlog.error("%s", e.what()); + abort_connection_with_unexpected_error(e); + } return; } @@ -570,7 +649,8 @@ void Viewport::flushPendingClipboard() } -void Viewport::handlePointerEvent(const rfb::Point& pos, uint16_t buttonMask) +void Viewport::handlePointerEvent(const core::Point& pos, + uint16_t buttonMask) { filterPointerEvent(pos, buttonMask); } @@ -602,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; @@ -633,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; @@ -655,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) @@ -697,15 +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"), (const char *)menuKey); - 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); @@ -716,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 @@ -739,15 +955,15 @@ 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()) - window()->cursor(cursor, cursorHotspot.x, cursorHotspot.y); + if (Fl::belowmouse() == this) + showCursor(); if (m == nullptr) return; @@ -790,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); @@ -822,17 +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(); - // FIXME: Need to recheck cursor for dotWhenNoCursor + if (Fl::belowmouse() == self) + self->showCursor(); } diff --git a/vncviewer/Viewport.h b/vncviewer/Viewport.h index 41696d9d..8e3f473e 100644 --- a/vncviewer/Viewport.h +++ b/vncviewer/Viewport.h @@ -20,12 +20,13 @@ #ifndef __VIEWPORT_H__ #define __VIEWPORT_H__ -#include <rfb/Rect.h> +#include <core/Rect.h> #include <FL/Fl_Widget.H> #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 rfb::Point& hotspot, - const uint8_t* data); + void setCursor(); // Change client LED state void setLEDState(unsigned int state); @@ -71,16 +71,20 @@ public: int handle(int event) override; protected: - void sendPointerEvent(const rfb::Point& pos, uint16_t buttonMask) override; + void sendPointerEvent(const core::Point& pos, + uint16_t buttonMask) override; private: bool hasFocus(); + // Show the currently set (or system) cursor + void showCursor(); + static void handleClipboardChange(int source, void *data); void flushPendingClipboard(); - void handlePointerEvent(const rfb::Point& pos, uint16_t buttonMask); + void handlePointerEvent(const core::Point& pos, uint16_t buttonMask); static void handlePointerTimeout(void *data); void resetKeyboard(); @@ -96,8 +100,6 @@ private: void initContextMenu(); void popupContextMenu(); - void setMenuKey(); - static void handleOptions(void *data); private: @@ -105,10 +107,14 @@ private: PlatformPixelBuffer* frameBuffer; - rfb::Point lastPointerPos; + core::Point lastPointerPos; uint16_t lastButtonMask; Keyboard* keyboard; + ShortcutHandler shortcutHandler; + bool shortcutBypass; + bool shortcutActive; + std::set<int> pressedKeys; bool firstLEDState; @@ -116,15 +122,14 @@ private: int clipboardSource; - uint32_t menuKeySym; - int menuKeyCode, menuKeyFLTK; Fl_Menu_Button *contextMenu; bool menuCtrlKey; bool menuAltKey; Fl_RGB_Image *cursor; - rfb::Point cursorHotspot; + core::Point cursorHotspot; + bool cursorIsBlank; }; #endif diff --git a/vncviewer/Win32TouchHandler.cxx b/vncviewer/Win32TouchHandler.cxx index 2be27ede..2777cca2 100644 --- a/vncviewer/Win32TouchHandler.cxx +++ b/vncviewer/Win32TouchHandler.cxx @@ -24,14 +24,15 @@ #include <stdexcept> +#include <core/LogWriter.h> + #define XK_MISCELLANY #include <rfb/keysymdef.h> -#include <rfb/LogWriter.h> #include "i18n.h" #include "Win32TouchHandler.h" -static rfb::LogWriter vlog("Win32TouchHandler"); +static core::LogWriter vlog("Win32TouchHandler"); static const DWORD MOUSEMOVE_FLAGS = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK; @@ -309,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/XInputTouchHandler.cxx b/vncviewer/XInputTouchHandler.cxx index 2b2e02c6..545a097e 100644 --- a/vncviewer/XInputTouchHandler.cxx +++ b/vncviewer/XInputTouchHandler.cxx @@ -30,16 +30,17 @@ #include <FL/x.H> +#include <core/LogWriter.h> + #ifndef XK_MISCELLANY #define XK_MISCELLANY #include <rfb/keysymdef.h> #endif -#include <rfb/LogWriter.h> #include "i18n.h" #include "XInputTouchHandler.h" -static rfb::LogWriter vlog("XInputTouchHandler"); +static core::LogWriter vlog("XInputTouchHandler"); static bool grabbed = false; diff --git a/vncviewer/cocoa.h b/vncviewer/cocoa.h index 64acefbf..09db9a45 100644 --- a/vncviewer/cocoa.h +++ b/vncviewer/cocoa.h @@ -1,4 +1,4 @@ -/* 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 @@ -21,11 +21,12 @@ class Fl_Window; -int cocoa_get_level(Fl_Window *win); -void cocoa_set_level(Fl_Window *win, int level); +void cocoa_prevent_native_fullscreen(Fl_Window *win); -int cocoa_capture_displays(Fl_Window *win); -void cocoa_release_displays(Fl_Window *win); +bool cocoa_is_trusted(bool prompt=false); + +bool cocoa_tap_keyboard(); +void cocoa_untap_keyboard(); typedef struct CGColorSpace *CGColorSpaceRef; diff --git a/vncviewer/cocoa.mm b/vncviewer/cocoa.mm index 1d63b750..4d9908dd 100644 --- a/vncviewer/cocoa.mm +++ b/vncviewer/cocoa.mm @@ -1,4 +1,4 @@ -/* 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,108 +20,207 @@ #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 <rfb/Rect.h> +#include "cocoa.h" -static bool captured = false; +static CFMachPortRef event_tap; +static CFRunLoopSourceRef tap_source; -int cocoa_get_level(Fl_Window *win) +void cocoa_prevent_native_fullscreen(Fl_Window *win) { NSWindow *nsw; nsw = (NSWindow*)fl_xid(win); - return [nsw level]; + assert(nsw); +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 100700 + nsw.collectionBehavior |= NSWindowCollectionBehaviorFullScreenNone; +#endif } -void cocoa_set_level(Fl_Window *win, int level) +bool cocoa_is_trusted(bool prompt) { - NSWindow *nsw; - nsw = (NSWindow*)fl_xid(win); - [nsw setLevel:level]; -} + CFStringRef keys[1]; + CFBooleanRef values[1]; + CFDictionaryRef options; -int cocoa_capture_displays(Fl_Window *win) -{ - NSWindow *nsw; + Boolean trusted; - nsw = (NSWindow*)fl_xid(win); +#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; - rfb::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); + 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) @@ -130,6 +229,7 @@ CGColorSpaceRef cocoa_win_color_space(Fl_Window *win) NSColorSpace *nscs; nsw = (NSWindow*)fl_xid(win); + assert(nsw); nscs = [nsw colorSpace]; if (nscs == nil) { @@ -150,6 +250,7 @@ bool cocoa_win_is_zoomed(Fl_Window *win) { NSWindow *nsw; nsw = (NSWindow*)fl_xid(win); + assert(nsw); return [nsw isZoomed]; } @@ -157,5 +258,6 @@ void cocoa_win_zoom(Fl_Window *win) { NSWindow *nsw; nsw = (NSWindow*)fl_xid(win); + assert(nsw); [nsw zoom:nsw]; } 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/fltk/layout.h b/vncviewer/fltk/layout.h index 01dc73e6..9a74c234 100644 --- a/vncviewer/fltk/layout.h +++ b/vncviewer/fltk/layout.h @@ -24,6 +24,7 @@ #ifndef __FLTK_LAYOUT_H__ #define __FLTK_LAYOUT_H__ +#include <FL/Fl_Choice.H> #include <FL/fl_draw.H> /* Calculates the width of a string as printed by FLTK (pixels) */ @@ -38,6 +39,18 @@ static inline int gui_str_len(const char *str) return (int)(len + 0.5f); } +/* Adjusts an Fl_Choice so that all options are visible */ +static inline void fltk_adjust_choice(Fl_Choice *choice) +{ + int option_len; + + option_len = 0; + for (int i = 0; i < choice->size(); i++) + option_len = std::max(option_len, gui_str_len(choice->text(i))); + + choice->size(option_len + 30, choice->h()); +} + /**** MARGINS ****/ #define OUTER_MARGIN 15 diff --git a/vncviewer/menukey.cxx b/vncviewer/menukey.cxx deleted file mode 100644 index c12d8c93..00000000 --- a/vncviewer/menukey.cxx +++ /dev/null @@ -1,86 +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) -{ - const char *menuKeyStr; - - menuKeyStr = menuKey; - for(int i = 0; i < getMenuKeySymbolCount(); i++) { - if (!strcmp(menuSymbols[i].name, menuKeyStr)) { - *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 0a211a51..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,16 +30,22 @@ <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> + <!-- developer_name tag deprecated with Appstream 1.0 --> + <developer_name>The TigerVNC team</developer_name> + <developer id="org.tigervnc"> + <name>The TigerVNC team</name> + </developer> + <url type="homepage">https://tigervnc.org</url> </component> diff --git a/vncviewer/parameters.cxx b/vncviewer/parameters.cxx index a6229a34..d2000181 100644 --- a/vncviewer/parameters.cxx +++ b/vncviewer/parameters.cxx @@ -32,13 +32,12 @@ #include "parameters.h" -#include <os/os.h> +#include <core/Exception.h> +#include <core/LogWriter.h> +#include <core/string.h> +#include <core/xdgdirs.h> -#include <rdr/Exception.h> - -#include <rfb/LogWriter.h> #include <rfb/SecurityClient.h> -#include <rfb/util.h> #include <FL/fl_utf8.h> @@ -50,123 +49,199 @@ #include "i18n.h" -using namespace rfb; -using namespace std; - -static LogWriter vlog("Parameters"); - - -IntParameter pointerEventInterval("PointerEventInterval", - "Time in milliseconds to rate-limit" - " successive pointer events", 17); -BoolParameter emulateMiddleButton("EmulateMiddleButton", - "Emulate middle mouse button by pressing " - "left and right mouse buttons simultaneously", - false); -BoolParameter dotWhenNoCursor("DotWhenNoCursor", - "Show the dot cursor when the server sends an " - "invisible cursor", false); - -BoolParameter alertOnFatalError("AlertOnFatalError", - "Give a dialog on connection problems rather " - "than exiting immediately", true); - -BoolParameter reconnectOnError("ReconnectOnError", - "Give a dialog on connection problems rather " - "than exiting immediately and ask for a reconnect.", true); - -StringParameter passwordFile("PasswordFile", - "Password file for VNC authentication", ""); -AliasParameter passwd("passwd", "Alias for PasswordFile", &passwordFile); - -BoolParameter autoSelect("AutoSelect", - "Auto select pixel format and encoding. " - "Default if PreferredEncoding and FullColor are not specified.", - true); -BoolParameter fullColour("FullColor", - "Use full color", true); -AliasParameter fullColourAlias("FullColour", "Alias for FullColor", &fullColour); -IntParameter lowColourLevel("LowColorLevel", - "Color level to use on slow connections. " - "0 = Very Low, 1 = Low, 2 = Medium", 2); -AliasParameter lowColourLevelAlias("LowColourLevel", "Alias for LowColorLevel", &lowColourLevel); -StringParameter preferredEncoding("PreferredEncoding", - "Preferred encoding to use (Tight, ZRLE, Hextile or" - " Raw)", "Tight"); -BoolParameter customCompressLevel("CustomCompressLevel", - "Use custom compression level. " - "Default if CompressLevel is specified.", false); -IntParameter compressLevel("CompressLevel", - "Use specified compression level 0 = Low, 9 = High", - 2); -BoolParameter noJpeg("NoJPEG", - "Disable lossy JPEG compression in Tight encoding.", - false); -IntParameter qualityLevel("QualityLevel", - "JPEG quality level. 0 = Low, 9 = High", - 8); - -BoolParameter maximize("Maximize", "Maximize viewer window", false); -BoolParameter fullScreen("FullScreen", "Enable full screen", false); -StringParameter fullScreenMode("FullScreenMode", "Specify which monitors to use when in full screen. " - "Should be either Current, Selected or All", - "Current"); -BoolParameter fullScreenAllMonitors("FullScreenAllMonitors", - "[DEPRECATED] Enable full screen over all monitors", - false); -MonitorIndicesParameter fullScreenSelectedMonitors("FullScreenSelectedMonitors", - "Use the given list of monitors in full screen" - " when -FullScreenMode=Selected.", - "1"); -StringParameter desktopSize("DesktopSize", - "Reconfigure desktop size on the server on " - "connect (if possible)", ""); -StringParameter geometry("geometry", - "Specify size and position of viewer window", ""); - -BoolParameter listenMode("listen", "Listen for connections from VNC servers", false); - -BoolParameter remoteResize("RemoteResize", - "Dynamically resize the remote desktop size as " - "the size of the local client window changes. " - "(Does not work with all servers)", true); - -BoolParameter viewOnly("ViewOnly", - "Don't send any mouse or keyboard events to the server", - false); -BoolParameter shared("Shared", - "Don't disconnect other viewers upon connection - " - "share the desktop instead", - false); - -BoolParameter acceptClipboard("AcceptClipboard", - "Accept clipboard changes from the server", - true); -BoolParameter sendClipboard("SendClipboard", - "Send clipboard changes to the server", true); +static core::LogWriter vlog("Parameters"); + +core::IntParameter + pointerEventInterval("PointerEventInterval", + "Time in milliseconds to rate-limit successive " + "pointer events", + 17, 0, INT_MAX); +core::BoolParameter + emulateMiddleButton("EmulateMiddleButton", + "Emulate middle mouse button by pressing left " + "and right mouse buttons simultaneously", + false); +core::BoolParameter + dotWhenNoCursor("DotWhenNoCursor", + "[DEPRECATED] Show the dot cursor when the server " + "sends an invisible cursor", + false); +core::BoolParameter + alwaysCursor("AlwaysCursor", + "Show the local cursor when the server sends an " + "invisible cursor", + false); +core::EnumParameter + cursorType("CursorType", + "Specify which cursor type the local cursor should be. " + "Should be either Dot or System", + {"Dot", "System"}, "Dot"); + +core::BoolParameter + alertOnFatalError("AlertOnFatalError", + "Give a dialog on connection problems rather than " + "exiting immediately", + true); + +core::BoolParameter + reconnectOnError("ReconnectOnError", + "Give a dialog on connection problems rather than " + "exiting immediately and ask for a reconnect.", + true); + +core::StringParameter + passwordFile("PasswordFile", + "Password file for VNC authentication", + ""); +core::AliasParameter + passwd("passwd", "Alias for PasswordFile", &passwordFile); + +core::BoolParameter + autoSelect("AutoSelect", + "Auto select pixel format and encoding. Default if " + "PreferredEncoding and FullColor are not specified.", + true); +core::BoolParameter + fullColour("FullColor", "Use full color", true); +core::AliasParameter + fullColourAlias("FullColour", "Alias for FullColor", &fullColour); +core::IntParameter + lowColourLevel("LowColorLevel", + "Color level to use on slow connections. " + "0 = Very Low, 1 = Low, 2 = Medium", + 2, 0, 2); +core::AliasParameter + lowColourLevelAlias("LowColourLevel", + "Alias for LowColorLevel", &lowColourLevel); +core::EnumParameter + preferredEncoding("PreferredEncoding", + "Preferred encoding to use (Tight, ZRLE, Hextile, " +#ifdef HAVE_H264 + "H.264, " +#endif + "or Raw)", + {"Tight", "ZRLE", "Hextile", +#ifdef HAVE_H264 + "H.264", +#endif + "Raw"}, + "Tight"); +core::BoolParameter + customCompressLevel("CustomCompressLevel", + "Use custom compression level. Default if " + "CompressLevel is specified.", + false); +core::IntParameter + compressLevel("CompressLevel", + "Use specified compression level 0 = Low, 9 = High", + 2, 0, 9); +core::BoolParameter + noJpeg("NoJPEG", + "Disable lossy JPEG compression in Tight encoding.", + false); +core::IntParameter + qualityLevel("QualityLevel", + "JPEG quality level. 0 = Low, 9 = High", + 8, 0, 9); + +core::BoolParameter + maximize("Maximize", "Maximize viewer window", false); +core::BoolParameter + fullScreen("FullScreen", "Enable full screen", false); +core::EnumParameter + fullScreenMode("FullScreenMode", + "Specify which monitors to use when in full screen. " + "Should be either Current, Selected or All", + {"Current", "Selected", "All"}, "Current"); + +core::BoolParameter + fullScreenAllMonitors("FullScreenAllMonitors", + "[DEPRECATED] Enable full screen over all " + "monitors", + false); +MonitorIndicesParameter + fullScreenSelectedMonitors("FullScreenSelectedMonitors", + "Use the given list of monitors in full " + "screen when -FullScreenMode=Selected.", + {1}); +core::StringParameter + desktopSize("DesktopSize", + "Reconfigure desktop size on the server on connect (if " + "possible)", + ""); +core::StringParameter + geometry("geometry", + "Specify size and position of viewer window", + ""); + +core::BoolParameter + listenMode("listen", + "Listen for connections from VNC servers", + false); + +core::BoolParameter + remoteResize("RemoteResize", + "Dynamically resize the remote desktop size as the size " + "of the local client window changes. (Does not work " + "with all servers)", + true); + +core::BoolParameter + viewOnly("ViewOnly", + "Don't send any mouse or keyboard events to the server", + false); +core::BoolParameter + shared("Shared", + "Don't disconnect other viewers upon connection - " + "share the desktop instead", + false); + +core::BoolParameter + acceptClipboard("AcceptClipboard", + "Accept clipboard changes from the server", + true); +core::BoolParameter + sendClipboard("SendClipboard", + "Send clipboard changes to the server", + true); #if !defined(WIN32) && !defined(__APPLE__) -BoolParameter setPrimary("SetPrimary", - "Set the primary selection as well as the " - "clipboard selection", true); -BoolParameter sendPrimary("SendPrimary", - "Send the primary selection to the " - "server as well as the clipboard selection", - true); -StringParameter display("display", - "Specifies the X display on which the VNC viewer window should appear.", - ""); +core::BoolParameter + setPrimary("SetPrimary", + "Set the primary selection as well as the clipboard " + "selection", + true); +core::BoolParameter + sendPrimary("SendPrimary", + "Send the primary selection to the server as well as the " + "clipboard selection", + true); +core::StringParameter + display("display", + "Specifies the X display on which the TigerVNC window " + "should appear.", + ""); #endif -StringParameter menuKey("MenuKey", "The key which brings up the popup menu", - "F8"); - -BoolParameter fullscreenSystemKeys("FullscreenSystemKeys", - "Pass special keys (like Alt+Tab) directly " - "to the server when in full-screen mode.", - true); +// 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", + "Pass special keys (like Alt+Tab) directly to " + "the server when in full-screen mode.", + true); #ifndef WIN32 -StringParameter via("via", "Gateway to tunnel via", ""); +core::StringParameter + via("via", "Gateway to tunnel via", ""); #endif static const char* IDENTIFIER_STRING = "TigerVNC Configuration file Version 1.0"; @@ -175,13 +250,13 @@ static const char* IDENTIFIER_STRING = "TigerVNC Configuration file Version 1.0" * We only save the sub set of parameters that can be modified from * the graphical user interface */ -static VoidParameter* parameterArray[] = { +static core::VoidParameter* parameterArray[] = { /* Security */ #ifdef HAVE_GNUTLS - &CSecurityTLS::X509CA, - &CSecurityTLS::X509CRL, + &rfb::CSecurityTLS::X509CA, + &rfb::CSecurityTLS::X509CRL, #endif // HAVE_GNUTLS - &SecurityClient::secTypes, + &rfb::SecurityClient::secTypes, /* Misc. */ &reconnectOnError, &shared, @@ -201,19 +276,22 @@ static VoidParameter* parameterArray[] = { /* Input */ &viewOnly, &emulateMiddleButton, - &dotWhenNoCursor, + &alwaysCursor, + &cursorType, &acceptClipboard, &sendClipboard, #if !defined(WIN32) && !defined(__APPLE__) &sendPrimary, &setPrimary, #endif - &menuKey, - &fullscreenSystemKeys + &fullscreenSystemKeys, + /* Keyboard shortcuts */ + &shortcutModifiers, }; -static VoidParameter* readOnlyParameterArray[] = { - &fullScreenAllMonitors +static core::VoidParameter* readOnlyParameterArray[] = { + &fullScreenAllMonitors, + &dotWhenNoCursor }; // Encoding Table @@ -320,7 +398,7 @@ static void setKeyString(const char *_name, const char *_value, HKEY* hKey) { LONG res = RegSetValueExW(*hKey, name, 0, REG_SZ, (BYTE*)&value, (wcslen(value)+1)*2); if (res != ERROR_SUCCESS) - throw rdr::win32_error("RegSetValueExW", res); + throw core::win32_error("RegSetValueExW", res); } @@ -336,7 +414,7 @@ static void setKeyInt(const char *_name, const int _value, HKEY* hKey) { LONG res = RegSetValueExW(*hKey, name, 0, REG_DWORD, (BYTE*)&value, sizeof(DWORD)); if (res != ERROR_SUCCESS) - throw rdr::win32_error("RegSetValueExW", res); + throw core::win32_error("RegSetValueExW", res); } @@ -357,7 +435,7 @@ static bool getKeyString(const char* _name, char* dest, size_t destSize, HKEY* h if (res != ERROR_SUCCESS){ delete [] value; if (res != ERROR_FILE_NOT_FOUND) - throw rdr::win32_error("RegQueryValueExW", res); + throw core::win32_error("RegQueryValueExW", res); // The value does not exist, defaults will be used. return false; } @@ -394,7 +472,7 @@ static bool getKeyInt(const char* _name, int* dest, HKEY* hKey) { LONG res = RegQueryValueExW(*hKey, name, nullptr, nullptr, (LPBYTE)&value, &dwordsize); if (res != ERROR_SUCCESS){ if (res != ERROR_FILE_NOT_FOUND) - throw rdr::win32_error("RegQueryValueExW", res); + throw core::win32_error("RegQueryValueExW", res); // The value does not exist, defaults will be used. return false; } @@ -414,13 +492,14 @@ static void removeValue(const char* _name, HKEY* hKey) { LONG res = RegDeleteValueW(*hKey, name); if (res != ERROR_SUCCESS) { if (res != ERROR_FILE_NOT_FOUND) - throw rdr::win32_error("RegDeleteValueW", res); + throw core::win32_error("RegDeleteValueW", res); // The value does not exist, no need to remove it. return; } } -void saveHistoryToRegKey(const list<string>& serverHistory) { +void saveHistoryToRegKey(const std::list<std::string>& serverHistory) +{ HKEY hKey; LONG res = RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\TigerVNC\\vncviewer\\history", 0, nullptr, @@ -428,14 +507,14 @@ void saveHistoryToRegKey(const list<string>& serverHistory) { &hKey, nullptr); if (res != ERROR_SUCCESS) - throw rdr::win32_error(_("Failed to create registry key"), res); + throw core::win32_error(_("Failed to create registry key"), res); unsigned index = 0; assert(SERVER_HISTORY_SIZE < 100); char indexString[3]; try { - for (const string& entry : serverHistory) { + for (const std::string& entry : serverHistory) { if (index > SERVER_HISTORY_SIZE) break; snprintf(indexString, 3, "%d", index); @@ -449,7 +528,7 @@ void saveHistoryToRegKey(const list<string>& serverHistory) { res = RegCloseKey(hKey); if (res != ERROR_SUCCESS) - throw rdr::win32_error(_("Failed to close registry key"), res); + throw core::win32_error(_("Failed to close registry key"), res); } static void saveToReg(const char* servername) { @@ -461,57 +540,74 @@ static void saveToReg(const char* servername) { REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, nullptr, &hKey, nullptr); if (res != ERROR_SUCCESS) - throw rdr::win32_error(_("Failed to create registry key"), res); + throw core::win32_error(_("Failed to create registry key"), res); try { setKeyString("ServerName", servername, &hKey); } catch (std::exception& e) { RegCloseKey(hKey); - throw std::runtime_error(format(_("Failed to save \"%s\": %s"), - "ServerName", e.what())); + throw std::runtime_error(core::format( + _("Failed to save \"%s\": %s"), "ServerName", e.what())); } - for (size_t i = 0; i < sizeof(parameterArray)/sizeof(VoidParameter*); i++) { + for (core::VoidParameter* param : parameterArray) { + core::IntParameter* iparam; + core::BoolParameter* bparam; + + if (param->isDefault()) { + try { + removeValue(param->getName(), &hKey); + } catch (std::exception& e) { + RegCloseKey(hKey); + throw std::runtime_error( + core::format(_("Failed to remove \"%s\": %s"), + param->getName(), e.what())); + } + continue; + } + + iparam = dynamic_cast<core::IntParameter*>(param); + bparam = dynamic_cast<core::BoolParameter*>(param); + try { - if (dynamic_cast<StringParameter*>(parameterArray[i]) != nullptr) { - setKeyString(parameterArray[i]->getName(), *(StringParameter*)parameterArray[i], &hKey); - } else if (dynamic_cast<IntParameter*>(parameterArray[i]) != nullptr) { - setKeyInt(parameterArray[i]->getName(), (int)*(IntParameter*)parameterArray[i], &hKey); - } else if (dynamic_cast<BoolParameter*>(parameterArray[i]) != nullptr) { - setKeyInt(parameterArray[i]->getName(), (int)*(BoolParameter*)parameterArray[i], &hKey); + if (iparam != nullptr) { + setKeyInt(iparam->getName(), (int)*(iparam), &hKey); + } else if (bparam != nullptr) { + setKeyInt(bparam->getName(), (int)*(bparam), &hKey); } else { - throw std::logic_error(_("Unknown parameter type")); + setKeyString(param->getName(), param->getValueStr().c_str(), &hKey); } } catch (std::exception& e) { RegCloseKey(hKey); - throw std::runtime_error(format(_("Failed to save \"%s\": %s"), - parameterArray[i]->getName(), - e.what())); + throw std::runtime_error( + core::format(_("Failed to save \"%s\": %s"), + param->getName(), e.what())); } } // Remove read-only parameters to replicate the behaviour of Linux/macOS when they // store a config to disk. If the parameter hasn't been migrated at this point it // will be lost. - for (size_t i = 0; i < sizeof(readOnlyParameterArray)/sizeof(VoidParameter*); i++) { + for (core::VoidParameter* param : readOnlyParameterArray) { try { - removeValue(readOnlyParameterArray[i]->getName(), &hKey); + removeValue(param->getName(), &hKey); } catch (std::exception& e) { RegCloseKey(hKey); - throw std::runtime_error(format(_("Failed to remove \"%s\": %s"), - readOnlyParameterArray[i]->getName(), - e.what())); + throw std::runtime_error( + core::format(_("Failed to remove \"%s\": %s"), + param->getName(), e.what())); } } res = RegCloseKey(hKey); if (res != ERROR_SUCCESS) - throw rdr::win32_error(_("Failed to close registry key"), res); + throw core::win32_error(_("Failed to close registry key"), res); } -list<string> loadHistoryFromRegKey() { +std::list<std::string> loadHistoryFromRegKey() +{ HKEY hKey; - list<string> serverHistory; + std::list<std::string> serverHistory; LONG res = RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\TigerVNC\\vncviewer\\history", 0, @@ -522,7 +618,7 @@ list<string> loadHistoryFromRegKey() { return serverHistory; } - throw rdr::win32_error(_("Failed to open registry key"), res); + throw core::win32_error(_("Failed to open registry key"), res); } unsigned index; @@ -549,31 +645,35 @@ list<string> loadHistoryFromRegKey() { res = RegCloseKey(hKey); if (res != ERROR_SUCCESS) - throw rdr::win32_error(_("Failed to close registry key"), res); + throw core::win32_error(_("Failed to close registry key"), res); return serverHistory; } -static void getParametersFromReg(VoidParameter* parameters[], +static void getParametersFromReg(core::VoidParameter* parameters[], size_t parameters_len, HKEY* hKey) { const size_t buffersize = 256; int intValue = 0; char stringValue[buffersize]; - for (size_t i = 0; i < parameters_len/sizeof(VoidParameter*); i++) { + for (size_t i = 0; i < parameters_len; i++) { + core::IntParameter* iparam; + core::BoolParameter* bparam; + + iparam = dynamic_cast<core::IntParameter*>(parameters[i]); + bparam = dynamic_cast<core::BoolParameter*>(parameters[i]); + try { - if (dynamic_cast<StringParameter*>(parameters[i]) != nullptr) { + if (iparam != nullptr) { + if (getKeyInt(iparam->getName(), &intValue, hKey)) + iparam->setParam(intValue); + } else if (bparam != nullptr) { + if (getKeyInt(bparam->getName(), &intValue, hKey)) + bparam->setParam(intValue); + } else { if (getKeyString(parameters[i]->getName(), stringValue, buffersize, hKey)) parameters[i]->setParam(stringValue); - } else if (dynamic_cast<IntParameter*>(parameters[i]) != nullptr) { - if (getKeyInt(parameters[i]->getName(), &intValue, hKey)) - ((IntParameter*)parameters[i])->setParam(intValue); - } else if (dynamic_cast<BoolParameter*>(parameters[i]) != nullptr) { - if (getKeyInt(parameters[i]->getName(), &intValue, hKey)) - ((BoolParameter*)parameters[i])->setParam(intValue); - } else { - throw std::logic_error(_("Unknown parameter type")); } } catch(std::exception& e) { // Just ignore this entry and continue with the rest @@ -596,7 +696,7 @@ static char* loadFromReg() { return nullptr; } - throw rdr::win32_error(_("Failed to open registry key"), res); + throw core::win32_error(_("Failed to open registry key"), res); } const size_t buffersize = 256; @@ -612,13 +712,18 @@ static char* loadFromReg() { strcpy(servername, ""); } - getParametersFromReg(parameterArray, sizeof(parameterArray), &hKey); + getParametersFromReg(parameterArray, + sizeof(parameterArray) / + sizeof(core::VoidParameter*), + &hKey); getParametersFromReg(readOnlyParameterArray, - sizeof(readOnlyParameterArray), &hKey); + sizeof(readOnlyParameterArray) / + sizeof(core::VoidParameter*), + &hKey); res = RegCloseKey(hKey); if (res != ERROR_SUCCESS) - throw rdr::win32_error(_("Failed to close registry key"), res); + throw core::win32_error(_("Failed to close registry key"), res); return servername; } @@ -639,7 +744,7 @@ void saveViewerParameters(const char *filename, const char *servername) { return; #endif - const char* configDir = os::getvncconfigdir(); + const char* configDir = core::getvncconfigdir(); if (configDir == nullptr) throw std::runtime_error(_("Could not determine VNC config directory path")); @@ -650,78 +755,50 @@ void saveViewerParameters(const char *filename, const char *servername) { /* Write parameters to file */ FILE* f = fopen(filepath, "w+"); - if (!f) { - std::string msg = format(_("Could not open \"%s\""), filepath); - throw rdr::posix_error(msg.c_str(), errno); - } + if (!f) + throw core::posix_error( + core::format(_("Could not open \"%s\""), filepath), errno); fprintf(f, "%s\n", IDENTIFIER_STRING); fprintf(f, "\n"); if (!encodeValue(servername, encodingBuffer, buffersize)) { fclose(f); - throw std::runtime_error(format(_("Failed to save \"%s\": %s"), - "ServerName", - _("Could not encode parameter"))); + throw std::runtime_error( + core::format(_("Failed to save \"%s\": %s"), "ServerName", + _("Could not encode parameter"))); } fprintf(f, "ServerName=%s\n", encodingBuffer); - for (VoidParameter* param : parameterArray) { - if (dynamic_cast<StringParameter*>(param) != nullptr) { - if (!encodeValue(*(StringParameter*)param, - encodingBuffer, buffersize)) { - fclose(f); - throw std::runtime_error(format(_("Failed to save \"%s\": %s"), - param->getName(), - _("Could not encode parameter"))); - } - fprintf(f, "%s=%s\n", ((StringParameter*)param)->getName(), encodingBuffer); - } else if (dynamic_cast<IntParameter*>(param) != nullptr) { - fprintf(f, "%s=%d\n", ((IntParameter*)param)->getName(), (int)*(IntParameter*)param); - } else if (dynamic_cast<BoolParameter*>(param) != nullptr) { - fprintf(f, "%s=%d\n", ((BoolParameter*)param)->getName(), (int)*(BoolParameter*)param); - } else { + for (core::VoidParameter* param : parameterArray) { + if (param->isDefault()) + continue; + if (!encodeValue(param->getValueStr().c_str(), + encodingBuffer, buffersize)) { fclose(f); - throw std::logic_error(format(_("Failed to save \"%s\": %s"), - param->getName(), - _("Unknown parameter type"))); + throw std::runtime_error( + core::format(_("Failed to save \"%s\": %s"), param->getName(), + _("Could not encode parameter"))); } + fprintf(f, "%s=%s\n", param->getName(), encodingBuffer); } fclose(f); } static bool findAndSetViewerParameterFromValue( - VoidParameter* parameters[], size_t parameters_len, + core::VoidParameter* parameters[], size_t parameters_len, char* value, char* line) { const size_t buffersize = 256; char decodingBuffer[buffersize]; // Find and set the correct parameter - for (size_t i = 0; i < parameters_len/sizeof(VoidParameter*); i++) { - - if (dynamic_cast<StringParameter*>(parameters[i]) != nullptr) { - if (strcasecmp(line, ((StringParameter*)parameters[i])->getName()) == 0) { - if(!decodeValue(value, decodingBuffer, sizeof(decodingBuffer))) - throw std::runtime_error(_("Invalid format or too large value")); - ((StringParameter*)parameters[i])->setParam(decodingBuffer); - return false; - } - - } else if (dynamic_cast<IntParameter*>(parameters[i]) != nullptr) { - if (strcasecmp(line, ((IntParameter*)parameters[i])->getName()) == 0) { - ((IntParameter*)parameters[i])->setParam(atoi(value)); - return false; - } - - } else if (dynamic_cast<BoolParameter*>(parameters[i]) != nullptr) { - if (strcasecmp(line, ((BoolParameter*)parameters[i])->getName()) == 0) { - ((BoolParameter*)parameters[i])->setParam(atoi(value)); - return false; - } - - } else { - throw std::logic_error(_("Unknown parameter type")); + for (size_t i = 0; i < parameters_len; i++) { + if (strcasecmp(line, parameters[i]->getName()) == 0) { + if(!decodeValue(value, decodingBuffer, sizeof(decodingBuffer))) + throw std::runtime_error(_("Invalid format or too large value")); + parameters[i]->setParam(decodingBuffer); + return false; } } @@ -745,7 +822,7 @@ char* loadViewerParameters(const char *filename) { return loadFromReg(); #endif - const char* configDir = os::getvncconfigdir(); + const char* configDir = core::getvncconfigdir(); if (configDir == nullptr) throw std::runtime_error(_("Could not determine VNC config directory path")); @@ -759,10 +836,10 @@ char* loadViewerParameters(const char *filename) { if (!f) { if (!filename) return nullptr; // Use defaults. - std::string msg = format(_("Could not open \"%s\""), filepath); - throw rdr::posix_error(msg.c_str(), errno); + throw core::posix_error( + core::format(_("Could not open \"%s\""), filepath), errno); } - + int lineNr = 0; while (!feof(f)) { @@ -773,18 +850,19 @@ char* loadViewerParameters(const char *filename) { break; fclose(f); - std::string msg = format(_("Failed to read line %d in " - "file \"%s\""), lineNr, filepath); - throw rdr::posix_error(msg.c_str(), errno); + throw core::posix_error( + core::format(_("Failed to read line %d in file \"%s\""), + lineNr, filepath), + errno); } if (strlen(line) == (sizeof(line) - 1)) { fclose(f); - throw std::runtime_error(format("%s: %s", - format(_("Failed to read line %d " - "in file \"%s\""), - lineNr, filepath).c_str(), - _("Line too long"))); + std::string msg = core::format(_("Failed to read line %d in " + "file \"%s\""), + lineNr, filepath); + throw std::runtime_error( + core::format("%s: %s", msg.c_str(), _("Line too long"))); } // Make sure that the first line of the file has the file identifier string @@ -793,11 +871,10 @@ char* loadViewerParameters(const char *filename) { continue; fclose(f); - throw std::runtime_error(format(_("Configuration file %s is in " - "an invalid format"), - filepath)); + throw std::runtime_error(core::format( + _("Configuration file %s is in an invalid format"), filepath)); } - + // Skip empty lines and comments if ((line[0] == '\n') || (line[0] == '#') || (line[0] == '\r')) continue; @@ -815,8 +892,10 @@ char* loadViewerParameters(const char *filename) { // Find the parameter value char *value = strchr(line, '='); if (value == nullptr) { - vlog.error(_("Failed to read line %d in file %s: %s"), - lineNr, filepath, _("Invalid format")); + std::string msg = core::format(_("Failed to read line %d in " + "file \"%s\""), + lineNr, filepath); + vlog.error("%s: %s", msg.c_str(), _("Invalid format")); continue; } *value = '\0'; // line only contains the parameter name below. @@ -834,24 +913,34 @@ char* loadViewerParameters(const char *filename) { invalidParameterName = false; } else { - invalidParameterName = findAndSetViewerParameterFromValue(parameterArray, sizeof(parameterArray), - value, line); + invalidParameterName = findAndSetViewerParameterFromValue( + parameterArray, + sizeof(parameterArray) / sizeof(core::VoidParameter *), + value, line); if (invalidParameterName) { - invalidParameterName = findAndSetViewerParameterFromValue(readOnlyParameterArray, sizeof(readOnlyParameterArray), - value, line); + invalidParameterName = findAndSetViewerParameterFromValue( + readOnlyParameterArray, + sizeof(readOnlyParameterArray) / + sizeof(core::VoidParameter *), + value, line); } } } catch(std::exception& e) { // Just ignore this entry and continue with the rest - vlog.error(_("Failed to read line %d in file %s: %s"), - lineNr, filepath, e.what()); + std::string msg = core::format(_("Failed to read line %d in " + "file \"%s\""), + lineNr, filepath); + vlog.error("%s: %s", msg.c_str(), e.what()); continue; } - if (invalidParameterName) - vlog.error(_("Failed to read line %d in file %s: %s"), - lineNr, filepath, _("Unknown parameter")); + if (invalidParameterName) { + std::string msg = core::format(_("Failed to read line %d in " + "file \"%s\""), + lineNr, filepath); + vlog.error("%s: %s", msg.c_str(), _("Unknown parameter")); + } } fclose(f); f = nullptr; diff --git a/vncviewer/parameters.h b/vncviewer/parameters.h index a25c932d..4dc30db6 100644 --- a/vncviewer/parameters.h +++ b/vncviewer/parameters.h @@ -20,7 +20,8 @@ #ifndef __PARAMETERS_H__ #define __PARAMETERS_H__ -#include <rfb/Configuration.h> +#include <core/Configuration.h> + #include "MonitorIndicesParameter.h" #ifdef _WIN32 @@ -31,53 +32,55 @@ #define SERVER_HISTORY_SIZE 20 -extern rfb::IntParameter pointerEventInterval; -extern rfb::BoolParameter emulateMiddleButton; -extern rfb::BoolParameter dotWhenNoCursor; - -extern rfb::StringParameter passwordFile; - -extern rfb::BoolParameter autoSelect; -extern rfb::BoolParameter fullColour; -extern rfb::AliasParameter fullColourAlias; -extern rfb::IntParameter lowColourLevel; -extern rfb::AliasParameter lowColourLevelAlias; -extern rfb::StringParameter preferredEncoding; -extern rfb::BoolParameter customCompressLevel; -extern rfb::IntParameter compressLevel; -extern rfb::BoolParameter noJpeg; -extern rfb::IntParameter qualityLevel; - -extern rfb::BoolParameter maximize; -extern rfb::BoolParameter fullScreen; -extern rfb::StringParameter fullScreenMode; -extern rfb::BoolParameter fullScreenAllMonitors; // deprecated +extern core::IntParameter pointerEventInterval; +extern core::BoolParameter emulateMiddleButton; +extern core::BoolParameter dotWhenNoCursor; // deprecated +extern core::BoolParameter alwaysCursor; +extern core::EnumParameter cursorType; + +extern core::StringParameter passwordFile; + +extern core::BoolParameter autoSelect; +extern core::BoolParameter fullColour; +extern core::AliasParameter fullColourAlias; +extern core::IntParameter lowColourLevel; +extern core::AliasParameter lowColourLevelAlias; +extern core::EnumParameter preferredEncoding; +extern core::BoolParameter customCompressLevel; +extern core::IntParameter compressLevel; +extern core::BoolParameter noJpeg; +extern core::IntParameter qualityLevel; + +extern core::BoolParameter maximize; +extern core::BoolParameter fullScreen; +extern core::EnumParameter fullScreenMode; +extern core::BoolParameter fullScreenAllMonitors; // deprecated extern MonitorIndicesParameter fullScreenSelectedMonitors; -extern rfb::StringParameter desktopSize; -extern rfb::StringParameter geometry; -extern rfb::BoolParameter remoteResize; +extern core::StringParameter desktopSize; +extern core::StringParameter geometry; +extern core::BoolParameter remoteResize; -extern rfb::BoolParameter listenMode; +extern core::BoolParameter listenMode; -extern rfb::BoolParameter viewOnly; -extern rfb::BoolParameter shared; +extern core::BoolParameter viewOnly; +extern core::BoolParameter shared; -extern rfb::BoolParameter acceptClipboard; -extern rfb::BoolParameter setPrimary; -extern rfb::BoolParameter sendClipboard; +extern core::BoolParameter acceptClipboard; +extern core::BoolParameter setPrimary; +extern core::BoolParameter sendClipboard; #if !defined(WIN32) && !defined(__APPLE__) -extern rfb::BoolParameter sendPrimary; -extern rfb::StringParameter display; +extern core::BoolParameter sendPrimary; +extern core::StringParameter display; #endif -extern rfb::StringParameter menuKey; +extern core::EnumListParameter shortcutModifiers; -extern rfb::BoolParameter fullscreenSystemKeys; -extern rfb::BoolParameter alertOnFatalError; -extern rfb::BoolParameter reconnectOnError; +extern core::BoolParameter fullscreenSystemKeys; +extern core::BoolParameter alertOnFatalError; +extern core::BoolParameter reconnectOnError; #ifndef WIN32 -extern rfb::StringParameter via; +extern core::StringParameter via; #endif void saveViewerParameters(const char *filename, const char *servername=nullptr); diff --git a/vncviewer/touch.cxx b/vncviewer/touch.cxx index 572e726e..8b998425 100644 --- a/vncviewer/touch.cxx +++ b/vncviewer/touch.cxx @@ -36,7 +36,7 @@ #include <FL/Fl.H> #include <FL/x.H> -#include <rfb/LogWriter.h> +#include <core/LogWriter.h> #include "i18n.h" #include "vncviewer.h" @@ -49,7 +49,7 @@ #include "touch.h" -static rfb::LogWriter vlog("Touch"); +static core::LogWriter vlog("Touch"); #if !defined(WIN32) && !defined(__APPLE__) static int xi_major; diff --git a/vncviewer/vncviewer.cxx b/vncviewer/vncviewer.cxx index 4efe6e93..324ec632 100644 --- a/vncviewer/vncviewer.cxx +++ b/vncviewer/vncviewer.cxx @@ -35,7 +35,7 @@ #include <sys/stat.h> #ifdef WIN32 -#include <os/winerrno.h> +#include <core/winerrno.h> #include <direct.h> #endif @@ -48,16 +48,18 @@ #include <X11/XKBlib.h> #endif -#include <rfb/Logger_stdio.h> +#include <core/Exception.h> +#include <core/Logger_stdio.h> +#include <core/LogWriter.h> +#include <core/Timer.h> + #ifdef HAVE_GNUTLS #include <rfb/CSecurityTLS.h> #endif -#include <rfb/Hostname.h> -#include <rfb/LogWriter.h> -#include <rfb/Timer.h> -#include <rdr/Exception.h> + +#include <core/xdgdirs.h> + #include <network/TcpSocket.h> -#include <os/os.h> #include <FL/Fl_PNG_Image.H> #include <FL/Fl_Sys_Menu_Bar.H> @@ -79,10 +81,7 @@ #include "win32.h" #endif -static rfb::LogWriter vlog("main"); - -using namespace network; -using namespace rfb; +static core::LogWriter vlog("main"); char vncServerName[VNCSERVERNAMELEN] = { '\0' }; @@ -101,11 +100,11 @@ 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."), - PACKAGE_VERSION, BUILD_TIMESTAMP, 2024); + PACKAGE_VERSION, BUILD_TIMESTAMP, 2025); return buffer; } @@ -171,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()); } @@ -182,12 +181,13 @@ static void mainloop(const char* vncserver, network::Socket* sock) exitMainloop = false; - cc = new CConn(vncserver, sock); + cc = new CConn(); + cc->connect(vncserver, sock); while (!exitMainloop) { int next_timer; - next_timer = Timer::checkTimeouts(); + next_timer = core::Timer::checkTimeouts(); if (next_timer < 0) next_timer = INT_MAX; @@ -242,7 +242,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; } @@ -254,7 +254,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 @@ -263,7 +263,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); } @@ -388,7 +388,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"); @@ -446,7 +446,7 @@ static void usage(const char *programName) } #endif - fprintf(stderr, + fprintf(stderr, _( "\n" "Usage: %s [parameters] [host][:displayNum]\n" " %s [parameters] [host][::port]\n" @@ -454,7 +454,7 @@ static void usage(const char *programName) " %s [parameters] [unix socket]\n" #endif " %s [parameters] -listen [port]\n" - " %s [parameters] [.tigervnc file]\n", + " %s [parameters] [.tigervnc file]\n"), programName, programName, #ifndef WIN32 programName, @@ -462,21 +462,21 @@ static void usage(const char *programName) programName, programName); #if !defined(WIN32) && !defined(__APPLE__) - fprintf(stderr,"\n" + 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" - " man page for details.\n"); + " -geometry geometry - Initial position of the main TigerVNC window. See the\n" + " man page for details.\n")); #endif - fprintf(stderr,"\n" + fprintf(stderr, _("\n" "Parameters can be turned on with -<param> or off with -<param>=0\n" "Parameters which take a value can be specified as " "-<param> <value>\n" "Other valid forms are <param>=<value> -<param>=<value> " "--<param>=<value>\n" - "Parameter names are case-insensitive. The parameters are:\n\n"); - Configuration::listParams(79, 14); + "Parameter names are case-insensitive. The parameters are:\n\n")); + core::Configuration::listParams(79, 14); #ifdef WIN32 // Just wait for the user to kill the console window @@ -528,6 +528,12 @@ migrateDeprecatedOptions() fullScreenMode.setParam("all"); } + if (dotWhenNoCursor) { + vlog.info(_("DotWhenNoCursor is deprecated, set AlwaysCursor to 1 and CursorType to 'Dot' instead")); + + alwaysCursor.setParam(true); + cursorType.setParam("Dot"); + } } static void @@ -535,7 +541,7 @@ create_base_dirs() { const char *dir; - dir = os::getvncconfigdir(); + dir = core::getvncconfigdir(); if (dir == nullptr) { vlog.error(_("Could not determine VNC config directory path")); return; @@ -551,31 +557,31 @@ create_base_dirs() vlog.info(_("%%APPDATA%%\\vnc is deprecated, please switch to the %%APPDATA%%\\TigerVNC location.")); #endif - if (os::mkdir_p(dir, 0755) == -1) { + if (core::mkdir_p(dir, 0755) == -1) { if (errno != EEXIST) vlog.error(_("Could not create VNC config directory \"%s\": %s"), dir, strerror(errno)); } - dir = os::getvncdatadir(); + dir = core::getvncdatadir(); if (dir == nullptr) { vlog.error(_("Could not determine VNC data directory path")); return; } - if (os::mkdir_p(dir, 0755) == -1) { + if (core::mkdir_p(dir, 0755) == -1) { if (errno != EEXIST) vlog.error(_("Could not create VNC data directory \"%s\": %s"), dir, strerror(errno)); } - dir = os::getvncstatedir(); + dir = core::getvncstatedir(); if (dir == nullptr) { vlog.error(_("Could not determine VNC state directory path")); return; } - if (os::mkdir_p(dir, 0755) == -1) { + if (core::mkdir_p(dir, 0755) == -1) { if (errno != EEXIST) vlog.error(_("Could not create VNC state directory \"%s\": %s"), dir, strerror(errno)); @@ -602,7 +608,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); } @@ -610,10 +618,10 @@ static void mktunnel() { const char *gatewayHost; std::string remoteHost; - int localPort = findFreeTcpPort(); + int localPort = network::findFreeTcpPort(); int remotePort; - getHostAndPort(vncServerName, &remoteHost, &remotePort); + network::getHostAndPort(vncServerName, &remoteHost, &remotePort); snprintf(vncServerName, VNCSERVERNAMELEN, "localhost::%d", localPort); vncServerName[VNCSERVERNAMELEN - 1] = '\0'; gatewayHost = (const char*)via; @@ -645,13 +653,13 @@ int main(int argc, char** argv) bind_textdomain_codeset(PACKAGE_NAME, "UTF-8"); bind_textdomain_codeset("libc", "UTF-8"); - rfb::initStdIOLoggers(); + core::initStdIOLoggers(); #ifdef WIN32 - rfb::initFileLogger("C:\\temp\\vncviewer.log"); + core::initFileLogger("C:\\temp\\vncviewer.log"); #else - rfb::initFileLogger("/tmp/vncviewer.log"); + core::initFileLogger("/tmp/vncviewer.log"); #endif - rfb::LogWriter::setLogParams("*:stderr:30"); + core::LogWriter::setLogParams("*:stderr:30"); #ifdef SIGHUP signal(SIGHUP, CleanupSignalHandler); @@ -659,8 +667,6 @@ int main(int argc, char** argv) signal(SIGINT, CleanupSignalHandler); signal(SIGTERM, CleanupSignalHandler); - Configuration::enableViewerParams(); - /* Load the default parameter settings */ char defaultServerName[VNCSERVERNAMELEN] = ""; try { @@ -675,40 +681,40 @@ int main(int argc, char** argv) } for (int i = 1; i < argc;) { - /* We need to resolve an ambiguity for booleans */ - if (argv[i][0] == '-' && i+1 < argc) { - VoidParameter *param; - - param = Configuration::getParam(&argv[i][1]); - if ((param != nullptr) && - (dynamic_cast<BoolParameter*>(param) != nullptr)) { - if ((strcasecmp(argv[i+1], "0") == 0) || - (strcasecmp(argv[i+1], "1") == 0) || - (strcasecmp(argv[i+1], "true") == 0) || - (strcasecmp(argv[i+1], "false") == 0) || - (strcasecmp(argv[i+1], "yes") == 0) || - (strcasecmp(argv[i+1], "no") == 0)) { - param->setParam(argv[i+1]); - i += 2; - continue; - } - } - } + int ret; - if (Configuration::setParam(argv[i])) { - i++; + ret = core::Configuration::handleParamArg(argc, argv, i); + if (ret > 0) { + i += ret; continue; } + if (strcmp(argv[i], "-h") == 0 || + strcmp(argv[i], "--help") == 0) { + usage(argv[0]); + } + + if (strcmp(argv[i], "-v") == 0 || + strcmp(argv[i], "--version") == 0) { + /* We already print version on every start */ + return 0; + } + if (argv[i][0] == '-') { - if (i+1 < argc) { - if (Configuration::setParam(&argv[i][1], argv[i+1])) { - i += 2; - continue; - } - } + fprintf(stderr, "\n"); + fprintf(stderr, _("%s: Unrecognized option '%s'\n"), + argv[0], argv[i]); + fprintf(stderr, _("See '%s --help' for more information.\n"), + argv[0]); + exit(1); + } - usage(argv[0]); + if (vncServerName[0] != '\0') { + fprintf(stderr, "\n"); + fprintf(stderr, _("%s: Extra argument '%s'\n"), argv[0], argv[i]); + fprintf(stderr, _("See '%s --help' for more information.\n"), + argv[0]); + exit(0); } strncpy(vncServerName, argv[i], VNCSERVERNAMELEN); @@ -734,7 +740,7 @@ int main(int argc, char** argv) create_base_dirs(); - Socket *sock = nullptr; + network::Socket* sock = nullptr; #ifndef WIN32 /* Specifying -via and -listen together is nonsense */ @@ -748,7 +754,7 @@ int main(int argc, char** argv) #endif if (listenMode) { - std::list<SocketListener*> listeners; + std::list<network::SocketListener*> listeners; try { int port = 5500; if (isdigit(vncServerName[0])) @@ -764,7 +770,7 @@ int main(int argc, char** argv) while (sock == nullptr) { fd_set rfds; FD_ZERO(&rfds); - for (SocketListener* listener : listeners) + for (network::SocketListener* listener : listeners) FD_SET(listener->getFd(), &rfds); int n = select(FD_SETSIZE, &rfds, nullptr, nullptr, nullptr); @@ -773,11 +779,11 @@ int main(int argc, char** argv) vlog.debug("Interrupted select() system call"); continue; } else { - throw rdr::socket_error("select", errno); + throw core::socket_error("select", errno); } } - for (SocketListener* listener : listeners) + for (network::SocketListener* listener : listeners) if (FD_ISSET(listener->getFd(), &rfds)) { sock = listener->accept(); if (sock) 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 ade45b78..9a8ad001 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 @@ -115,94 +131,62 @@ This can be accessed from the popup menu or from the "Connection details" dialog box. .TP -.B \-display \fIXdisplay\fP -Specifies the X display on which the VNC viewer window should appear. -. -.TP -.B \-geometry \fIgeometry\fP -Initial position of the main VNC viewer 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 -window will be placed by the window manager by default. -. -.TP -.B \-listen \fI[port]\fP -Causes vncviewer to listen on the given port (default 5500) for reverse -connections from a VNC server. WinVNC supports reverse connections initiated -using the 'Add new client' menu option or the '\-connect' command-line option. -Xvnc supports reverse connections with a helper program called -.B vncconfig. -. -.TP -.B \-SecurityTypes \fIsec-types\fP -Specify which security schemes to attempt to use when authenticating with -the server. Valid values are a comma separated list of \fBNone\fP, -\fBVncAuth\fP, \fBPlain\fP, \fBTLSNone\fP, \fBTLSVnc\fP, \fBTLSPlain\fP, -\fBX509None\fP, \fBX509Vnc\fP, \fBX509Plain\fP, \fBRA2\fP, \fBRA2ne\fP, -\fBRA2_256\fP and \fBRA2ne_256\fP. Default is to attempt -every supported scheme. +.B \-AcceptClipboard +Accept clipboard changes from the server. Default is on. . .TP -.B \-passwd, \-PasswordFile \fIpassword-file\fP -If you are on a filesystem which gives you access to the password file used by -the server, you can specify it here to avoid typing it in. It will usually be -\fI$XDG_CONFIG_HOME/tigervnc/passwd\fP, or \fI~/.config/tigervnc/passwd\fP -if the former is unset. +.B \-AlertOnFatalError +Display a dialog with any fatal error before exiting. Default is on. . .TP -.B \-X509CA \fIpath\fP -Path to CA certificate to use when authenticating remote servers using any -of the X509 security schemes (X509None, X509Vnc, etc.). Must be in PEM -format. Default is \fI$XDG_CONFIG_HOME/tigervnc/x509_ca.pem\fP, or -\fI~/.config/tigervnc/x509_ca.pem\fP. +.B \-AlwaysCursor +Show a local cursor when the server sends an invisible cursor. Default is off. . .TP -.B \-X509CRL \fIpath\fP -Path to certificate revocation list to use in conjunction with -\fB-X509CA\fP. Must also be in PEM format. Default is -\fI$XDG_CONFIG_HOME/tigervnc/x509_crl.pem\fP, or -\fI~/.config/tigervnc/x509_crl.pem\fP. +.B \-AutoSelect +Use automatic selection of encoding and pixel format (default is on). Normally +the viewer tests the speed of the connection to the server and chooses the +encoding and pixel format appropriately. Turn it off with \fB-AutoSelect=0\fP. . .TP -.B \-Shared -When you make a connection to a VNC server, all other existing connections are -normally closed. This option requests that they be left open, allowing you to -share the desktop with someone already using it. +.B \-CompressLevel \fIlevel\fP +Use specified lossless compression level. 0 = Low, 9 = High. Default is 2. . .TP -.B \-ViewOnly -Specifies that no keyboard or mouse events should be sent to the server. -Useful if you want to view a desktop without interfering; often needs to be -combined with -.B \-Shared. +.B \-CursorType \fItype\fP +Specify which cursor type to use when a local cursor is shown. It should be +either "Dot", or "System". Ignored if AlwaysCursor is off. +The default is "Dot". . .TP -.B \-AcceptClipboard -Accept clipboard changes from the server. Default is on. +.B \-CustomCompressLevel +Use custom compression level as specified by \fBCompressLevel\fP. Default is +off. . .TP -.B \-SetPrimary -Set the primary selection as well as the clipboard selection. -Default is on. +.B \-DesktopSize \fIwidth\fPx\fIheight\fP +Instead of keeping the existing remote screen size, the client will attempt to +switch to the specified since when connecting. If the server does not support +the SetDesktopSize message then the screen will retain the original size. . .TP -.B \-MaxCutText \fIbytes\fP -The maximum size of a clipboard update that will be accepted from a server. -Default is \fB262144\fP. +.B \-display \fIXdisplay\fP +Specifies the X display on which the TigerVNC window should appear. . .TP -.B \-SendClipboard -Send clipboard changes to the server. Default is on. +.B \-DotWhenNoCursor (DEPRECATED) +Show the dot cursor when the server sends an invisible cursor. Replaced by +\fB-AlwaysCursor\fP and \fB-CursorType=Dot\fP . .TP -.B \-SendPrimary -Send the primary selection to the server as well as the clipboard -selection. Default is on. +.B \-EmulateMiddleButton +Emulate middle mouse button by pressing left and right mouse buttons +simultaneously. Default is off. . .TP -.B \-Maximize -Maximize viewer window. +.B \-FullColor, \-FullColour +Tells the VNC server to send full-color pixels in the best format for this +display. This is default. . .TP .B \-FullScreen @@ -216,7 +200,7 @@ full-screen mode. Replaced by \fB-FullScreenMode=all\fP .TP .B \-FullScreenMode \fImode\fP Specify which monitors to use when in full screen. It should be either "Current", -"Selected" (specified by \fB-FullScreenSelectedMonitors\fP) or "All". +"Selected" (specified by \fB-FullScreenSelectedMonitors\fP) or "All". The default is "Current". . .TP @@ -229,30 +213,37 @@ 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 \-DesktopSize \fIwidth\fPx\fIheight\fP -Instead of keeping the existing remote screen size, the client will attempt to -switch to the specified since when connecting. If the server does not support -the SetDesktopSize message then the screen will retain the original size. +.B \-geometry \fIgeometry\fP +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 +window will be placed by the window manager by default. . .TP -.B \-RemoteResize -Dynamically resize the remote desktop size as the size of the local client -window changes. Note that this may not work with all VNC servers. +.B \-GnuTLSPriority \fIpriority\fP +GnuTLS priority string that controls the TLS session’s handshake algorithms. +See the GnuTLS manual for possible values. Default is \fBNORMAL\fP. . .TP -.B \-AutoSelect -Use automatic selection of encoding and pixel format (default is on). Normally -the viewer tests the speed of the connection to the server and chooses the -encoding and pixel format appropriately. Turn it off with \fB-AutoSelect=0\fP. +.B \-listen \fI[port]\fP +Causes vncviewer to listen on the given port (default 5500) for reverse +connections from a VNC server. WinVNC supports reverse connections initiated +using the 'Add new client' menu option or the '\-connect' command-line option. +Xvnc supports reverse connections with a helper program called +.B vncconfig. . .TP -.B \-FullColor, \-FullColour -Tells the VNC server to send full-color pixels in the best format for this -display. This is default. +.B \-Log \fIlogname\fP:\fIdest\fP:\fIlevel\fP[, ...] +Configures the debug log settings. \fIdest\fP can currently be \fBstderr\fP or +\fBstdout\fP, and \fIlevel\fP is between 0 and 100, 100 meaning most verbose +output. \fIlogname\fP is usually \fB*\fP meaning all, but you can target a +specific source file if you know the name of its "LogWriter". Default is +\fB*:stderr:30\fP. . .TP .B \-LowColorLevel, \-LowColourLevel \fIlevel\fP @@ -263,54 +254,94 @@ vncviewer. If you would like to force vncviewer to use reduced color level use \fB-AutoSelect=0\fP parameter. . .TP -.B \-PreferredEncoding \fIencoding\fP -This option specifies the preferred encoding to use from one of "Tight", "ZRLE", -"hextile" or "raw". +.B \-MaxCutText \fIbytes\fP +The maximum size of a clipboard update that will be accepted from a server. +Default is \fB262144\fP. +. +.TP +.B \-Maximize +Maximize viewer window. . .TP .B \-NoJpeg Disable lossy JPEG compression in Tight encoding. Default is off. . .TP +.B \-passwd, \-PasswordFile \fIpassword-file\fP +If you are on a filesystem which gives you access to the password file used by +the server, you can specify it here to avoid typing it in. It will usually be +\fI$XDG_CONFIG_HOME/tigervnc/passwd\fP, or \fI~/.config/tigervnc/passwd\fP +if the former is unset. +. +.TP +.B \-PointerEventInterval \fItime\fP +Time in milliseconds to rate-limit successive pointer events. Default is +17 ms (60 Hz). +. +.TP +.B \-PreferredEncoding \fIencoding\fP +This option specifies the preferred encoding to use from one of "Tight", +"ZRLE", "Hextile", "H.264", or "Raw". +. +.TP .B \-QualityLevel \fIlevel\fP JPEG quality level. 0 = Low, 9 = High. May be adjusted automatically if \fB-AutoSelect\fP is turned on. Default is 8. . .TP -.B \-CompressLevel \fIlevel\fP -Use specified lossless compression level. 0 = Low, 9 = High. Default is 2. +.B \-ReconnectOnError +Display a dialog with any error and offer the possibility to retry +establishing the connection. In case this is off no dialog to +re-connect will be offered. Default is on. . .TP -.B \-CustomCompressLevel -Use custom compression level. Default if \fBCompressLevel\fP is specified. +.B \-RemoteResize +Dynamically resize the remote desktop size as the size of the local client +window changes. Note that this may not work with all VNC servers. . .TP -.B \-DotWhenNoCursor -Show the dot cursor when the server sends an invisible cursor. Default is off. +.B \-SecurityTypes \fIsec-types\fP +Specify which security schemes to attempt to use when authenticating with +the server. Valid values are a comma separated list of \fBNone\fP, +\fBVncAuth\fP, \fBPlain\fP, \fBTLSNone\fP, \fBTLSVnc\fP, \fBTLSPlain\fP, +\fBX509None\fP, \fBX509Vnc\fP, \fBX509Plain\fP, \fBRA2\fP, \fBRA2ne\fP, +\fBRA2_256\fP and \fBRA2ne_256\fP. Default is to attempt +every supported scheme. . .TP -.B \-PointerEventInterval \fItime\fP -Time in milliseconds to rate-limit successive pointer events. Default is -17 ms (60 Hz). +.B \-SendClipboard +Send clipboard changes to the server. Default is on. . .TP -.B \-EmulateMiddleButton -Emulate middle mouse button by pressing left and right mouse buttons -simultaneously. Default is off. +.B \-SendPrimary +Send the primary selection to the server as well as the clipboard +selection. Default is on. . .TP -.B \-Log \fIlogname\fP:\fIdest\fP:\fIlevel\fP -Configures the debug log settings. \fIdest\fP can currently be \fBstderr\fP or -\fBstdout\fP, and \fIlevel\fP is between 0 and 100, 100 meaning most verbose -output. \fIlogname\fP is usually \fB*\fP meaning all, but you can target a -specific source file if you know the name of its "LogWriter". Default is -\fB*:stderr:30\fP. +.B \-SetPrimary +Set the primary selection as well as the clipboard selection. +Default is on. +. +.TP +.B \-Shared +When you make a connection to a VNC server, all other existing connections are +normally closed. This option requests that they be left open, allowing you to +share the desktop with someone already using it. . .TP -.B \-MenuKey \fIkeysym-name\fP -This option specifies the key which brings up the popup 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. +.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. +. +.TP +.B \-UseIPv6 +Use IPv6 for incoming and outgoing connections. Default is on. . .TP \fB\-via\fR \fIgateway\fR @@ -319,25 +350,36 @@ before connection, connect to the \fIhost\fR through that tunnel (TigerVNC\-specific). By default, this option invokes SSH local port forwarding, assuming that SSH client binary can be accessed as /usr/bin/ssh. Note that when using the \fB\-via\fR option, the host -machine name should be specified as known to the gateway machine, e.g. +machine name should be specified as known to the gateway machine, e.g. "localhost" denotes the \fIgateway\fR, not the machine where vncviewer was launched. The environment variable \fIVNC_VIA_CMD\fR can override the default tunnel command of -\fB/usr/bin/ssh\ -f\ -L\ "$L":"$H":"$R"\ "$G"\ sleep\ 20\fR. The tunnel +\fB/usr/bin/ssh\ \-f\ \-L\ "$L":"$H":"$R"\ "$G"\ sleep\ 20\fR. The tunnel command is executed with the environment variables \fIL\fR, \fIH\fR, \fIR\fR, and \fIG\fR taking the values of the local port number, the remote host, the port number on the remote host, and the gateway machine respectively. . .TP -.B \-AlertOnFatalError -Display a dialog with any fatal error before exiting. Default is on. +.B \-ViewOnly +Specifies that no keyboard or mouse events should be sent to the server. +Useful if you want to view a desktop without interfering; often needs to be +combined with +.B \-Shared. . .TP -.B \-ReconnectOnError -Display a dialog with any error and offer the possibility to retry -establishing the connection. In case this is off no dialog to -re-connect will be offered. Default is on. +.B \-X509CA \fIpath\fP +Path to CA certificate to use when authenticating remote servers using any +of the X509 security schemes (X509None, X509Vnc, etc.). Must be in PEM +format. Default is \fI$XDG_CONFIG_HOME/tigervnc/x509_ca.pem\fP, or +\fI~/.config/tigervnc/x509_ca.pem\fP. +. +.TP +.B \-X509CRL \fIpath\fP +Path to certificate revocation list to use in conjunction with +\fB-X509CA\fP. Must also be in PEM format. Default is +\fI$XDG_CONFIG_HOME/tigervnc/x509_crl.pem\fP, or +\fI~/.config/tigervnc/x509_crl.pem\fP. .SH FILES .TP diff --git a/vncviewer/vncviewer.rc.in b/vncviewer/vncviewer.rc.in index a186ee95..375da7af 100644 --- a/vncviewer/vncviewer.rc.in +++ b/vncviewer/vncviewer.rc.in @@ -42,12 +42,12 @@ 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-2024 TigerVNC Team and many others (see README.rst)\0" + VALUE "LegalCopyright", "Copyright (C) 1999-2025 TigerVNC team and many others (see README.rst)\0" VALUE "LegalTrademarks", "TigerVNC\0" VALUE "OriginalFilename", "vncviewer.exe\0" VALUE "PrivateBuild", "\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... |