diff options
98 files changed, 3497 insertions, 1472 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 578c49c2..f7911cd7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,19 +52,17 @@ jobs: steps: - uses: actions/checkout@v4 - uses: msys2/setup-msys2@v2 + with: + update: true - name: Install dependencies run: | pacman --sync --noconfirm --needed \ make mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake pacman --sync --noconfirm --needed \ - mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-fltk1.3 mingw-w64-x86_64-libjpeg-turbo \ mingw-w64-x86_64-gnutls mingw-w64-x86_64-pixman \ mingw-w64-x86_64-nettle mingw-w64-x86_64-gmp \ mingw-w64-x86_64-gtest - # MSYS2 only packages FLTK 1.4 now: - # https://github.com/msys2/MINGW-packages/issues/22769 - pacman --upgrade --noconfirm --needed \ - https://mirror.msys2.org/mingw/mingw64/mingw-w64-x86_64-fltk-1.3.9-2-any.pkg.tar.zst - name: Configure run: cmake -G "MSYS Makefiles" -DCMAKE_BUILD_TYPE=Debug -S . -B build - name: Build @@ -113,7 +111,7 @@ jobs: - uses: actions/upload-artifact@v4 with: name: macOS - path: build/TigerVNC-*.dmg + path: build/release/TigerVNC-*.dmg - name: Test working-directory: build run: ctest --test-dir tests/unit/ --output-junit test-results.xml diff --git a/BUILDING.txt b/BUILDING.txt index 7e73d72e..4f5c61ad 100644 --- a/BUILDING.txt +++ b/BUILDING.txt @@ -327,9 +327,9 @@ make tarball Create a binary tarball containing the TigerVNC viewer -make servertarball - Create a binary tarball containing both the TigerVNC server and viewer +macOS +----- make dmg diff --git a/CMakeLists.txt b/CMakeLists.txt index 27dac16f..3efbbec9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -278,13 +278,22 @@ if(BUILD_JAVA) add_subdirectory(java) endif() -option(BUILD_VIEWER "Build TigerVNC viewer" ON) +trioption(BUILD_VIEWER "Build TigerVNC viewer") if(BUILD_VIEWER) # Check for FLTK set(FLTK_SKIP_FLUID TRUE) set(FLTK_SKIP_OPENGL TRUE) set(FLTK_SKIP_FORMS TRUE) - find_package(FLTK REQUIRED) + if(BUILD_VIEWER STREQUAL "AUTO") + find_package(FLTK) + else() + find_package(FLTK REQUIRED) + endif() + + if(NOT FLTK_FOUND) + message(WARNING "FLTK NOT found. TigerVNC viewer disabled.") + set(BUILD_VIEWER 0) + endif() if(UNIX AND NOT APPLE) # No proper handling for extra X11 libs that FLTK might need... @@ -308,18 +317,20 @@ if(BUILD_VIEWER) endif() endif() - set(CMAKE_REQUIRED_FLAGS "-Wno-error") - set(CMAKE_REQUIRED_INCLUDES ${FLTK_INCLUDE_DIR}) - set(CMAKE_REQUIRED_LIBRARIES ${FLTK_LIBRARIES}) + if(FLTK_FOUND) + set(CMAKE_REQUIRED_FLAGS "-Wno-error") + set(CMAKE_REQUIRED_INCLUDES ${FLTK_INCLUDE_DIR}) + set(CMAKE_REQUIRED_LIBRARIES ${FLTK_LIBRARIES}) - check_cxx_source_compiles("#include <FL/Fl.H>\n#if FL_MAJOR_VERSION != 1 || FL_MINOR_VERSION != 3\n#error Wrong FLTK version\n#endif\nint main(int, char**) { return 0; }" OK_FLTK_VERSION) - if(NOT OK_FLTK_VERSION) - message(FATAL_ERROR "Incompatible version of FLTK") - endif() + check_cxx_source_compiles("#include <FL/Fl.H>\n#if FL_MAJOR_VERSION != 1 || FL_MINOR_VERSION != 3\n#error Wrong FLTK version\n#endif\nint main(int, char**) { return 0; }" OK_FLTK_VERSION) + if(NOT OK_FLTK_VERSION) + message(FATAL_ERROR "Incompatible version of FLTK") + endif() - set(CMAKE_REQUIRED_FLAGS) - set(CMAKE_REQUIRED_INCLUDES) - set(CMAKE_REQUIRED_LIBRARIES) + set(CMAKE_REQUIRED_FLAGS) + set(CMAKE_REQUIRED_INCLUDES) + set(CMAKE_REQUIRED_LIBRARIES) + endif() endif() # Check for GNUTLS library diff --git a/common/core/Configuration.cxx b/common/core/Configuration.cxx index 129d1b9e..e6affb06 100644 --- a/common/core/Configuration.cxx +++ b/common/core/Configuration.cxx @@ -570,6 +570,10 @@ bool ListParameter<ValueType>::setParam(const char* v) entry.erase(0, entry.find_first_not_of(" \f\n\r\t\v")); entry.erase(entry.find_last_not_of(" \f\n\r\t\v")+1); + // Special case, entire v was just whitespace + if (entry.empty() && (entries.size() == 1)) + break; + if (!decodeEntry(entry.c_str(), &e)) { vlog.error("List parameter %s: Invalid value '%s'", getName(), entry.c_str()); diff --git a/common/core/Configuration.h b/common/core/Configuration.h index 57bc48e1..431dd0c5 100644 --- a/common/core/Configuration.h +++ b/common/core/Configuration.h @@ -86,6 +86,8 @@ namespace core { std::list<VoidParameter*>::iterator begin() { return params.begin(); } std::list<VoidParameter*>::iterator end() { return params.end(); } + // - Returns the number of parameters + int size() { return params.size(); } // - Get the Global Configuration object // NB: This call does NOT lock the Configuration system. diff --git a/common/core/string.cxx b/common/core/string.cxx index 49501a9f..091836db 100644 --- a/common/core/string.cxx +++ b/common/core/string.cxx @@ -64,6 +64,9 @@ namespace core { std::vector<std::string> out; const char *start, *stop; + if (src[0] == '\0') + return out; + start = src; do { stop = strchr(start, delimiter); diff --git a/common/network/Socket.cxx b/common/network/Socket.cxx index f5b44239..7fc39d1e 100644 --- a/common/network/Socket.cxx +++ b/common/network/Socket.cxx @@ -120,7 +120,7 @@ void Socket::shutdown() } isShutdown_ = true; - ::shutdown(getFd(), SHUT_RDWR); + ::shutdown(getFd(), SHUT_WR); } bool Socket::isShutdown() const diff --git a/common/network/TcpSocket.cxx b/common/network/TcpSocket.cxx index e941aa67..bf3a224c 100644 --- a/common/network/TcpSocket.cxx +++ b/common/network/TcpSocket.cxx @@ -698,7 +698,7 @@ TcpFilter::Pattern TcpFilter::parsePattern(const char* p) { if (parts.size() > 2) throw std::invalid_argument("Invalid filter specified"); - if (parts[0].empty()) { + if (parts.empty() || parts[0].empty()) { // Match any address memset (&pattern.address, 0, sizeof (pattern.address)); pattern.address.u.sa.sa_family = AF_UNSPEC; diff --git a/common/rdr/CMakeLists.txt b/common/rdr/CMakeLists.txt index 55c69132..bff947ba 100644 --- a/common/rdr/CMakeLists.txt +++ b/common/rdr/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(rdr STATIC TLSException.cxx TLSInStream.cxx TLSOutStream.cxx + TLSSocket.cxx ZlibInStream.cxx ZlibOutStream.cxx) diff --git a/common/rdr/InStream.h b/common/rdr/InStream.h index 5bec276d..7ad4996f 100644 --- a/common/rdr/InStream.h +++ b/common/rdr/InStream.h @@ -187,9 +187,7 @@ namespace rdr { private: const uint8_t* restorePoint; -#ifdef RFB_INSTREAM_CHECK size_t checkedBytes; -#endif inline void check(size_t bytes) { #ifdef RFB_INSTREAM_CHECK @@ -209,11 +207,7 @@ namespace rdr { protected: - InStream() : restorePoint(nullptr) -#ifdef RFB_INSTREAM_CHECK - ,checkedBytes(0) -#endif - {} + InStream() : restorePoint(nullptr), checkedBytes(0) {} const uint8_t* ptr; const uint8_t* end; }; diff --git a/common/rdr/TLSException.cxx b/common/rdr/TLSException.cxx index a1896af4..8c93a3d3 100644 --- a/common/rdr/TLSException.cxx +++ b/common/rdr/TLSException.cxx @@ -35,11 +35,28 @@ using namespace rdr; #ifdef HAVE_GNUTLS -tls_error::tls_error(const char* s, int err_) noexcept +tls_error::tls_error(const char* s, int err_, int alert_) noexcept : std::runtime_error(core::format("%s: %s (%d)", s, - gnutls_strerror(err_), err_)), - err(err_) + strerror(err_, alert_), err_)), + err(err_), alert(alert_) { } + +const char* tls_error::strerror(int err_, int alert_) const noexcept +{ + const char* msg; + + msg = nullptr; + + if ((alert_ != -1) && + ((err_ == GNUTLS_E_WARNING_ALERT_RECEIVED) || + (err_ == GNUTLS_E_FATAL_ALERT_RECEIVED))) + msg = gnutls_alert_get_name((gnutls_alert_description_t)alert_); + + if (msg == nullptr) + msg = gnutls_strerror(err_); + + return msg; +} #endif /* HAVE_GNUTLS */ diff --git a/common/rdr/TLSException.h b/common/rdr/TLSException.h index b35a675f..75ee94f5 100644 --- a/common/rdr/TLSException.h +++ b/common/rdr/TLSException.h @@ -27,8 +27,10 @@ namespace rdr { class tls_error : public std::runtime_error { public: - int err; - tls_error(const char* s, int err_) noexcept; + int err, alert; + tls_error(const char* s, int err_, int alert_=-1) noexcept; + private: + const char* strerror(int err_, int alert_) const noexcept; }; } diff --git a/common/rdr/TLSInStream.cxx b/common/rdr/TLSInStream.cxx index 223e3ee6..3e5ea2be 100644 --- a/common/rdr/TLSInStream.cxx +++ b/common/rdr/TLSInStream.cxx @@ -1,7 +1,7 @@ /* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. * Copyright (C) 2005 Martin Koegler * Copyright (C) 2010 TigerVNC Team - * Copyright (C) 2012-2021 Pierre Ossman for Cendio AB + * Copyright 2012-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 @@ -23,73 +23,25 @@ #include <config.h> #endif -#include <core/Exception.h> -#include <core/LogWriter.h> - -#include <rdr/TLSException.h> #include <rdr/TLSInStream.h> +#include <rdr/TLSSocket.h> -#include <errno.h> +#ifdef HAVE_GNUTLS -#ifdef HAVE_GNUTLS using namespace rdr; -static core::LogWriter vlog("TLSInStream"); - -ssize_t TLSInStream::pull(gnutls_transport_ptr_t str, void* data, size_t size) -{ - TLSInStream* self= (TLSInStream*) str; - InStream *in = self->in; - - self->streamEmpty = false; - self->saved_exception = nullptr; - - try { - if (!in->hasData(1)) { - self->streamEmpty = true; - gnutls_transport_set_errno(self->session, EAGAIN); - return -1; - } - - if (in->avail() < size) - size = in->avail(); - - in->readBytes((uint8_t*)data, size); - } catch (end_of_stream&) { - return 0; - } catch (std::exception& e) { - core::socket_error* se; - vlog.error("Failure reading TLS data: %s", e.what()); - se = dynamic_cast<core::socket_error*>(&e); - if (se) - gnutls_transport_set_errno(self->session, se->err); - else - gnutls_transport_set_errno(self->session, EINVAL); - self->saved_exception = std::current_exception(); - return -1; - } - - return size; -} - -TLSInStream::TLSInStream(InStream* _in, gnutls_session_t _session) - : session(_session), in(_in) +TLSInStream::TLSInStream(TLSSocket* sock_) + : sock(sock_) { - gnutls_transport_ptr_t recv, send; - - gnutls_transport_set_pull_function(session, pull); - gnutls_transport_get_ptr2(session, &recv, &send); - gnutls_transport_set_ptr2(session, this, send); } TLSInStream::~TLSInStream() { - gnutls_transport_set_pull_function(session, nullptr); } bool TLSInStream::fillBuffer() { - size_t n = readTLS((uint8_t*) end, availSpace()); + size_t n = sock->readTLS((uint8_t*) end, availSpace()); if (n == 0) return false; end += n; @@ -97,35 +49,4 @@ bool TLSInStream::fillBuffer() return true; } -size_t TLSInStream::readTLS(uint8_t* buf, size_t len) -{ - int n; - - while (true) { - streamEmpty = false; - n = gnutls_record_recv(session, (void *) buf, len); - if (n == GNUTLS_E_INTERRUPTED || n == GNUTLS_E_AGAIN) { - // GnuTLS returns GNUTLS_E_AGAIN for a bunch of other scenarios - // other than the pull function returning EAGAIN, so we have to - // double check that the underlying stream really is empty - if (!streamEmpty) - continue; - else - return 0; - } - break; - }; - - if (n == GNUTLS_E_PULL_ERROR) - std::rethrow_exception(saved_exception); - - if (n < 0) - throw tls_error("readTLS", n); - - if (n == 0) - throw end_of_stream(); - - return n; -} - #endif diff --git a/common/rdr/TLSInStream.h b/common/rdr/TLSInStream.h index bc9c74ba..94266e50 100644 --- a/common/rdr/TLSInStream.h +++ b/common/rdr/TLSInStream.h @@ -1,5 +1,6 @@ /* Copyright (C) 2005 Martin Koegler * Copyright (C) 2010 TigerVNC Team + * Copyright 2012-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 @@ -22,28 +23,25 @@ #ifdef HAVE_GNUTLS -#include <gnutls/gnutls.h> #include <rdr/BufferedInStream.h> namespace rdr { + class TLSSocket; + class TLSInStream : public BufferedInStream { public: - TLSInStream(InStream* in, gnutls_session_t session); + TLSInStream(TLSSocket* sock); virtual ~TLSInStream(); private: bool fillBuffer() override; - size_t readTLS(uint8_t* buf, size_t len); - static ssize_t pull(gnutls_transport_ptr_t str, void* data, size_t size); - - gnutls_session_t session; - InStream* in; - bool streamEmpty; - std::exception_ptr saved_exception; + TLSSocket* sock; }; -}; + +} #endif + #endif diff --git a/common/rdr/TLSOutStream.cxx b/common/rdr/TLSOutStream.cxx index c3ae2d0a..ba9d182f 100644 --- a/common/rdr/TLSOutStream.cxx +++ b/common/rdr/TLSOutStream.cxx @@ -1,7 +1,7 @@ /* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. * Copyright (C) 2005 Martin Koegler * Copyright (C) 2010 TigerVNC Team - * Copyright (C) 2012-2021 Pierre Ossman for Cendio AB + * Copyright 2012-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 @@ -23,103 +23,42 @@ #include <config.h> #endif -#include <core/Exception.h> -#include <core/LogWriter.h> - -#include <rdr/TLSException.h> #include <rdr/TLSOutStream.h> - -#include <errno.h> +#include <rdr/TLSSocket.h> #ifdef HAVE_GNUTLS -using namespace rdr; -static core::LogWriter vlog("TLSOutStream"); +using namespace rdr; -ssize_t TLSOutStream::push(gnutls_transport_ptr_t str, const void* data, - size_t size) +TLSOutStream::TLSOutStream(TLSSocket* sock_) + : sock(sock_) { - TLSOutStream* self= (TLSOutStream*) str; - OutStream *out = self->out; - - self->saved_exception = nullptr; - - try { - out->writeBytes((const uint8_t*)data, size); - out->flush(); - } catch (std::exception& e) { - core::socket_error* se; - vlog.error("Failure sending TLS data: %s", e.what()); - se = dynamic_cast<core::socket_error*>(&e); - if (se) - gnutls_transport_set_errno(self->session, se->err); - else - gnutls_transport_set_errno(self->session, EINVAL); - self->saved_exception = std::current_exception(); - return -1; - } - - return size; -} - -TLSOutStream::TLSOutStream(OutStream* _out, gnutls_session_t _session) - : session(_session), out(_out) -{ - gnutls_transport_ptr_t recv, send; - - gnutls_transport_set_push_function(session, push); - gnutls_transport_get_ptr2(session, &recv, &send); - gnutls_transport_set_ptr2(session, recv, this); } TLSOutStream::~TLSOutStream() { -#if 0 - try { -// flush(); - } catch (Exception&) { - } -#endif - gnutls_transport_set_push_function(session, nullptr); } void TLSOutStream::flush() { BufferedOutStream::flush(); - out->flush(); + sock->out->flush(); } void TLSOutStream::cork(bool enable) { BufferedOutStream::cork(enable); - out->cork(enable); + sock->out->cork(enable); } bool TLSOutStream::flushBuffer() { while (sentUpTo < ptr) { - size_t n = writeTLS(sentUpTo, ptr - sentUpTo); + size_t n = sock->writeTLS(sentUpTo, ptr - sentUpTo); sentUpTo += n; } return true; } -size_t TLSOutStream::writeTLS(const uint8_t* data, size_t length) -{ - int n; - - n = gnutls_record_send(session, data, length); - if (n == GNUTLS_E_INTERRUPTED || n == GNUTLS_E_AGAIN) - return 0; - - if (n == GNUTLS_E_PUSH_ERROR) - std::rethrow_exception(saved_exception); - - if (n < 0) - throw tls_error("writeTLS", n); - - return n; -} - #endif diff --git a/common/rdr/TLSOutStream.h b/common/rdr/TLSOutStream.h index 0ae9c460..aa9572ba 100644 --- a/common/rdr/TLSOutStream.h +++ b/common/rdr/TLSOutStream.h @@ -21,14 +21,16 @@ #define __RDR_TLSOUTSTREAM_H__ #ifdef HAVE_GNUTLS -#include <gnutls/gnutls.h> + #include <rdr/BufferedOutStream.h> namespace rdr { + class TLSSocket; + class TLSOutStream : public BufferedOutStream { public: - TLSOutStream(OutStream* out, gnutls_session_t session); + TLSOutStream(TLSSocket* out); virtual ~TLSOutStream(); void flush() override; @@ -36,15 +38,12 @@ namespace rdr { private: bool flushBuffer() override; - size_t writeTLS(const uint8_t* data, size_t length); - static ssize_t push(gnutls_transport_ptr_t str, const void* data, size_t size); - - gnutls_session_t session; - OutStream* out; - std::exception_ptr saved_exception; + TLSSocket* sock; }; -}; + +} #endif + #endif diff --git a/common/rdr/TLSSocket.cxx b/common/rdr/TLSSocket.cxx new file mode 100644 index 00000000..a29e41c1 --- /dev/null +++ b/common/rdr/TLSSocket.cxx @@ -0,0 +1,228 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * Copyright (C) 2005 Martin Koegler + * Copyright (C) 2010 TigerVNC Team + * Copyright (C) 2012-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 + +#include <core/Exception.h> +#include <core/LogWriter.h> + +#include <rdr/InStream.h> +#include <rdr/OutStream.h> +#include <rdr/TLSException.h> +#include <rdr/TLSSocket.h> + +#include <errno.h> + +#ifdef HAVE_GNUTLS + +using namespace rdr; + +static core::LogWriter vlog("TLSSocket"); + +TLSSocket::TLSSocket(InStream* in_, OutStream* out_, + gnutls_session_t session_) + : session(session_), in(in_), out(out_), tlsin(this), tlsout(this) +{ + gnutls_transport_set_pull_function( + session, [](gnutls_transport_ptr_t sock, void* data, size_t size) { + return ((TLSSocket*)sock)->pull(data, size); + }); + gnutls_transport_set_push_function( + session, [](gnutls_transport_ptr_t sock, const void* data, size_t size) { + return ((TLSSocket*)sock)->push(data, size); + }); + gnutls_transport_set_ptr(session, this); +} + +TLSSocket::~TLSSocket() +{ + gnutls_transport_set_pull_function(session, nullptr); + gnutls_transport_set_push_function(session, nullptr); + gnutls_transport_set_ptr(session, nullptr); +} + +bool TLSSocket::handshake() +{ + int err; + + err = gnutls_handshake(session); + if (err != GNUTLS_E_SUCCESS) { + gnutls_alert_description_t alert; + const char* msg; + + if ((err == GNUTLS_E_PULL_ERROR) || (err == GNUTLS_E_PUSH_ERROR)) + std::rethrow_exception(saved_exception); + + alert = gnutls_alert_get(session); + msg = nullptr; + + if ((err == GNUTLS_E_WARNING_ALERT_RECEIVED) || + (err == GNUTLS_E_FATAL_ALERT_RECEIVED)) + msg = gnutls_alert_get_name(alert); + + if (msg == nullptr) + msg = gnutls_strerror(err); + + if (!gnutls_error_is_fatal(err)) { + vlog.debug("Deferring completion of TLS handshake: %s", msg); + return false; + } + + vlog.error("TLS Handshake failed: %s\n", msg); + gnutls_alert_send_appropriate(session, err); + throw rdr::tls_error("TLS Handshake failed", err, alert); + } + + return true; +} + +void TLSSocket::shutdown() +{ + int ret; + + try { + if (tlsout.hasBufferedData()) { + tlsout.cork(false); + tlsout.flush(); + if (tlsout.hasBufferedData()) + vlog.error("Failed to flush remaining socket data on close"); + } + } catch (std::exception& e) { + vlog.error("Failed to flush remaining socket data on close: %s", e.what()); + } + + // FIXME: We can't currently wait for the response, so we only send + // our close and hope for the best + ret = gnutls_bye(session, GNUTLS_SHUT_WR); + if ((ret != GNUTLS_E_SUCCESS) && (ret != GNUTLS_E_INVALID_SESSION)) + vlog.error("TLS shutdown failed: %s", gnutls_strerror(ret)); +} + +size_t TLSSocket::readTLS(uint8_t* buf, size_t len) +{ + int n; + + while (true) { + streamEmpty = false; + n = gnutls_record_recv(session, (void *) buf, len); + if (n == GNUTLS_E_INTERRUPTED || n == GNUTLS_E_AGAIN) { + // GnuTLS returns GNUTLS_E_AGAIN for a bunch of other scenarios + // other than the pull function returning EAGAIN, so we have to + // double check that the underlying stream really is empty + if (!streamEmpty) + continue; + else + return 0; + } + break; + }; + + if (n == GNUTLS_E_PULL_ERROR) + std::rethrow_exception(saved_exception); + + if (n < 0) { + gnutls_alert_send_appropriate(session, n); + throw tls_error("readTLS", n, gnutls_alert_get(session)); + } + + if (n == 0) + throw end_of_stream(); + + return n; +} + +size_t TLSSocket::writeTLS(const uint8_t* data, size_t length) +{ + int n; + + n = gnutls_record_send(session, data, length); + if (n == GNUTLS_E_INTERRUPTED || n == GNUTLS_E_AGAIN) + return 0; + + if (n == GNUTLS_E_PUSH_ERROR) + std::rethrow_exception(saved_exception); + + if (n < 0) { + gnutls_alert_send_appropriate(session, n); + throw tls_error("writeTLS", n, gnutls_alert_get(session)); + } + + return n; +} + +ssize_t TLSSocket::pull(void* data, size_t size) +{ + streamEmpty = false; + saved_exception = nullptr; + + try { + if (!in->hasData(1)) { + streamEmpty = true; + gnutls_transport_set_errno(session, EAGAIN); + return -1; + } + + if (in->avail() < size) + size = in->avail(); + + in->readBytes((uint8_t*)data, size); + } catch (end_of_stream&) { + return 0; + } catch (std::exception& e) { + core::socket_error* se; + vlog.error("Failure reading TLS data: %s", e.what()); + se = dynamic_cast<core::socket_error*>(&e); + if (se) + gnutls_transport_set_errno(session, se->err); + else + gnutls_transport_set_errno(session, EINVAL); + saved_exception = std::current_exception(); + return -1; + } + + return size; +} + +ssize_t TLSSocket::push(const void* data, size_t size) +{ + saved_exception = nullptr; + + try { + out->writeBytes((const uint8_t*)data, size); + out->flush(); + } catch (std::exception& e) { + core::socket_error* se; + vlog.error("Failure sending TLS data: %s", e.what()); + se = dynamic_cast<core::socket_error*>(&e); + if (se) + gnutls_transport_set_errno(session, se->err); + else + gnutls_transport_set_errno(session, EINVAL); + saved_exception = std::current_exception(); + return -1; + } + + return size; +} + +#endif diff --git a/common/rdr/TLSSocket.h b/common/rdr/TLSSocket.h new file mode 100644 index 00000000..ca29f8bc --- /dev/null +++ b/common/rdr/TLSSocket.h @@ -0,0 +1,81 @@ +/* Copyright (C) 2005 Martin Koegler + * Copyright (C) 2010 TigerVNC Team + * Copyright 2012-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 __RDR_TLSSOCKET_H__ +#define __RDR_TLSSOCKET_H__ + +#ifdef HAVE_GNUTLS + +#include <exception> + +#include <gnutls/gnutls.h> + +#include <rdr/TLSInStream.h> +#include <rdr/TLSOutStream.h> + +namespace rdr { + + class InStream; + class OutStream; + + class TLSInStream; + class TLSOutStream; + + class TLSSocket { + public: + TLSSocket(InStream* in, OutStream* out, gnutls_session_t session); + virtual ~TLSSocket(); + + TLSInStream& inStream() { return tlsin; } + TLSOutStream& outStream() { return tlsout; } + + bool handshake(); + void shutdown(); + + protected: + /* Used by the stream classes */ + size_t readTLS(uint8_t* buf, size_t len); + size_t writeTLS(const uint8_t* data, size_t length); + + friend TLSInStream; + friend TLSOutStream; + + private: + ssize_t pull(void* data, size_t size); + ssize_t push(const void* data, size_t size); + + gnutls_session_t session; + + InStream* in; + OutStream* out; + + TLSInStream tlsin; + TLSOutStream tlsout; + + bool streamEmpty; + + std::exception_ptr saved_exception; + }; + +} + +#endif + +#endif diff --git a/common/rfb/CMakeLists.txt b/common/rfb/CMakeLists.txt index e37142f8..23eb5e4f 100644 --- a/common/rfb/CMakeLists.txt +++ b/common/rfb/CMakeLists.txt @@ -76,7 +76,7 @@ if(WIN32) endif(WIN32) if(UNIX AND NOT APPLE) - target_sources(rfb PRIVATE UnixPasswordValidator.cxx pam.c) + target_sources(rfb PRIVATE UnixPasswordValidator.cxx) target_link_libraries(rfb ${PAM_LIBS}) endif() diff --git a/common/rfb/CSecurityTLS.cxx b/common/rfb/CSecurityTLS.cxx index 44f738b4..face6527 100644 --- a/common/rfb/CSecurityTLS.cxx +++ b/common/rfb/CSecurityTLS.cxx @@ -3,7 +3,7 @@ * Copyright (C) 2005 Martin Koegler * Copyright (C) 2010 TigerVNC Team * Copyright (C) 2010 m-privacy GmbH - * Copyright (C) 2012-2021 Pierre Ossman for Cendio AB + * Copyright 2012-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 @@ -43,8 +43,7 @@ #include <rfb/Exception.h> #include <rdr/TLSException.h> -#include <rdr/TLSInStream.h> -#include <rdr/TLSOutStream.h> +#include <rdr/TLSSocket.h> #include <gnutls/x509.h> @@ -75,7 +74,7 @@ static const char* configdirfn(const char* fn) CSecurityTLS::CSecurityTLS(CConnection* cc_, bool _anon) : CSecurity(cc_), session(nullptr), anon_cred(nullptr), cert_cred(nullptr), - anon(_anon), tlsis(nullptr), tlsos(nullptr), + anon(_anon), tlssock(nullptr), rawis(nullptr), rawos(nullptr) { int err = gnutls_global_init(); @@ -85,27 +84,8 @@ CSecurityTLS::CSecurityTLS(CConnection* cc_, bool _anon) void CSecurityTLS::shutdown() { - if (tlsos) { - try { - if (tlsos->hasBufferedData()) { - tlsos->cork(false); - tlsos->flush(); - if (tlsos->hasBufferedData()) - vlog.error("Failed to flush remaining socket data on close"); - } - } catch (std::exception& e) { - vlog.error("Failed to flush remaining socket data on close: %s", e.what()); - } - } - - if (session) { - int ret; - // FIXME: We can't currently wait for the response, so we only send - // our close and hope for the best - ret = gnutls_bye(session, GNUTLS_SHUT_WR); - if ((ret != GNUTLS_E_SUCCESS) && (ret != GNUTLS_E_INVALID_SESSION)) - vlog.error("TLS shutdown failed: %s", gnutls_strerror(ret)); - } + if (tlssock) + tlssock->shutdown(); if (anon_cred) { gnutls_anon_free_client_credentials(anon_cred); @@ -123,13 +103,9 @@ void CSecurityTLS::shutdown() rawos = nullptr; } - if (tlsis) { - delete tlsis; - tlsis = nullptr; - } - if (tlsos) { - delete tlsos; - tlsos = nullptr; + if (tlssock) { + delete tlssock; + tlssock = nullptr; } if (session) { @@ -171,26 +147,18 @@ bool CSecurityTLS::processMsg() setParam(); - // Create these early as they set up the push/pull functions - // for GnuTLS - tlsis = new rdr::TLSInStream(is, session); - tlsos = new rdr::TLSOutStream(os, session); + tlssock = new rdr::TLSSocket(is, os, session); rawis = is; rawos = os; } - int err; - err = gnutls_handshake(session); - if (err != GNUTLS_E_SUCCESS) { - if (!gnutls_error_is_fatal(err)) { - vlog.debug("Deferring completion of TLS handshake: %s", gnutls_strerror(err)); + try { + if (!tlssock->handshake()) return false; - } - - vlog.error("TLS Handshake failed: %s\n", gnutls_strerror (err)); + } catch (std::exception&) { shutdown(); - throw rdr::tls_error("TLS Handshake failed", err); + throw; } vlog.debug("TLS handshake completed with %s", @@ -198,7 +166,7 @@ bool CSecurityTLS::processMsg() checkSession(); - cc->setStreams(tlsis, tlsos); + cc->setStreams(&tlssock->inStream(), &tlssock->outStream()); return true; } @@ -277,6 +245,10 @@ void CSecurityTLS::setParam() vlog.debug("Anonymous session has been set"); } else { + const char* hostname; + size_t len; + bool valid; + ret = gnutls_certificate_allocate_credentials(&cert_cred); if (ret != GNUTLS_E_SUCCESS) throw rdr::tls_error("gnutls_certificate_allocate_credentials()", ret); @@ -294,10 +266,22 @@ void CSecurityTLS::setParam() if (ret != GNUTLS_E_SUCCESS) throw rdr::tls_error("gnutls_credentials_set()", ret); - if (gnutls_server_name_set(session, GNUTLS_NAME_DNS, - client->getServerName(), - strlen(client->getServerName())) != GNUTLS_E_SUCCESS) - vlog.error("Failed to configure the server name for TLS handshake"); + // Only DNS hostnames are allowed, and some servers will reject the + // connection if we provide anything else (e.g. an IPv6 address) + hostname = client->getServerName(); + len = strlen(hostname); + valid = true; + for (size_t i = 0; i < len; i++) { + if (!isalnum(hostname[i]) && hostname[i] != '.') + valid = false; + } + + if (valid) { + if (gnutls_server_name_set(session, GNUTLS_NAME_DNS, + client->getServerName(), + strlen(client->getServerName())) != GNUTLS_E_SUCCESS) + vlog.error("Failed to configure the server name for TLS handshake"); + } vlog.debug("X509 session has been set"); } @@ -324,12 +308,16 @@ void CSecurityTLS::checkSession() if (anon) return; - if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) + if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) { + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_UNSUPPORTED_CERTIFICATE); throw protocol_error("Unsupported certificate type"); + } err = gnutls_certificate_verify_peers2(session, &status); if (err != 0) { vlog.error("Server certificate verification failed: %s", gnutls_strerror(err)); + gnutls_alert_send_appropriate(session, err); throw rdr::tls_error("Server certificate verification()", err); } @@ -346,13 +334,17 @@ void CSecurityTLS::checkSession() GNUTLS_CRT_X509, &status_str, 0); - if (err != GNUTLS_E_SUCCESS) + if (err != GNUTLS_E_SUCCESS) { + gnutls_alert_send_appropriate(session, err); throw rdr::tls_error("Failed to get certificate error description", err); + } error = (const char*)status_str.data; gnutls_free(status_str.data); + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_BAD_CERTIFICATE); throw protocol_error( core::format("Invalid server certificate: %s", error.c_str())); } @@ -361,8 +353,10 @@ void CSecurityTLS::checkSession() GNUTLS_CRT_X509, &status_str, 0); - if (err != GNUTLS_E_SUCCESS) + if (err != GNUTLS_E_SUCCESS) { + gnutls_alert_send_appropriate(session, err); throw rdr::tls_error("Failed to get certificate error description", err); + } vlog.info("Server certificate errors: %s", status_str.data); @@ -372,16 +366,21 @@ void CSecurityTLS::checkSession() /* Process overridable errors later */ cert_list = gnutls_certificate_get_peers(session, &cert_list_size); - if (!cert_list_size) + if (!cert_list_size) { + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_UNSUPPORTED_CERTIFICATE); throw protocol_error("Empty certificate chain"); + } /* Process only server's certificate, not issuer's certificate */ gnutls_x509_crt_t crt; gnutls_x509_crt_init(&crt); err = gnutls_x509_crt_import(crt, &cert_list[0], GNUTLS_X509_FMT_DER); - if (err != GNUTLS_E_SUCCESS) + if (err != GNUTLS_E_SUCCESS) { + gnutls_alert_send_appropriate(session, err); throw rdr::tls_error("Failed to decode server certificate", err); + } if (gnutls_x509_crt_check_hostname(crt, client->getServerName()) == 0) { vlog.info("Server certificate doesn't match given server name"); @@ -420,12 +419,15 @@ void CSecurityTLS::checkSession() if ((known != GNUTLS_E_NO_CERTIFICATE_FOUND) && (known != GNUTLS_E_CERTIFICATE_KEY_MISMATCH)) { + gnutls_alert_send_appropriate(session, known); throw rdr::tls_error("Could not load known hosts database", known); } err = gnutls_x509_crt_print(crt, GNUTLS_CRT_PRINT_ONELINE, &info); - if (err != GNUTLS_E_SUCCESS) + if (err != GNUTLS_E_SUCCESS) { + gnutls_alert_send_appropriate(session, known); throw rdr::tls_error("Could not find certificate to display", err); + } len = strlen((char*)info.data); for (size_t i = 0; i < len - 1; i++) { @@ -456,8 +458,11 @@ void CSecurityTLS::checkSession() if (!cc->showMsgBox(MsgBoxFlags::M_YESNO, "Unknown certificate issuer", - text.c_str())) + text.c_str())) { + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_UNKNOWN_CA); throw auth_cancelled(); + } status &= ~(GNUTLS_CERT_INVALID | GNUTLS_CERT_SIGNER_NOT_FOUND | @@ -478,8 +483,11 @@ void CSecurityTLS::checkSession() if (!cc->showMsgBox(MsgBoxFlags::M_YESNO, "Certificate is not yet valid", - text.c_str())) + text.c_str())) { + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_BAD_CERTIFICATE); throw auth_cancelled(); + } status &= ~GNUTLS_CERT_NOT_ACTIVATED; } @@ -498,8 +506,11 @@ void CSecurityTLS::checkSession() if (!cc->showMsgBox(MsgBoxFlags::M_YESNO, "Expired certificate", - text.c_str())) + text.c_str())) { + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_BAD_CERTIFICATE); throw auth_cancelled(); + } status &= ~GNUTLS_CERT_EXPIRED; } @@ -518,14 +529,19 @@ void CSecurityTLS::checkSession() if (!cc->showMsgBox(MsgBoxFlags::M_YESNO, "Insecure certificate algorithm", - text.c_str())) + text.c_str())) { + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_BAD_CERTIFICATE); throw auth_cancelled(); + } status &= ~GNUTLS_CERT_INSECURE_ALGORITHM; } if (status != 0) { vlog.error("Unhandled certificate problems: 0x%x", status); + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_BAD_CERTIFICATE); throw std::logic_error("Unhandled certificate problems"); } @@ -544,8 +560,11 @@ void CSecurityTLS::checkSession() if (!cc->showMsgBox(MsgBoxFlags::M_YESNO, "Certificate hostname mismatch", - text.c_str())) + text.c_str())) { + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_BAD_CERTIFICATE); throw auth_cancelled(); + } } } else if (known == GNUTLS_E_CERTIFICATE_KEY_MISMATCH) { std::string text; @@ -571,8 +590,11 @@ void CSecurityTLS::checkSession() if (!cc->showMsgBox(MsgBoxFlags::M_YESNO, "Unexpected server certificate", - text.c_str())) + text.c_str())) { + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_UNKNOWN_CA); throw auth_cancelled(); + } status &= ~(GNUTLS_CERT_INVALID | GNUTLS_CERT_SIGNER_NOT_FOUND | @@ -594,8 +616,11 @@ void CSecurityTLS::checkSession() if (!cc->showMsgBox(MsgBoxFlags::M_YESNO, "Unexpected server certificate", - text.c_str())) + text.c_str())) { + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_BAD_CERTIFICATE); throw auth_cancelled(); + } status &= ~GNUTLS_CERT_NOT_ACTIVATED; } @@ -615,8 +640,11 @@ void CSecurityTLS::checkSession() if (!cc->showMsgBox(MsgBoxFlags::M_YESNO, "Unexpected server certificate", - text.c_str())) + text.c_str())) { + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_BAD_CERTIFICATE); throw auth_cancelled(); + } status &= ~GNUTLS_CERT_EXPIRED; } @@ -636,14 +664,19 @@ void CSecurityTLS::checkSession() if (!cc->showMsgBox(MsgBoxFlags::M_YESNO, "Unexpected server certificate", - text.c_str())) + text.c_str())) { + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_BAD_CERTIFICATE); throw auth_cancelled(); + } status &= ~GNUTLS_CERT_INSECURE_ALGORITHM; } if (status != 0) { vlog.error("Unhandled certificate problems: 0x%x", status); + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_BAD_CERTIFICATE); throw std::logic_error("Unhandled certificate problems"); } @@ -663,8 +696,11 @@ void CSecurityTLS::checkSession() if (!cc->showMsgBox(MsgBoxFlags::M_YESNO, "Unexpected server certificate", - text.c_str())) + text.c_str())) { + gnutls_alert_send(session, GNUTLS_AL_FATAL, + GNUTLS_A_BAD_CERTIFICATE); throw auth_cancelled(); + } } } diff --git a/common/rfb/CSecurityTLS.h b/common/rfb/CSecurityTLS.h index 9b70366d..51b7dac1 100644 --- a/common/rfb/CSecurityTLS.h +++ b/common/rfb/CSecurityTLS.h @@ -2,6 +2,7 @@ * Copyright (C) 2004 Red Hat Inc. * Copyright (C) 2005 Martin Koegler * Copyright (C) 2010 TigerVNC Team + * Copyright 2012-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 @@ -34,8 +35,7 @@ namespace rdr { class InStream; class OutStream; - class TLSInStream; - class TLSOutStream; + class TLSSocket; } namespace rfb { @@ -63,8 +63,7 @@ namespace rfb { gnutls_certificate_credentials_t cert_cred; bool anon; - rdr::TLSInStream* tlsis; - rdr::TLSOutStream* tlsos; + rdr::TLSSocket* tlssock; rdr::InStream* rawis; rdr::OutStream* rawos; diff --git a/common/rfb/SSecurityPlain.cxx b/common/rfb/SSecurityPlain.cxx index 06631f81..4fa63250 100644 --- a/common/rfb/SSecurityPlain.cxx +++ b/common/rfb/SSecurityPlain.cxx @@ -113,8 +113,9 @@ bool SSecurityPlain::processMsg() password[plen] = 0; username[ulen] = 0; plen = 0; - if (!valid->validate(sc, username, password)) - throw auth_error("Authentication failed"); + std::string msg = "Authentication failed"; + if (!valid->validate(sc, username, password, msg)) + throw auth_error(msg); } return true; diff --git a/common/rfb/SSecurityPlain.h b/common/rfb/SSecurityPlain.h index f2bc3483..4c030455 100644 --- a/common/rfb/SSecurityPlain.h +++ b/common/rfb/SSecurityPlain.h @@ -29,14 +29,20 @@ namespace rfb { class PasswordValidator { public: - bool validate(SConnection* sc, const char *username, const char *password) - { return validUser(username) ? validateInternal(sc, username, password) : false; } + bool validate(SConnection* sc, + const char *username, + const char *password, + std::string &msg) + { return validUser(username) ? validateInternal(sc, username, password, msg) : false; } static core::StringListParameter plainUsers; virtual ~PasswordValidator() { } protected: - virtual bool validateInternal(SConnection* sc, const char *username, const char *password)=0; + virtual bool validateInternal(SConnection* sc, + const char *username, + const char *password, + std::string &msg) = 0; static bool validUser(const char* username); }; diff --git a/common/rfb/SSecurityRSAAES.cxx b/common/rfb/SSecurityRSAAES.cxx index 6afb52dd..405005ab 100644 --- a/common/rfb/SSecurityRSAAES.cxx +++ b/common/rfb/SSecurityRSAAES.cxx @@ -583,9 +583,10 @@ void SSecurityRSAAES::verifyUserPass() #elif !defined(__APPLE__) UnixPasswordValidator *valid = new UnixPasswordValidator(); #endif - if (!valid->validate(sc, username, password)) { + std::string msg = "Authentication failed"; + if (!valid->validate(sc, username, password, msg)) { delete valid; - throw auth_error("Authentication failed"); + throw auth_error(msg); } delete valid; #else diff --git a/common/rfb/SSecurityTLS.cxx b/common/rfb/SSecurityTLS.cxx index 2e173771..316c2a44 100644 --- a/common/rfb/SSecurityTLS.cxx +++ b/common/rfb/SSecurityTLS.cxx @@ -2,7 +2,7 @@ * Copyright (C) 2004 Red Hat Inc. * Copyright (C) 2005 Martin Koegler * Copyright (C) 2010 TigerVNC Team - * Copyright (C) 2012-2021 Pierre Ossman for Cendio AB + * Copyright 2012-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 @@ -37,8 +37,7 @@ #include <rfb/Exception.h> #include <rdr/TLSException.h> -#include <rdr/TLSInStream.h> -#include <rdr/TLSOutStream.h> +#include <rdr/TLSSocket.h> #include <gnutls/x509.h> @@ -72,7 +71,7 @@ static core::LogWriter vlog("TLS"); SSecurityTLS::SSecurityTLS(SConnection* sc_, bool _anon) : SSecurity(sc_), session(nullptr), anon_cred(nullptr), - cert_cred(nullptr), anon(_anon), tlsis(nullptr), tlsos(nullptr), + cert_cred(nullptr), anon(_anon), tlssock(nullptr), rawis(nullptr), rawos(nullptr) { int ret; @@ -88,27 +87,8 @@ SSecurityTLS::SSecurityTLS(SConnection* sc_, bool _anon) void SSecurityTLS::shutdown() { - if (tlsos) { - try { - if (tlsos->hasBufferedData()) { - tlsos->cork(false); - tlsos->flush(); - if (tlsos->hasBufferedData()) - vlog.error("Failed to flush remaining socket data on close"); - } - } catch (std::exception& e) { - vlog.error("Failed to flush remaining socket data on close: %s", e.what()); - } - } - - if (session) { - int ret; - // FIXME: We can't currently wait for the response, so we only send - // our close and hope for the best - ret = gnutls_bye(session, GNUTLS_SHUT_WR); - if ((ret != GNUTLS_E_SUCCESS) && (ret != GNUTLS_E_INVALID_SESSION)) - vlog.error("TLS shutdown failed: %s", gnutls_strerror(ret)); - } + if (tlssock) + tlssock->shutdown(); #if defined (SSECURITYTLS__USE_DEPRECATED_DH) if (dh_params) { @@ -133,13 +113,9 @@ void SSecurityTLS::shutdown() rawos = nullptr; } - if (tlsis) { - delete tlsis; - tlsis = nullptr; - } - if (tlsos) { - delete tlsos; - tlsos = nullptr; + if (tlssock) { + delete tlssock; + tlssock = nullptr; } if (session) { @@ -185,30 +161,24 @@ bool SSecurityTLS::processMsg() os->writeU8(1); os->flush(); - // Create these early as they set up the push/pull functions - // for GnuTLS - tlsis = new rdr::TLSInStream(is, session); - tlsos = new rdr::TLSOutStream(os, session); + tlssock = new rdr::TLSSocket(is, os, session); rawis = is; rawos = os; } - err = gnutls_handshake(session); - if (err != GNUTLS_E_SUCCESS) { - if (!gnutls_error_is_fatal(err)) { - vlog.debug("Deferring completion of TLS handshake: %s", gnutls_strerror(err)); + try { + if (!tlssock->handshake()) return false; - } - vlog.error("TLS Handshake failed: %s", gnutls_strerror (err)); + } catch (std::exception&) { shutdown(); - throw rdr::tls_error("TLS Handshake failed", err); + throw; } vlog.debug("TLS handshake completed with %s", gnutls_session_get_desc(session)); - sc->setStreams(tlsis, tlsos); + sc->setStreams(&tlssock->inStream(), &tlssock->outStream()); return true; } diff --git a/common/rfb/SSecurityTLS.h b/common/rfb/SSecurityTLS.h index 7e80117c..61eec2a8 100644 --- a/common/rfb/SSecurityTLS.h +++ b/common/rfb/SSecurityTLS.h @@ -2,6 +2,7 @@ * Copyright (C) 2004 Red Hat Inc. * Copyright (C) 2005 Martin Koegler * Copyright (C) 2010 TigerVNC Team + * Copyright 2012-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 @@ -41,8 +42,7 @@ namespace rdr { class InStream; class OutStream; - class TLSInStream; - class TLSOutStream; + class TLSSocket; } namespace rfb { @@ -72,8 +72,7 @@ namespace rfb { bool anon; - rdr::TLSInStream* tlsis; - rdr::TLSOutStream* tlsos; + rdr::TLSSocket* tlssock; rdr::InStream* rawis; rdr::OutStream* rawos; diff --git a/common/rfb/UnixPasswordValidator.cxx b/common/rfb/UnixPasswordValidator.cxx index 36b8dd7a..8239463a 100644 --- a/common/rfb/UnixPasswordValidator.cxx +++ b/common/rfb/UnixPasswordValidator.cxx @@ -22,24 +22,119 @@ #include <config.h> #endif +#include <assert.h> +#include <string.h> +#include <security/pam_appl.h> + #include <core/Configuration.h> +#include <core/LogWriter.h> #include <rfb/UnixPasswordValidator.h> -#include <rfb/pam.h> using namespace rfb; +static core::LogWriter vlog("UnixPasswordValidator"); + static core::StringParameter pamService ("PAMService", "Service name for PAM password validation", "vnc"); core::AliasParameter pam_service("pam_service", "Alias for PAMService", &pamService); -int do_pam_auth(const char *service, const char *username, - const char *password); +std::string UnixPasswordValidator::displayName; + +typedef struct +{ + const char *username; + const char *password; + std::string &msg; +} AuthData; + +#if defined(__sun) +static int pam_callback(int count, struct pam_message **in, + struct pam_response **out, void *ptr) +#else +static int pam_callback(int count, const struct pam_message **in, + struct pam_response **out, void *ptr) +#endif +{ + int i; + AuthData *auth = (AuthData *) ptr; + struct pam_response *resp = + (struct pam_response *) malloc (sizeof (struct pam_response) * count); + + if (!resp && count) + return PAM_CONV_ERR; + + for (i = 0; i < count; i++) { + resp[i].resp_retcode = PAM_SUCCESS; + switch (in[i]->msg_style) { + case PAM_TEXT_INFO: + vlog.info("%s info: %s", (const char *) pamService, in[i]->msg); + auth->msg = in[i]->msg; + resp[i].resp = nullptr; + break; + case PAM_ERROR_MSG: + vlog.error("%s error: %s", (const char *) pamService, in[i]->msg); + auth->msg = in[i]->msg; + resp[i].resp = nullptr; + break; + case PAM_PROMPT_ECHO_ON: /* Send Username */ + resp[i].resp = strdup(auth->username); + break; + case PAM_PROMPT_ECHO_OFF: /* Send Password */ + resp[i].resp = strdup(auth->password); + break; + default: + free(resp); + return PAM_CONV_ERR; + } + } -bool UnixPasswordValidator::validateInternal(SConnection * /*sc*/, + *out = resp; + return PAM_SUCCESS; +} + +bool UnixPasswordValidator::validateInternal(SConnection * /* sc */, const char *username, - const char *password) + const char *password, + std::string &msg) { - return do_pam_auth(pamService, username, password); + int ret; + AuthData auth = { username, password, msg }; + struct pam_conv conv = { + pam_callback, + &auth + }; + pam_handle_t *pamh = nullptr; + ret = pam_start(pamService, username, &conv, &pamh); + if (ret != PAM_SUCCESS) { + /* Can't call pam_strerror() here because the content of pamh undefined */ + vlog.error("pam_start(%s) failed: %d", (const char *) pamService, ret); + return false; + } +#ifdef PAM_XDISPLAY + /* At this point, displayName should never be empty */ + assert(displayName.length() > 0); + /* Pass the display name to PAM modules but PAM_XDISPLAY may not be + * recognized by modules built with old versions of PAM */ + ret = pam_set_item(pamh, PAM_XDISPLAY, displayName.c_str()); + if (ret != PAM_SUCCESS && ret != PAM_BAD_ITEM) { + vlog.error("pam_set_item(PAM_XDISPLAY) failed: %d (%s)", ret, pam_strerror(pamh, ret)); + goto error; + } +#endif + ret = pam_authenticate(pamh, 0); + if (ret != PAM_SUCCESS) { + vlog.error("pam_authenticate() failed: %d (%s)", ret, pam_strerror(pamh, ret)); + goto error; + } + ret = pam_acct_mgmt(pamh, 0); + if (ret != PAM_SUCCESS) { + vlog.error("pam_acct_mgmt() failed: %d (%s)", ret, pam_strerror(pamh, ret)); + goto error; + } + return true; +error: + pam_end(pamh, ret); + return false; } diff --git a/common/rfb/UnixPasswordValidator.h b/common/rfb/UnixPasswordValidator.h index 4d623d6c..a2cc89c5 100644 --- a/common/rfb/UnixPasswordValidator.h +++ b/common/rfb/UnixPasswordValidator.h @@ -26,9 +26,19 @@ namespace rfb { class UnixPasswordValidator: public PasswordValidator { + public: + static void setDisplayName(const std::string& display) { + displayName = display; + } + protected: - bool validateInternal(SConnection * sc, const char *username, - const char *password) override; + bool validateInternal(SConnection *sc, + const char *username, + const char *password, + std::string &msg) override; + + private: + static std::string displayName; }; } diff --git a/common/rfb/WinPasswdValidator.cxx b/common/rfb/WinPasswdValidator.cxx index 84832e81..a6281950 100644 --- a/common/rfb/WinPasswdValidator.cxx +++ b/common/rfb/WinPasswdValidator.cxx @@ -30,7 +30,8 @@ using namespace rfb; // This method will only work for Windows NT, 2000, and XP (and possibly Vista) bool WinPasswdValidator::validateInternal(rfb::SConnection* /*sc*/, const char* username, - const char* password) + const char* password, + std::string & /* msg */) { HANDLE handle; diff --git a/common/rfb/WinPasswdValidator.h b/common/rfb/WinPasswdValidator.h index 340a6234..993cafea 100644 --- a/common/rfb/WinPasswdValidator.h +++ b/common/rfb/WinPasswdValidator.h @@ -21,6 +21,7 @@ #ifndef __RFB_WINPASSWDVALIDATOR_H__ #define __RFB_WINPASSWDVALIDATOR_H__ +#include <string> #include <rfb/SSecurityPlain.h> namespace rfb @@ -30,7 +31,10 @@ namespace rfb WinPasswdValidator() {}; virtual ~WinPasswdValidator() {}; protected: - bool validateInternal(SConnection *sc, const char* username, const char* password) override; + bool validateInternal(SConnection *sc, + const char *username, + const char *password, + std::string &msg) override; }; } diff --git a/common/rfb/pam.c b/common/rfb/pam.c deleted file mode 100644 index f9e5ce74..00000000 --- a/common/rfb/pam.c +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2006 Martin Koegler - * Copyright (C) 2010 TigerVNC Team - * - * 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 <stdlib.h> -#include <string.h> -#include <security/pam_appl.h> - -#include <rfb/pam.h> - -typedef struct -{ - const char *username; - const char *password; -} AuthData; - -#if defined(__sun) -static int pam_callback(int count, struct pam_message **in, - struct pam_response **out, void *ptr) -#else -static int pam_callback(int count, const struct pam_message **in, - struct pam_response **out, void *ptr) -#endif -{ - int i; - AuthData *auth = (AuthData *) ptr; - struct pam_response *resp = - (struct pam_response *) malloc (sizeof (struct pam_response) * count); - - if (!resp && count) - return PAM_CONV_ERR; - - for (i = 0; i < count; i++) { - resp[i].resp_retcode = PAM_SUCCESS; - switch (in[i]->msg_style) { - case PAM_TEXT_INFO: - case PAM_ERROR_MSG: - resp[i].resp = 0; - break; - case PAM_PROMPT_ECHO_ON: /* Send Username */ - resp[i].resp = strdup(auth->username); - break; - case PAM_PROMPT_ECHO_OFF: /* Send Password */ - resp[i].resp = strdup(auth->password); - break; - default: - free(resp); - return PAM_CONV_ERR; - } - } - - *out = resp; - return PAM_SUCCESS; -} - - -int do_pam_auth(const char *service, const char *username, const char *password) -{ - int ret; - AuthData auth = { username, password }; - struct pam_conv conv = { - pam_callback, - &auth - }; - pam_handle_t *h = 0; - ret = pam_start(service, username, &conv, &h); - if (ret == PAM_SUCCESS) - ret = pam_authenticate(h, 0); - if (ret == PAM_SUCCESS) - ret = pam_acct_mgmt(h, 0); - pam_end(h, ret); - - return ret == PAM_SUCCESS ? 1 : 0; -} - diff --git a/common/rfb/pam.h b/common/rfb/pam.h deleted file mode 100644 index d378d19c..00000000 --- a/common/rfb/pam.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2006 Martin Koegler - * Copyright (C) 2010 TigerVNC Team - * - * 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 __RFB_PAM_H__ -#define __RFB_PAM_H__ - -#ifdef __cplusplus -extern "C" { -#endif - -int do_pam_auth(const char *service, const char *username, const char *password); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/contrib/packages/deb/ubuntu-focal/debian/rules b/contrib/packages/deb/ubuntu-focal/debian/rules index 6c3fd051..66089917 100644 --- a/contrib/packages/deb/ubuntu-focal/debian/rules +++ b/contrib/packages/deb/ubuntu-focal/debian/rules @@ -55,6 +55,11 @@ config-stamp: xorg-source-stamp # Add here commands to configure the package. cmake -G"Unix Makefiles" \ -DBUILD_STATIC=off \ + -DENABLE_NLS=ON \ + -DENABLE_H264=ON \ + -DENABLE_GNUTLS=ON \ + -DENABLE_NETTLE=ON \ + -DBUILD_VIEWER=ON \ -DCMAKE_INSTALL_PREFIX:PATH=/usr \ -DCMAKE_INSTALL_LIBEXECDIR:PATH=lib/$(DEB_HOST_MULTIARCH) \ -DCMAKE_INSTALL_UNITDIR:PATH=/lib/systemd/system diff --git a/contrib/packages/deb/ubuntu-jammy/debian/rules b/contrib/packages/deb/ubuntu-jammy/debian/rules index 9ed05508..25e21f5b 100644 --- a/contrib/packages/deb/ubuntu-jammy/debian/rules +++ b/contrib/packages/deb/ubuntu-jammy/debian/rules @@ -55,6 +55,11 @@ config-stamp: xorg-source-stamp # Add here commands to configure the package. cmake -G"Unix Makefiles" \ -DBUILD_STATIC=off \ + -DENABLE_NLS=ON \ + -DENABLE_H264=ON \ + -DENABLE_GNUTLS=ON \ + -DENABLE_NETTLE=ON \ + -DBUILD_VIEWER=ON \ -DCMAKE_INSTALL_PREFIX:PATH=/usr \ -DCMAKE_INSTALL_LIBEXECDIR:PATH=lib/$(DEB_HOST_MULTIARCH) \ -DCMAKE_INSTALL_UNITDIR:PATH=/lib/systemd/system diff --git a/contrib/packages/deb/ubuntu-noble/debian/rules b/contrib/packages/deb/ubuntu-noble/debian/rules index 9ed05508..25e21f5b 100644 --- a/contrib/packages/deb/ubuntu-noble/debian/rules +++ b/contrib/packages/deb/ubuntu-noble/debian/rules @@ -55,6 +55,11 @@ config-stamp: xorg-source-stamp # Add here commands to configure the package. cmake -G"Unix Makefiles" \ -DBUILD_STATIC=off \ + -DENABLE_NLS=ON \ + -DENABLE_H264=ON \ + -DENABLE_GNUTLS=ON \ + -DENABLE_NETTLE=ON \ + -DBUILD_VIEWER=ON \ -DCMAKE_INSTALL_PREFIX:PATH=/usr \ -DCMAKE_INSTALL_LIBEXECDIR:PATH=lib/$(DEB_HOST_MULTIARCH) \ -DCMAKE_INSTALL_UNITDIR:PATH=/lib/systemd/system diff --git a/contrib/packages/rpm/el8/SPECS/tigervnc.spec b/contrib/packages/rpm/el8/SPECS/tigervnc.spec index 307e26f6..8f4519e7 100644 --- a/contrib/packages/rpm/el8/SPECS/tigervnc.spec +++ b/contrib/packages/rpm/el8/SPECS/tigervnc.spec @@ -139,7 +139,11 @@ export CFLAGS="$RPM_OPT_FLAGS -fpic" %endif export CXXFLAGS="$CFLAGS -std=c++11" -%cmake +%cmake \ + -DENABLE_NLS=ON \ + -DENABLE_GNUTLS=ON \ + -DENABLE_NETTLE=ON \ + -DBUILD_VIEWER=ON %cmake_build diff --git a/contrib/packages/rpm/el9/SPECS/tigervnc.spec b/contrib/packages/rpm/el9/SPECS/tigervnc.spec index 5d120bc0..a3953c99 100644 --- a/contrib/packages/rpm/el9/SPECS/tigervnc.spec +++ b/contrib/packages/rpm/el9/SPECS/tigervnc.spec @@ -138,7 +138,11 @@ export CFLAGS="$RPM_OPT_FLAGS -fpic" %endif export CXXFLAGS="$CFLAGS -std=c++11" -%cmake +%cmake \ + -DENABLE_NLS=ON \ + -DENABLE_GNUTLS=ON \ + -DENABLE_NETTLE=ON \ + -DBUILD_VIEWER=ON %cmake_build diff --git a/java/com/tigervnc/vncviewer/MANIFEST.MF b/java/com/tigervnc/vncviewer/MANIFEST.MF index 9e282655..5f67f81c 100644 --- a/java/com/tigervnc/vncviewer/MANIFEST.MF +++ b/java/com/tigervnc/vncviewer/MANIFEST.MF @@ -1,5 +1,5 @@ Manifest-Version: 1.0 Main-Class: com.tigervnc.vncviewer.VncViewer -Application-Name: TigerVNC viewer +Application-Name: TigerVNC Permissions: all-permissions Codebase: * diff --git a/java/com/tigervnc/vncviewer/OptionsDialog.java b/java/com/tigervnc/vncviewer/OptionsDialog.java index ccf27327..9c442cbc 100644 --- a/java/com/tigervnc/vncviewer/OptionsDialog.java +++ b/java/com/tigervnc/vncviewer/OptionsDialog.java @@ -177,7 +177,7 @@ class OptionsDialog extends Dialog { @SuppressWarnings({"rawtypes","unchecked"}) public OptionsDialog() { super(true); - setTitle("VNC viewer options"); + setTitle("TigerVNC options"); setResizable(false); getContentPane().setLayout( diff --git a/java/com/tigervnc/vncviewer/ServerDialog.java b/java/com/tigervnc/vncviewer/ServerDialog.java index 01f91db2..9c92698e 100644 --- a/java/com/tigervnc/vncviewer/ServerDialog.java +++ b/java/com/tigervnc/vncviewer/ServerDialog.java @@ -47,7 +47,7 @@ class ServerDialog extends Dialog implements Runnable { super(true); this.vncServerName = vncServerName; setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); - setTitle("VNC viewer: Connection details"); + setTitle("TigerVNC"); setResizable(false); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { @@ -258,7 +258,7 @@ class ServerDialog extends Dialog implements Runnable { JOptionPane op = new JOptionPane(msg, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION, null, options, options[1]); - JDialog dlg = op.createDialog(this, "TigerVNC Viewer"); + JDialog dlg = op.createDialog(this, "TigerVNC"); dlg.setIconImage(VncViewer.frameIcon); dlg.setAlwaysOnTop(true); dlg.setVisible(true); diff --git a/java/com/tigervnc/vncviewer/Viewport.java b/java/com/tigervnc/vncviewer/Viewport.java index 0f614162..3c6f623e 100644 --- a/java/com/tigervnc/vncviewer/Viewport.java +++ b/java/com/tigervnc/vncviewer/Viewport.java @@ -647,7 +647,7 @@ class Viewport extends JPanel implements ActionListener { this, ID.OPTIONS, EnumSet.noneOf(MENU.class)); menu_add(contextMenu, "Connection info...", KeyEvent.VK_I, this, ID.INFO, EnumSet.noneOf(MENU.class)); - menu_add(contextMenu, "About TigerVNC viewer...", KeyEvent.VK_T, + menu_add(contextMenu, "About TigerVNC...", KeyEvent.VK_T, this, ID.ABOUT, EnumSet.of(MENU.DIVIDER)); menu_add(contextMenu, "Dismiss menu", KeyEvent.VK_M, diff --git a/java/com/tigervnc/vncviewer/VncViewer.java b/java/com/tigervnc/vncviewer/VncViewer.java index ea2c2125..2a372b98 100644 --- a/java/com/tigervnc/vncviewer/VncViewer.java +++ b/java/com/tigervnc/vncviewer/VncViewer.java @@ -361,7 +361,7 @@ public class VncViewer implements Runnable { JOptionPane op = new JOptionPane(msg, JOptionPane.INFORMATION_MESSAGE, JOptionPane.DEFAULT_OPTION, VncViewer.logoIcon, options); - JDialog dlg = op.createDialog(parent, "About TigerVNC viewer for Java"); + JDialog dlg = op.createDialog(parent, "About TigerVNC"); dlg.setIconImage(VncViewer.frameIcon); dlg.setAlwaysOnTop(true); dlg.setVisible(true); @@ -380,7 +380,7 @@ public class VncViewer implements Runnable { void reportException(java.lang.Exception e) { String title, msg = e.getMessage(); int msgType = JOptionPane.ERROR_MESSAGE; - title = "TigerVNC viewer : Error"; + title = "TigerVNC : Error"; e.printStackTrace(); JOptionPane.showMessageDialog(null, msg, title, msgType); } diff --git a/po/CMakeLists.txt b/po/CMakeLists.txt index 7d316e78..03fb2632 100644 --- a/po/CMakeLists.txt +++ b/po/CMakeLists.txt @@ -23,6 +23,7 @@ if (GETTEXT_XGETTEXT_EXECUTABLE) "--directory=${PROJECT_SOURCE_DIR}" "--output=${CMAKE_CURRENT_SOURCE_DIR}/tigervnc.pot" --default-domain=tigervnc + --from-code=UTF-8 --keyword=_ --keyword=p_:1c,2 --keyword=N_ @@ -1,14 +1,14 @@ # Serbian translation for tigervnc. # Copyright © 2020 the TigerVNC Team (msgids) # This file is distributed under the same license as the tigervnc package. -# МироÑлав Ðиколић <miroslavnikolic@rocketmail.com>, 2016-2024. +# МироÑлав Ðиколић <miroslavnikolic@rocketmail.com>, 2016-2025. # msgid "" msgstr "" -"Project-Id-Version: tigervnc-1.13.90\n" +"Project-Id-Version: tigervnc-1.14.90\n" "Report-Msgid-Bugs-To: tigervnc-devel@googlegroups.com\n" -"POT-Creation-Date: 2024-06-20 15:01+0200\n" -"PO-Revision-Date: 2024-12-15 19:41+0100\n" +"POT-Creation-Date: 2025-01-14 16:15+0100\n" +"PO-Revision-Date: 2025-05-18 09:05+0200\n" "Last-Translator: МироÑлав Ðиколић <miroslavnikolic@rocketmail.com>\n" "Language-Team: Serbian <(nothing)>\n" "Language: sr\n" @@ -19,17 +19,17 @@ msgstr "" "X-Bugs: Report translation errors to the Language-Team address.\n" "X-Generator: Poedit 3.5\n" -#: vncviewer/CConn.cxx:99 +#: vncviewer/CConn.cxx:102 #, c-format msgid "Connected to socket %s" msgstr "Повезан на прикључницу „%s“" -#: vncviewer/CConn.cxx:106 +#: vncviewer/CConn.cxx:109 #, c-format msgid "Connected to host %s port %d" msgstr "Повезан Ñа домаћином „%s“ прикључник %d" -#: vncviewer/CConn.cxx:111 +#: vncviewer/CConn.cxx:114 #, c-format msgid "" "Failed to connect to \"%s\":\n" @@ -40,85 +40,101 @@ msgstr "" "\n" "%s" -#: vncviewer/CConn.cxx:155 +#: vncviewer/CConn.cxx:151 #, c-format msgid "Desktop name: %.80s" msgstr "Ðазив радне површи: %.80s" -#: vncviewer/CConn.cxx:160 +#: vncviewer/CConn.cxx:154 #, c-format msgid "Host: %.80s port: %d" msgstr "Домаћин: %.80s прикључник: %d" -#: vncviewer/CConn.cxx:165 +#: vncviewer/CConn.cxx:158 #, c-format msgid "Size: %d x %d" msgstr "Величина: %d x %d" -#: vncviewer/CConn.cxx:173 +#: vncviewer/CConn.cxx:165 #, c-format msgid "Pixel format: %s" msgstr "Формат пикÑела: %s" -#: vncviewer/CConn.cxx:180 +#: vncviewer/CConn.cxx:170 #, c-format msgid "(server default %s)" msgstr "(оÑновно на Ñерверу %s)" -#: vncviewer/CConn.cxx:185 +#: vncviewer/CConn.cxx:173 #, c-format msgid "Requested encoding: %s" msgstr "Затражено кодирање: %s" -#: vncviewer/CConn.cxx:190 +#: vncviewer/CConn.cxx:177 #, c-format msgid "Last used encoding: %s" msgstr "ПоÑледње коришћено кодирање: %s" -#: vncviewer/CConn.cxx:195 +#: vncviewer/CConn.cxx:181 #, c-format msgid "Line speed estimate: %d kbit/s" msgstr "Процењена брзина линије: %d kbit/s" -#: vncviewer/CConn.cxx:200 +#: vncviewer/CConn.cxx:185 #, c-format msgid "Protocol version: %d.%d" msgstr "Издања протокола: %d.%d" -#: vncviewer/CConn.cxx:205 +#: vncviewer/CConn.cxx:189 #, c-format msgid "Security method: %s" msgstr "Метода безбедноÑти: %s" -#: vncviewer/CConn.cxx:266 vncviewer/CConn.cxx:268 +#: vncviewer/CConn.cxx:250 vncviewer/CConn.cxx:252 msgid "The connection was dropped by the server before the session could be established." msgstr "Сервер је одбацио везу пре него ли је ÑеÑија могла да Ñе уÑпоÑтави." -#: vncviewer/CConn.cxx:326 +#: vncviewer/CConn.cxx:262 +#, c-format +msgid "Authentication failed: %s" +msgstr "Потврђивање идентитета није уÑпело: %s" + +#: vncviewer/CConn.cxx:263 +#, c-format +msgid "" +"Failed to authenticate with the server. Reason given by the server:\n" +"\n" +"%s" +msgstr "" +"ÐиÑам уÑпео да потврдим идентитет Ñа Ñервером. Разлог који је дао Ñервер:\n" +"\n" +"%s" + +#: vncviewer/CConn.cxx:335 #, c-format msgid "SetDesktopSize failed: %d" msgstr "ÐеуÑпело подешавање величине радне површи: %d" -#: vncviewer/CConn.cxx:399 +#: vncviewer/CConn.cxx:408 msgid "Invalid SetColourMapEntries from server!" msgstr "ÐеиÑправни уноÑи подешавања мапе боје Ñа Ñервера!" -#: vncviewer/CConn.cxx:507 +#: vncviewer/CConn.cxx:516 #, c-format msgid "Throughput %d kbit/s - changing to quality %d" msgstr "ПропуÑноÑÑ‚ је %d kbit/s — мењам на квалитет %d" -#: vncviewer/CConn.cxx:529 +#: vncviewer/CConn.cxx:538 #, c-format msgid "Throughput %d kbit/s - full color is now enabled" msgstr "ПропуÑноÑÑ‚ је %d kbit/s — пуна боја је Ñада омогућена" -#: vncviewer/CConn.cxx:532 +#: vncviewer/CConn.cxx:541 #, c-format msgid "Throughput %d kbit/s - full color is now disabled" msgstr "ПропуÑноÑÑ‚ је %d kbit/s — пуна боја је Ñада онемогућена" -#: vncviewer/CConn.cxx:558 +#: vncviewer/CConn.cxx:567 #, c-format msgid "Using pixel format %s" msgstr "КориÑтим формат пикÑела %s" @@ -131,21 +147,21 @@ msgstr "Ðаведена је неиÑправна геометрија!" msgid "Reducing window size to fit on current monitor" msgstr "Смањујем величину прозора да Ñтане на текући монитор" -#: vncviewer/DesktopWindow.cxx:648 +#: vncviewer/DesktopWindow.cxx:646 msgid "Adjusting window size to avoid accidental full-screen request" msgstr "Прилагођавам величину прозора да би Ñе избегли Ñлучајни захтеви за целим екраном" -#: vncviewer/DesktopWindow.cxx:696 +#: vncviewer/DesktopWindow.cxx:694 #, c-format msgid "Press %s to open the context menu" msgstr "ПритиÑните „%s“ да отворите приручни изборник" -#: vncviewer/DesktopWindow.cxx:1097 vncviewer/DesktopWindow.cxx:1105 -#: vncviewer/DesktopWindow.cxx:1125 +#: vncviewer/DesktopWindow.cxx:1094 vncviewer/DesktopWindow.cxx:1102 +#: vncviewer/DesktopWindow.cxx:1122 msgid "Failure grabbing keyboard" msgstr "ÐеуÑпех хватања таÑтатуре" -#: vncviewer/DesktopWindow.cxx:1415 +#: vncviewer/DesktopWindow.cxx:1411 msgid "Invalid screen layout computed for resize request!" msgstr "Прорачунат је неодговарајући раÑпоред екрана за захтев промене величине!" @@ -153,251 +169,307 @@ msgstr "Прорачунат је неодговарајући раÑпоред msgid "Invalid state for 3 button emulation" msgstr "ÐеиÑправно Ñтање за опонашање 3 дугмета" +#: vncviewer/KeyboardWin32.cxx:242 +#, c-format +msgid "No scan code for extended virtual key 0x%02x" +msgstr "Ðема шифре прегледа за проширени виртуелни кључ 0x%02x" + +#: vncviewer/KeyboardWin32.cxx:244 +#, c-format +msgid "No scan code for virtual key 0x%02x" +msgstr "Ðема шифре прегледа за виртуелни кључ 0x%02x" + +#: vncviewer/KeyboardWin32.cxx:250 +#, c-format +msgid "Invalid scan code 0x%02x" +msgstr "ÐеиÑправан код Ñкенирања 0x%02x" + +#: vncviewer/KeyboardWin32.cxx:262 +#, c-format +msgid "No symbol for extended virtual key 0x%02x" +msgstr "Ðема Ñимбола за проширени виртуелни кључ 0x%02x" + +#: vncviewer/KeyboardWin32.cxx:264 +#, c-format +msgid "No symbol for virtual key 0x%02x" +msgstr "Ðема Ñимбола за виртуелни кључ 0x%02x" + +#: vncviewer/KeyboardWin32.cxx:423 +#, c-format +msgid "Failed to update keyboard LED state: %lu" +msgstr "ÐиÑам уÑпео да оÑвежим Ñтање диоде таÑтатуре: %lu" + +#: vncviewer/KeyboardX11.cxx:104 +#, c-format +msgid "No symbol for key code %d (in the current state)" +msgstr "Ðема Ñимбола за шифру кључа %d (у текућем Ñтању)" + +#: vncviewer/KeyboardX11.cxx:129 +#, c-format +msgid "Failed to get keyboard LED state: %d" +msgstr "ÐиÑам уÑпео да добавим Ñтање диоде таÑтатуре: %d" + +#: vncviewer/KeyboardX11.cxx:174 +msgid "Failed to update keyboard LED state" +msgstr "ÐиÑам уÑпео да оÑвежим Ñтање диоде таÑтатуре" + #: vncviewer/MonitorIndicesParameter.cxx:52 -#: vncviewer/MonitorIndicesParameter.cxx:102 +#: vncviewer/MonitorIndicesParameter.cxx:100 msgid "Failed to get system monitor configuration" msgstr "ÐиÑам уÑпео да добавим подешавање монитора ÑиÑтема" -#: vncviewer/MonitorIndicesParameter.cxx:80 +#: vncviewer/MonitorIndicesParameter.cxx:79 #, c-format msgid "Invalid configuration specified for %s" msgstr "ÐеиÑправно подешавање је наведено за „%s“" -#: vncviewer/MonitorIndicesParameter.cxx:88 +#: vncviewer/MonitorIndicesParameter.cxx:86 #, c-format msgid "Monitor index %d does not exist" msgstr "Ð˜Ð½Ð´ÐµÐºÑ Ð¼Ð¾Ð½Ð¸Ñ‚Ð¾Ñ€Ð° %d не поÑтоји" -#: vncviewer/MonitorIndicesParameter.cxx:166 -#: vncviewer/MonitorIndicesParameter.cxx:186 +#: vncviewer/MonitorIndicesParameter.cxx:162 +#: vncviewer/MonitorIndicesParameter.cxx:182 #, c-format msgid "Invalid monitor index '%s'" msgstr "ÐеиÑправан Ð¸Ð½Ð´ÐµÐºÑ Ð¼Ð¾Ð½Ð¸Ñ‚Ð¾Ñ€Ð° „%s“" -#: vncviewer/MonitorIndicesParameter.cxx:174 +#: vncviewer/MonitorIndicesParameter.cxx:170 #, c-format msgid "Unexpected character '%c'" msgstr "Ðеочекивани знак „%c“" #: vncviewer/OptionsDialog.cxx:64 -msgid "TigerVNC Options" +msgid "TigerVNC options" msgstr "Опције ТигарВÐЦ-а" -#: vncviewer/OptionsDialog.cxx:97 vncviewer/ServerDialog.cxx:102 -#: vncviewer/vncviewer.cxx:395 +#: vncviewer/OptionsDialog.cxx:97 vncviewer/ServerDialog.cxx:107 +#: vncviewer/vncviewer.cxx:397 msgid "Cancel" msgstr "Откажи" -#: vncviewer/OptionsDialog.cxx:102 vncviewer/vncviewer.cxx:394 +#: vncviewer/OptionsDialog.cxx:102 vncviewer/vncviewer.cxx:396 msgid "OK" msgstr "У реду" -#: vncviewer/OptionsDialog.cxx:502 +#: vncviewer/OptionsDialog.cxx:514 msgid "Compression" msgstr "Сажимање" -#: vncviewer/OptionsDialog.cxx:518 +#: vncviewer/OptionsDialog.cxx:530 msgid "Auto select" msgstr "Сам изабери" -#: vncviewer/OptionsDialog.cxx:529 +#: vncviewer/OptionsDialog.cxx:541 msgid "Preferred encoding" msgstr "Жељено кодирање" -#: vncviewer/OptionsDialog.cxx:590 +#: vncviewer/OptionsDialog.cxx:602 msgid "Color level" msgstr "Ðиво боје" -#: vncviewer/OptionsDialog.cxx:602 +#: vncviewer/OptionsDialog.cxx:614 msgid "Full" msgstr "Пуна" -#: vncviewer/OptionsDialog.cxx:609 +#: vncviewer/OptionsDialog.cxx:621 msgid "Medium" msgstr "Средња" -#: vncviewer/OptionsDialog.cxx:616 +#: vncviewer/OptionsDialog.cxx:628 msgid "Low" msgstr "Слаба" -#: vncviewer/OptionsDialog.cxx:623 +#: vncviewer/OptionsDialog.cxx:635 msgid "Very low" msgstr "Врло Ñлаба" -#: vncviewer/OptionsDialog.cxx:645 +#: vncviewer/OptionsDialog.cxx:657 msgid "Custom compression level:" msgstr "Произвољни ниво Ñажимања:" -#: vncviewer/OptionsDialog.cxx:652 +#: vncviewer/OptionsDialog.cxx:664 msgid "level (0=fast, 9=best)" msgstr "ниво (0=брзо, 9=најбоље)" -#: vncviewer/OptionsDialog.cxx:659 +#: vncviewer/OptionsDialog.cxx:671 msgid "Allow JPEG compression:" msgstr "Дозволи ЈПЕГ Ñажимање:" -#: vncviewer/OptionsDialog.cxx:666 +#: vncviewer/OptionsDialog.cxx:678 msgid "quality (0=poor, 9=best)" msgstr "квалитет (0=лош, 9=најбољи)" -#: vncviewer/OptionsDialog.cxx:677 +#: vncviewer/OptionsDialog.cxx:689 msgid "Security" msgstr "БезбедноÑÑ‚" -#: vncviewer/OptionsDialog.cxx:691 +#: vncviewer/OptionsDialog.cxx:703 msgid "Encryption" msgstr "Шифровање" -#: vncviewer/OptionsDialog.cxx:703 vncviewer/OptionsDialog.cxx:770 -#: vncviewer/OptionsDialog.cxx:876 +#: vncviewer/OptionsDialog.cxx:715 vncviewer/OptionsDialog.cxx:782 +#: vncviewer/OptionsDialog.cxx:905 msgid "None" msgstr "Ðишта" -#: vncviewer/OptionsDialog.cxx:710 +#: vncviewer/OptionsDialog.cxx:722 msgid "TLS with anonymous certificates" msgstr "ТЛС Ñа анонимним уверењима" -#: vncviewer/OptionsDialog.cxx:716 +#: vncviewer/OptionsDialog.cxx:728 msgid "TLS with X509 certificates" msgstr "ТЛС Ñа X509 уверењима" -#: vncviewer/OptionsDialog.cxx:723 +#: vncviewer/OptionsDialog.cxx:735 msgid "Path to X509 CA certificate" msgstr "Путања до X509 уверења" -#: vncviewer/OptionsDialog.cxx:730 +#: vncviewer/OptionsDialog.cxx:742 msgid "Path to X509 CRL file" msgstr "Путања до X509 ЦРЛ датотеке" -#: vncviewer/OptionsDialog.cxx:758 +#: vncviewer/OptionsDialog.cxx:770 msgid "Authentication" msgstr "Потврђивање идентитета" -#: vncviewer/OptionsDialog.cxx:776 +#: vncviewer/OptionsDialog.cxx:788 msgid "Standard VNC (insecure without encryption)" msgstr "Стандардни Ð’ÐЦ (неÑигурно без шифровања)" -#: vncviewer/OptionsDialog.cxx:782 +#: vncviewer/OptionsDialog.cxx:794 msgid "Username and password (insecure without encryption)" msgstr "КориÑник и лозинка (неÑигурно без шифровања)" -#: vncviewer/OptionsDialog.cxx:805 +#: vncviewer/OptionsDialog.cxx:822 msgid "Input" msgstr "Улаз" -#: vncviewer/OptionsDialog.cxx:818 +#: vncviewer/OptionsDialog.cxx:835 msgid "View only (ignore mouse and keyboard)" msgstr "Само преглед (занемари миша и таÑтатуру)" -#: vncviewer/OptionsDialog.cxx:825 +#: vncviewer/OptionsDialog.cxx:842 msgid "Mouse" msgstr "Миш" -#: vncviewer/OptionsDialog.cxx:837 +#: vncviewer/OptionsDialog.cxx:854 msgid "Emulate middle mouse button" msgstr "Опонашај Ñредње дугме миша" -#: vncviewer/OptionsDialog.cxx:843 -msgid "Show dot when no cursor" -msgstr "Прикажи тачку када нема курзора" +#: vncviewer/OptionsDialog.cxx:860 +msgid "Show local cursor when not provided by server" +msgstr "Прикажи локални курзор када га Ñервер не доÑтави" + +#: vncviewer/OptionsDialog.cxx:865 +msgid "Cursor type" +msgstr "Ð’Ñ€Ñта курзора" + +#: vncviewer/OptionsDialog.cxx:867 +msgid "Dot" +msgstr "Тачка" + +#: vncviewer/OptionsDialog.cxx:868 +msgid "System" +msgstr "СиÑтем" -#: vncviewer/OptionsDialog.cxx:859 +#: vncviewer/OptionsDialog.cxx:888 msgid "Keyboard" msgstr "ТаÑтатура" -#: vncviewer/OptionsDialog.cxx:871 +#: vncviewer/OptionsDialog.cxx:900 msgid "Pass system keys directly to server (full screen)" msgstr "ПроÑледи ÑиÑтемÑке кључеве директно на Ñервер (пун екран)" -#: vncviewer/OptionsDialog.cxx:874 +#: vncviewer/OptionsDialog.cxx:903 msgid "Menu key" msgstr "ТаÑтер изборника" -#: vncviewer/OptionsDialog.cxx:895 +#: vncviewer/OptionsDialog.cxx:926 msgid "Clipboard" msgstr "ОÑтава" -#: vncviewer/OptionsDialog.cxx:907 +#: vncviewer/OptionsDialog.cxx:938 msgid "Accept clipboard from server" msgstr "Прихвати оÑтаву Ñа Ñервера" -#: vncviewer/OptionsDialog.cxx:915 +#: vncviewer/OptionsDialog.cxx:946 msgid "Also set primary selection" msgstr "Такође поÑтави први избор" -#: vncviewer/OptionsDialog.cxx:922 +#: vncviewer/OptionsDialog.cxx:953 msgid "Send clipboard to server" msgstr "Пошаљи оÑтаву на Ñервер" -#: vncviewer/OptionsDialog.cxx:930 +#: vncviewer/OptionsDialog.cxx:961 msgid "Send primary selection as clipboard" msgstr "Пошаљи први избор као оÑтаву" -#: vncviewer/OptionsDialog.cxx:951 +#: vncviewer/OptionsDialog.cxx:982 msgid "Display" msgstr "Приказ" -#: vncviewer/OptionsDialog.cxx:965 +#: vncviewer/OptionsDialog.cxx:996 msgid "Display mode" msgstr "Режим приказа" -#: vncviewer/OptionsDialog.cxx:978 +#: vncviewer/OptionsDialog.cxx:1009 msgid "Windowed" msgstr "Упрозорен" -#: vncviewer/OptionsDialog.cxx:986 +#: vncviewer/OptionsDialog.cxx:1017 msgid "Full screen on current monitor" msgstr "Цео екран на текућем монитору" -#: vncviewer/OptionsDialog.cxx:994 +#: vncviewer/OptionsDialog.cxx:1025 msgid "Full screen on all monitors" msgstr "Цео екран на Ñвим мониторима" -#: vncviewer/OptionsDialog.cxx:1002 +#: vncviewer/OptionsDialog.cxx:1033 msgid "Full screen on selected monitor(s)" msgstr "Цео екран на изабраном монитору" -#: vncviewer/OptionsDialog.cxx:1031 +#: vncviewer/OptionsDialog.cxx:1062 msgid "Miscellaneous" msgstr "Разно" -#: vncviewer/OptionsDialog.cxx:1039 +#: vncviewer/OptionsDialog.cxx:1070 msgid "Shared (don't disconnect other viewers)" msgstr "Дељено (не прекидај везу другим прегледачима)" -#: vncviewer/OptionsDialog.cxx:1045 +#: vncviewer/OptionsDialog.cxx:1076 msgid "Ask to reconnect on connection errors" msgstr "Питај за поновно повезивање при грешкама везе" -#: vncviewer/ServerDialog.cxx:58 -msgid "VNC Viewer: Connection Details" +#: vncviewer/ServerDialog.cxx:63 +msgid "VNC viewer: Connection details" msgstr "Ð’ÐЦ прегледач: ПојединоÑти повезивања" -#: vncviewer/ServerDialog.cxx:68 +#: vncviewer/ServerDialog.cxx:73 msgid "VNC server:" msgstr "Ð’ÐЦ Ñервер:" -#: vncviewer/ServerDialog.cxx:75 +#: vncviewer/ServerDialog.cxx:80 msgid "Options..." msgstr "МогућноÑти..." -#: vncviewer/ServerDialog.cxx:79 +#: vncviewer/ServerDialog.cxx:84 msgid "Load..." msgstr "Учитавам..." -#: vncviewer/ServerDialog.cxx:83 -msgid "Save As..." +#: vncviewer/ServerDialog.cxx:88 +msgid "Save as..." msgstr "Сачувај као..." -#: vncviewer/ServerDialog.cxx:97 +#: vncviewer/ServerDialog.cxx:102 msgid "About..." msgstr "О програму..." -#: vncviewer/ServerDialog.cxx:106 +#: vncviewer/ServerDialog.cxx:111 msgid "Connect" msgstr "Повежи Ñе" -#: vncviewer/ServerDialog.cxx:143 +#: vncviewer/ServerDialog.cxx:147 #, c-format msgid "" "Unable to load the server history:\n" @@ -408,15 +480,15 @@ msgstr "" "\n" "%s" -#: vncviewer/ServerDialog.cxx:172 vncviewer/ServerDialog.cxx:212 +#: vncviewer/ServerDialog.cxx:176 vncviewer/ServerDialog.cxx:216 msgid "TigerVNC configuration (*.tigervnc)" msgstr "Подешавање ТиграВÐЦ (*.tigervnc)" -#: vncviewer/ServerDialog.cxx:173 +#: vncviewer/ServerDialog.cxx:177 msgid "Select a TigerVNC configuration file" msgstr "Изаберите датотеку подешавања ТиграВÐЦ" -#: vncviewer/ServerDialog.cxx:195 vncviewer/vncviewer.cxx:515 +#: vncviewer/ServerDialog.cxx:199 vncviewer/vncviewer.cxx:517 #, c-format msgid "" "Unable to load the specified configuration file:\n" @@ -427,24 +499,24 @@ msgstr "" "\n" "%s" -#: vncviewer/ServerDialog.cxx:213 +#: vncviewer/ServerDialog.cxx:217 msgid "Save the TigerVNC configuration to file" msgstr "Сачувајте подешавање ТиграВÐЦ у датотеку" -#: vncviewer/ServerDialog.cxx:239 +#: vncviewer/ServerDialog.cxx:243 #, c-format msgid "%s already exists. Do you want to overwrite?" msgstr "„%s“ већ поÑтоји. Желите ли да је препишете?" -#: vncviewer/ServerDialog.cxx:240 vncviewer/vncviewer.cxx:392 +#: vncviewer/ServerDialog.cxx:244 vncviewer/vncviewer.cxx:394 msgid "No" msgstr "Ðе" -#: vncviewer/ServerDialog.cxx:240 +#: vncviewer/ServerDialog.cxx:244 msgid "Overwrite" msgstr "Препиши" -#: vncviewer/ServerDialog.cxx:256 +#: vncviewer/ServerDialog.cxx:260 #, c-format msgid "" "Unable to save the specified configuration file:\n" @@ -455,7 +527,7 @@ msgstr "" "\n" "%s" -#: vncviewer/ServerDialog.cxx:290 +#: vncviewer/ServerDialog.cxx:294 #, c-format msgid "" "Unable to save the default configuration:\n" @@ -466,7 +538,7 @@ msgstr "" "\n" "%s" -#: vncviewer/ServerDialog.cxx:303 +#: vncviewer/ServerDialog.cxx:306 #, c-format msgid "" "Unable to save the server history:\n" @@ -477,205 +549,147 @@ msgstr "" "\n" "%s" -#: vncviewer/ServerDialog.cxx:320 vncviewer/ServerDialog.cxx:386 -msgid "Could not obtain the state directory path" -msgstr "Ðе могу да набавим путању директоријума Ñтања" +#: vncviewer/ServerDialog.cxx:351 vncviewer/ServerDialog.cxx:429 +#: vncviewer/vncviewer.cxx:580 +msgid "Could not determine VNC state directory path" +msgstr "Ðе могу да одредим путању фаÑцикле Ð’ÐЦ Ñтања" -#: vncviewer/ServerDialog.cxx:332 vncviewer/ServerDialog.cxx:394 -#: vncviewer/parameters.cxx:644 vncviewer/parameters.cxx:750 +#: vncviewer/ServerDialog.cxx:363 vncviewer/ServerDialog.cxx:437 +#: vncviewer/parameters.cxx:671 vncviewer/parameters.cxx:752 #, c-format -msgid "Could not open \"%s\": %s" -msgstr "Ðе могу да отворим „%s“: %s" +msgid "Could not open \"%s\"" +msgstr "Ðе могу да отворим „%s“" -#: vncviewer/ServerDialog.cxx:347 vncviewer/ServerDialog.cxx:355 -#: vncviewer/parameters.cxx:764 vncviewer/parameters.cxx:770 -#: vncviewer/parameters.cxx:801 vncviewer/parameters.cxx:830 -#: vncviewer/parameters.cxx:836 +#: vncviewer/ServerDialog.cxx:378 vncviewer/ServerDialog.cxx:387 +#: vncviewer/parameters.cxx:766 vncviewer/parameters.cxx:773 +#: vncviewer/parameters.cxx:807 vncviewer/parameters.cxx:837 +#: vncviewer/parameters.cxx:844 #, c-format -msgid "Failed to read line %d in file %s: %s" -msgstr "ÐиÑам уÑпео да прочитам %d. ред у датотеци „%s“: %s" +msgid "Failed to read line %d in file \"%s\"" +msgstr "ÐиÑам уÑпео да прочитам %d. ред у датотеци „%s“" -#: vncviewer/ServerDialog.cxx:356 vncviewer/parameters.cxx:771 +#: vncviewer/ServerDialog.cxx:390 vncviewer/parameters.cxx:776 msgid "Line too long" msgstr "Ред је предуг" -#: vncviewer/UserDialog.cxx:99 +#: vncviewer/UserDialog.cxx:123 msgid "Opening password file failed" msgstr "Отварање датотеке лозинке није уÑпело" -#: vncviewer/UserDialog.cxx:118 +#: vncviewer/UserDialog.cxx:143 msgid "VNC authentication" msgstr "Потврђивање идентитета Ð’ÐЦ-а" -#: vncviewer/UserDialog.cxx:125 +#: vncviewer/UserDialog.cxx:150 msgid "This connection is secure" msgstr "Ова веза је безбедна" -#: vncviewer/UserDialog.cxx:129 +#: vncviewer/UserDialog.cxx:154 msgid "This connection is not secure" msgstr "Ова веза није безбедна" -#: vncviewer/UserDialog.cxx:151 +#: vncviewer/UserDialog.cxx:176 msgid "Username:" msgstr "КориÑник:" -#: vncviewer/UserDialog.cxx:164 +#: vncviewer/UserDialog.cxx:189 msgid "Password:" msgstr "Лозинка:" -#: vncviewer/UserDialog.cxx:207 -msgid "Authentication cancelled" -msgstr "Потврђивање идентитета је отказано" - -#: vncviewer/Viewport.cxx:390 -#, c-format -msgid "Failed to update keyboard LED state: %lu" -msgstr "ÐиÑам уÑпео да оÑвежим Ñтање диоде таÑтатуре: %lu" - -#: vncviewer/Viewport.cxx:396 vncviewer/Viewport.cxx:402 -#, c-format -msgid "Failed to update keyboard LED state: %d" -msgstr "ÐиÑам уÑпео да оÑвежим Ñтање диоде таÑтатуре: %d" - -#: vncviewer/Viewport.cxx:432 -msgid "Failed to update keyboard LED state" -msgstr "ÐиÑам уÑпео да оÑвежим Ñтање диоде таÑтатуре" - -#: vncviewer/Viewport.cxx:459 vncviewer/Viewport.cxx:467 -#: vncviewer/Viewport.cxx:484 -#, c-format -msgid "Failed to get keyboard LED state: %d" -msgstr "ÐиÑам уÑпео да добавим Ñтање диоде таÑтатуре: %d" - -#: vncviewer/Viewport.cxx:839 -msgid "No key code specified on key press" -msgstr "Ðије наведен код таÑтера на притиÑак иÑтог" - -#: vncviewer/Viewport.cxx:990 -#, c-format -msgid "No scan code for extended virtual key 0x%02x" -msgstr "Ðема шифре прегледа за проширени виртуелни кључ 0x%02x" +#: vncviewer/UserDialog.cxx:197 +msgid "Keep password for reconnect" +msgstr "Задржи лозинку за поновно повезивање" -#: vncviewer/Viewport.cxx:992 -#, c-format -msgid "No scan code for virtual key 0x%02x" -msgstr "Ðема шифре прегледа за виртуелни кључ 0x%02x" - -#: vncviewer/Viewport.cxx:998 -#, c-format -msgid "Invalid scan code 0x%02x" -msgstr "ÐеиÑправан код Ñкенирања 0x%02x" - -#: vncviewer/Viewport.cxx:1028 -#, c-format -msgid "No symbol for extended virtual key 0x%02x" -msgstr "Ðема Ñимбола за проширени виртуелни кључ 0x%02x" - -#: vncviewer/Viewport.cxx:1030 -#, c-format -msgid "No symbol for virtual key 0x%02x" -msgstr "Ðема Ñимбола за виртуелни кључ 0x%02x" - -#: vncviewer/Viewport.cxx:1136 -#, c-format -msgid "No symbol for key code 0x%02x (in the current state)" -msgstr "Ðема Ñимбола за шифру кључа 0x%02x (у текућем Ñтању)" - -#: vncviewer/Viewport.cxx:1169 -#, c-format -msgid "No symbol for key code %d (in the current state)" -msgstr "Ðема Ñимбола за шифру кључа %d (у текућем Ñтању)" - -#: vncviewer/Viewport.cxx:1229 +#: vncviewer/Viewport.cxx:695 msgctxt "ContextMenu|" msgid "Disconn&ect" msgstr "Пре&кини везу" -#: vncviewer/Viewport.cxx:1232 +#: vncviewer/Viewport.cxx:698 msgctxt "ContextMenu|" msgid "&Full screen" msgstr "&Пун екран" -#: vncviewer/Viewport.cxx:1235 +#: vncviewer/Viewport.cxx:701 msgctxt "ContextMenu|" msgid "Minimi&ze" msgstr "&Умањи" -#: vncviewer/Viewport.cxx:1237 +#: vncviewer/Viewport.cxx:703 msgctxt "ContextMenu|" msgid "Resize &window to session" msgstr "&Величина прозора на ÑеÑију" -#: vncviewer/Viewport.cxx:1242 +#: vncviewer/Viewport.cxx:708 msgctxt "ContextMenu|" msgid "&Ctrl" msgstr "&Ктрл" -#: vncviewer/Viewport.cxx:1245 +#: vncviewer/Viewport.cxx:711 msgctxt "ContextMenu|" msgid "&Alt" msgstr "&Ðлт" -#: vncviewer/Viewport.cxx:1251 +#: vncviewer/Viewport.cxx:717 #, c-format msgctxt "ContextMenu|" msgid "Send %s" msgstr "Пошаљи „%s“" -#: vncviewer/Viewport.cxx:1257 +#: vncviewer/Viewport.cxx:724 msgctxt "ContextMenu|" msgid "Send Ctrl-Alt-&Del" msgstr "Пошаљи Ктрл-Ðлт-&Дел" -#: vncviewer/Viewport.cxx:1260 +#: vncviewer/Viewport.cxx:727 msgctxt "ContextMenu|" msgid "&Refresh screen" msgstr "&ОÑвежи екран" -#: vncviewer/Viewport.cxx:1263 +#: vncviewer/Viewport.cxx:730 msgctxt "ContextMenu|" msgid "&Options..." msgstr "&МогућноÑти..." -#: vncviewer/Viewport.cxx:1265 +#: vncviewer/Viewport.cxx:732 msgctxt "ContextMenu|" msgid "Connection &info..." msgstr "Подаци о &вези..." -#: vncviewer/Viewport.cxx:1267 +#: vncviewer/Viewport.cxx:734 msgctxt "ContextMenu|" msgid "About &TigerVNC viewer..." msgstr "О &програму..." -#: vncviewer/Viewport.cxx:1356 +#: vncviewer/Viewport.cxx:830 msgid "VNC connection info" msgstr "Подаци о Ð’ÐЦ вези" -#: vncviewer/Win32TouchHandler.cxx:47 +#: vncviewer/Win32TouchHandler.cxx:48 msgid "Window is registered for touch instead of gestures" msgstr "Прозор је региÑтрован за додир умеÑто покрета" -#: vncviewer/Win32TouchHandler.cxx:82 +#: vncviewer/Win32TouchHandler.cxx:83 #, c-format msgid "Failed to set gesture configuration (error 0x%x)" msgstr "ÐиÑам уÑпео да поÑтавим подешавање покрета (грешка 0x%x)" -#: vncviewer/Win32TouchHandler.cxx:94 +#: vncviewer/Win32TouchHandler.cxx:95 #, c-format msgid "Failed to get gesture information (error 0x%x)" msgstr "ÐиÑам уÑпео да добавим информације покрета (грешка 0x%x)" -#: vncviewer/Win32TouchHandler.cxx:359 +#: vncviewer/Win32TouchHandler.cxx:360 #, c-format msgid "Invalid mouse button %d, must be a number between 1 and 7." msgstr "ÐеиÑправно дугме миша %d, мора бити број између 1 и 7." -#: vncviewer/Win32TouchHandler.cxx:424 +#: vncviewer/Win32TouchHandler.cxx:425 #, c-format msgid "Unhandled key 0x%x - can't generate keyboard event." msgstr "Ðеобрадиви таÑтер 0x%x – не могу да Ñтворим догађај таÑтатуре." -#: vncviewer/XInputTouchHandler.cxx:102 vncviewer/touch.cxx:108 +#: vncviewer/XInputTouchHandler.cxx:102 vncviewer/touch.cxx:107 #, c-format msgid "Unable to get X Input 2 event mask for window 0x%08lx" msgstr "Ðе могу да добавим маÑку „X Input 2“ догађаја за прозор 0x%08lx" @@ -685,7 +699,7 @@ msgstr "Ðе могу да добавим маÑку „X Input 2“ догађРmsgid "Window 0x%08lx has no X Input 2 event mask" msgstr "Прозор 0x%08lx нема маÑку „X Input 2“ догађаја" -#: vncviewer/XInputTouchHandler.cxx:112 vncviewer/touch.cxx:115 +#: vncviewer/XInputTouchHandler.cxx:112 vncviewer/touch.cxx:114 #, c-format msgid "Window 0x%08lx has more than one X Input 2 event mask" msgstr "Прозор 0x%08lx има више од једне маÑке „X Input 2“ догађаја" @@ -696,7 +710,6 @@ msgid "Failure grabbing device %i" msgstr "ÐеуÑпех хватања уређаја %i" #: vncviewer/org.tigervnc.vncviewer.metainfo.xml.in:13 -#: vncviewer/vncviewer.cxx:389 vncviewer/vncviewer.desktop.in.in:3 msgid "TigerVNC Viewer" msgstr "Прегледач ТигарВÐЦ" @@ -714,145 +727,146 @@ msgid "TigerVNC is a high-speed version of VNC based on the RealVNC 4 and X.org msgstr "ТигарВÐЦ великобрзинÑко издање Ð’ÐЦ-а заÑновано на оÑновама „RealVNC“-у 4 и „X.org“ кода. ТигарВÐЦ је започео као развојно залагање Ñледеће генерације за „TightVNC“ на ÐˆÑƒÐ½Ð¸ÐºÑ Ð¸ Ð›Ð¸Ð½ÑƒÐºÑ Ð¿Ð»Ð°Ñ‚Ñ„Ð¾Ñ€Ð¼Ð°Ð¼Ð°, али Ñе издваја из Ñвог родитељÑког пројекта у раним 2009 тако да Ñе „TightVNC“ може фокуÑирати на Виндоуз платформама. ТигарВÐЦ подржава варијанту „Tight“ кодирања тако да је поприлично убрзан коришћењем „libjpeg-turbo“ ЈПЕГ кодека." #: vncviewer/org.tigervnc.vncviewer.metainfo.xml.in:33 -msgid "TigerVNC Viewer connection to a CentOS machine" +msgid "TigerVNC viewer connection to a CentOS machine" msgstr "Веза ТигарВÐЦ прегледача Ñа CentOS рачунаром" #: vncviewer/org.tigervnc.vncviewer.metainfo.xml.in:37 -msgid "TigerVNC Viewer connection to a macOS machine" +msgid "TigerVNC viewer connection to a macOS machine" msgstr "Веза ТигарВÐЦ прегледача Ñа macOS рачунаром" #: vncviewer/org.tigervnc.vncviewer.metainfo.xml.in:41 -msgid "TigerVNC Viewer connection to a Windows machine" +msgid "TigerVNC viewer connection to a Windows machine" msgstr "Веза ТигарВÐЦ прегледача Ñа Виндоуз рачунаром" -#: vncviewer/parameters.cxx:307 vncviewer/parameters.cxx:332 -#: vncviewer/parameters.cxx:349 vncviewer/parameters.cxx:389 -#: vncviewer/parameters.cxx:409 +#. developer_name tag deprecated with Appstream 1.0 +#: vncviewer/org.tigervnc.vncviewer.metainfo.xml.in:46 +#: vncviewer/org.tigervnc.vncviewer.metainfo.xml.in:48 +msgid "The TigerVNC team" +msgstr "Тим ТигарВÐЦ-а" + +#: vncviewer/parameters.cxx:319 vncviewer/parameters.cxx:344 +#: vncviewer/parameters.cxx:361 vncviewer/parameters.cxx:401 +#: vncviewer/parameters.cxx:421 msgid "The name of the parameter is too large" msgstr "Ðазив параметра је превелик" -#: vncviewer/parameters.cxx:311 vncviewer/parameters.cxx:316 -#: vncviewer/parameters.cxx:367 +#: vncviewer/parameters.cxx:323 vncviewer/parameters.cxx:328 +#: vncviewer/parameters.cxx:379 msgid "The parameter is too large" msgstr "Параметар је превелик" -#: vncviewer/parameters.cxx:374 vncviewer/parameters.cxx:694 -#: vncviewer/parameters.cxx:815 +#: vncviewer/parameters.cxx:386 vncviewer/parameters.cxx:712 +#: vncviewer/parameters.cxx:822 msgid "Invalid format or too large value" msgstr "ÐеиÑправан Ð·Ð°Ð¿Ð¸Ñ Ð¸Ð»Ð¸ предуга вредноÑÑ‚" -#: vncviewer/parameters.cxx:428 vncviewer/parameters.cxx:459 +#: vncviewer/parameters.cxx:440 vncviewer/parameters.cxx:473 msgid "Failed to create registry key" msgstr "ÐиÑам уÑпео да направим кључ региÑтра" -#: vncviewer/parameters.cxx:447 vncviewer/parameters.cxx:502 -#: vncviewer/parameters.cxx:544 vncviewer/parameters.cxx:611 +#: vncviewer/parameters.cxx:461 vncviewer/parameters.cxx:528 +#: vncviewer/parameters.cxx:571 vncviewer/parameters.cxx:638 msgid "Failed to close registry key" msgstr "ÐиÑам уÑпео да затворим кључ региÑтра" -#: vncviewer/parameters.cxx:465 vncviewer/parameters.cxx:482 -#: vncviewer/parameters.cxx:652 vncviewer/parameters.cxx:662 -#: vncviewer/parameters.cxx:673 +#: vncviewer/parameters.cxx:479 vncviewer/parameters.cxx:506 +#: vncviewer/parameters.cxx:680 vncviewer/parameters.cxx:692 #, c-format msgid "Failed to save \"%s\": %s" msgstr "ÐиÑам уÑпео да Ñачувам „%s“: %s" -#: vncviewer/parameters.cxx:478 vncviewer/parameters.cxx:566 -#: vncviewer/parameters.cxx:675 vncviewer/parameters.cxx:712 -msgid "Unknown parameter type" -msgstr "Ðепозната врÑта параметра" - -#: vncviewer/parameters.cxx:495 +#: vncviewer/parameters.cxx:489 vncviewer/parameters.cxx:520 #, c-format msgid "Failed to remove \"%s\": %s" msgstr "ÐиÑам уÑпео да уклоним „%s“: %s" -#: vncviewer/parameters.cxx:517 vncviewer/parameters.cxx:589 +#: vncviewer/parameters.cxx:544 vncviewer/parameters.cxx:616 msgid "Failed to open registry key" msgstr "ÐиÑам уÑпео да отворим кључ региÑтра" -#: vncviewer/parameters.cxx:534 +#: vncviewer/parameters.cxx:561 #, c-format msgid "Failed to read server history entry %d: %s" msgstr "ÐиÑам уÑпео да прочитам ÑƒÐ½Ð¾Ñ Ð¸Ñторијата Ñервера %d: %s" -#: vncviewer/parameters.cxx:570 vncviewer/parameters.cxx:600 +#: vncviewer/parameters.cxx:597 vncviewer/parameters.cxx:627 #, c-format msgid "Failed to read parameter \"%s\": %s" msgstr "ÐиÑам уÑпео да прочитам параметар „%s“: %s" -#: vncviewer/parameters.cxx:634 vncviewer/parameters.cxx:738 -msgid "Could not obtain the config directory path" -msgstr "Ðе могу да набавим путању директоријума подешавања" +#: vncviewer/parameters.cxx:661 vncviewer/parameters.cxx:740 +#: vncviewer/vncviewer.cxx:546 +msgid "Could not determine VNC config directory path" +msgstr "Ðе могу да одредим путању фаÑцикле Ð’ÐЦ подешавања" -#: vncviewer/parameters.cxx:653 vncviewer/parameters.cxx:664 +#: vncviewer/parameters.cxx:682 vncviewer/parameters.cxx:694 msgid "Could not encode parameter" msgstr "Ðе могу да кодирам параметар" -#: vncviewer/parameters.cxx:780 +#: vncviewer/parameters.cxx:785 #, c-format msgid "Configuration file %s is in an invalid format" msgstr "Датотека подешавања „%s“ је у неиÑправном запиÑу" -#: vncviewer/parameters.cxx:802 +#: vncviewer/parameters.cxx:809 msgid "Invalid format" msgstr "ÐеиÑправан запиÑ" -#: vncviewer/parameters.cxx:837 +#: vncviewer/parameters.cxx:846 msgid "Unknown parameter" msgstr "Ðепознат параметар" -#: vncviewer/touch.cxx:76 +#: vncviewer/touch.cxx:75 #, c-format msgid "Got message (0x%x) for an unhandled window" msgstr "Добих поруку (0x%x) за неруковани прозор" -#: vncviewer/touch.cxx:139 vncviewer/touch.cxx:161 +#: vncviewer/touch.cxx:138 vncviewer/touch.cxx:160 #, c-format msgid "Invalid window 0x%08lx specified for pointer grab" msgstr "ÐеиÑправан прозор 0x%08lx је наведен за хватање показивача" -#: vncviewer/touch.cxx:184 vncviewer/touch.cxx:185 +#: vncviewer/touch.cxx:183 vncviewer/touch.cxx:184 #, c-format msgid "Failed to create touch handler: %s" msgstr "ÐиÑам уÑпео да Ñтворим руковаоца додира: %s" -#: vncviewer/touch.cxx:189 +#: vncviewer/touch.cxx:188 #, c-format msgid "Couldn't attach event handler to window (error 0x%x)" msgstr "Ðе могу да приложим руковаоца догађајем прозору (грешка 0x%x)" -#: vncviewer/touch.cxx:216 +#: vncviewer/touch.cxx:215 msgid "Failed to get event data for X Input event" msgstr "ÐиÑам уÑпео да добавим податке догађаја за „X Input“ догађај" -#: vncviewer/touch.cxx:229 +#: vncviewer/touch.cxx:228 msgid "X Input event for unknown window" msgstr "„X Input“ догађај за непознати прозор" -#: vncviewer/touch.cxx:255 +#: vncviewer/touch.cxx:254 msgid "X Input extension not available." msgstr "„X Input“ проширење није доÑтупно." -#: vncviewer/touch.cxx:262 +#: vncviewer/touch.cxx:261 msgid "X Input 2 (or newer) is not available." msgstr "„X Input 2“ (или новије) није доÑтупно." -#: vncviewer/touch.cxx:267 +#: vncviewer/touch.cxx:266 msgid "X Input 2.2 (or newer) is not available. Touch gestures will not be supported." msgstr "„X Input 2“ (или новије) није доÑтупно. Покрети додира неће бити подржани." #: vncviewer/vncviewer.cxx:104 #, c-format msgid "" -"TigerVNC Viewer v%s\n" +"TigerVNC viewer v%s\n" "Built on: %s\n" -"Copyright (C) 1999-%d TigerVNC Team and many others (see README.rst)\n" +"Copyright (C) 1999-%d TigerVNC team and many others (see README.rst)\n" "See https://www.tigervnc.org for information on TigerVNC." msgstr "" "Прегледач ТигарВÐЦ и%s\n" "Изграђен: %s\n" -"ÐуторÑка права © 1999-%d Тим Тигра Ð’ÐЦ-а и многи други (видите „README.rst“)\n" +"ÐуторÑка права © 1999-%d Тим ТигарВÐЦ-а и многи други (видите „README.rst“)\n" "ПоÑетите „https://www.tigervnc.org“ да Ñазнате више о програму." #: vncviewer/vncviewer.cxx:158 @@ -892,95 +906,173 @@ msgstr "Грешка покретања новог примерка програ #: vncviewer/vncviewer.cxx:266 #, c-format -msgid "Termination signal %d has been received. TigerVNC Viewer will now exit." +msgid "Termination signal %d has been received. TigerVNC viewer will now exit." msgstr "Примљен је Ñигнал за окончавање %d. Програм ће Ñада изаћи." -#: vncviewer/vncviewer.cxx:393 +#: vncviewer/vncviewer.cxx:391 vncviewer/vncviewer.desktop.in.in:3 +msgid "TigerVNC viewer" +msgstr "Прегледач ТигарВÐЦ" + +#: vncviewer/vncviewer.cxx:395 msgid "Yes" msgstr "Да" -#: vncviewer/vncviewer.cxx:396 +#: vncviewer/vncviewer.cxx:398 msgid "Close" msgstr "Затвори" -#: vncviewer/vncviewer.cxx:401 +#: vncviewer/vncviewer.cxx:403 msgid "About" msgstr "О програму" -#: vncviewer/vncviewer.cxx:404 +#: vncviewer/vncviewer.cxx:406 msgid "Hide" msgstr "Сакриј" -#: vncviewer/vncviewer.cxx:407 +#: vncviewer/vncviewer.cxx:409 msgid "Quit" msgstr "Изађи" -#: vncviewer/vncviewer.cxx:411 +#: vncviewer/vncviewer.cxx:413 msgid "Services" msgstr "УÑлуге" -#: vncviewer/vncviewer.cxx:412 -msgid "Hide Others" +#: vncviewer/vncviewer.cxx:414 +msgid "Hide others" msgstr "Сакриј оÑтале" -#: vncviewer/vncviewer.cxx:413 -msgid "Show All" +#: vncviewer/vncviewer.cxx:415 +msgid "Show all" msgstr "Прикажи Ñве" -#: vncviewer/vncviewer.cxx:422 +#: vncviewer/vncviewer.cxx:424 msgctxt "SysMenu|" msgid "&File" msgstr "&Датотека" -#: vncviewer/vncviewer.cxx:425 +#: vncviewer/vncviewer.cxx:427 msgctxt "SysMenu|File|" msgid "&New Connection" msgstr "&Ðова веза" -#: vncviewer/vncviewer.cxx:525 +#: vncviewer/vncviewer.cxx:450 +#, c-format +msgid "" +"\n" +"Usage: %s [parameters] [host][:displayNum]\n" +" %s [parameters] [host][::port]\n" +" %s [parameters] [unix socket]\n" +" %s [parameters] -listen [port]\n" +" %s [parameters] [.tigervnc file]\n" +msgstr "" +"\n" +"Коришћење: %s [parametri] [domaćin][:displayNum]\n" +" %s [parametri] [domaćin][::port]\n" +" %s [parametri] [prikljuÄnica linuksa]\n" +" %s [parametri] -listen [prikquÄnik]\n" +" %s [parametri] [.tigervnc datteka]\n" + +#: vncviewer/vncviewer.cxx:465 +#, c-format +msgid "" +"\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" +msgstr "" +"\n" +"Опције:\n" +"\n" +" -display Xdisplay – Ðаводи X приказ за прозор прегледача\n" +" -geometry geometry – почетни почожај главног прозора VNC прегледача. Вифите\n" +" Ñтраницу упутÑтва за више о томе.\n" + +#: vncviewer/vncviewer.cxx:472 +#, c-format +msgid "" +"\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" +msgstr "" +"\n" +"Параметри Ñе могу укључити Ñа „-<param>“ или иÑкључити Ñа „-<param>=0“\n" +"Параметри који имају вредноÑÑ‚ Ñе могу навеÑти као „-<param> <vrednost>“\n" +"Други иÑправни облици Ñу „<param>=<vrednost> -<param>=<vrednost> --<param>=<vrednost>“\n" +"Ðазиви параметара ниÑу оÑетљиви на величину Ñлова. Параметри Ñу:\n" +"\n" + +#: vncviewer/vncviewer.cxx:527 msgid "FullScreenAllMonitors is deprecated, set FullScreenMode to 'all' instead" msgstr "„FullScreenAllMonitors“ је заÑтарело, поÑтавите „FullScreenMode“ на „all“" -#: vncviewer/vncviewer.cxx:721 +#: vncviewer/vncviewer.cxx:532 +msgid "DotWhenNoCursor is deprecated, set AlwaysCursor to 1 and CursorType to 'Dot' instead" +msgstr "„DotWhenNoCursor“ је заÑтарело, поÑтавите „AlwaysCursor“ на 1 и „CursorType“ на „Dot“ (тачка)" + +#: vncviewer/vncviewer.cxx:553 msgid "~/.vnc is deprecated, please consult 'man vncviewer' for paths to migrate to." msgstr "„~/.vnc“ је заÑтарело, погледајте „man vncviewer“ за путање за преÑељење." -#: vncviewer/vncviewer.cxx:725 +#: vncviewer/vncviewer.cxx:557 #, c-format msgid "%%APPDATA%%\\vnc is deprecated, please switch to the %%APPDATA%%\\TigerVNC location." msgstr "„%%APPDATA%%\\vnc“ је заÑтарело, пређите на „%%APPDATA%%\\TigerVNC“." -#: vncviewer/vncviewer.cxx:730 +#: vncviewer/vncviewer.cxx:562 #, c-format -msgid "Could not create VNC config directory: %s" -msgstr "Ðе могу да направим фаÑциклу подешавања Ð’ÐЦ-а: %s" +msgid "Could not create VNC config directory \"%s\": %s" +msgstr "Ðе могу да направим фаÑциклу подешавања Ð’ÐЦ-а „%s“: %s" -#: vncviewer/vncviewer.cxx:735 +#: vncviewer/vncviewer.cxx:568 +msgid "Could not determine VNC data directory path" +msgstr "Ðе могу да одредим путању фаÑцикле Ð’ÐЦ података" + +#: vncviewer/vncviewer.cxx:574 +#, c-format +msgid "Could not create VNC data directory \"%s\": %s" +msgstr "Ðе могу да направим фаÑциклу података Ð’ÐЦ-а „%s“: %s" + +#: vncviewer/vncviewer.cxx:586 #, c-format -msgid "Could not create VNC data directory: %s" -msgstr "Ðе могу да направим фаÑциклу података Ð’ÐЦ-а: %s" +msgid "Could not create VNC state directory \"%s\": %s" +msgstr "Ðе могу да направим фаÑциклу Ñтања Ð’ÐЦ-а „%s“: %s" -#: vncviewer/vncviewer.cxx:740 +#: vncviewer/vncviewer.cxx:703 #, c-format -msgid "Could not create VNC state directory: %s" -msgstr "Ðе могу да направим фаÑциклу Ñтања Ð’ÐЦ-а: %s" +msgid "%s: Unrecognized option '%s'\n" +msgstr "%s: Ðепозната опција „%s“\n" + +#: vncviewer/vncviewer.cxx:705 vncviewer/vncviewer.cxx:713 +#, c-format +msgid "See '%s --help' for more information.\n" +msgstr "Видите „%s --help“ за више информација.\n" + +#: vncviewer/vncviewer.cxx:712 +#, c-format +msgid "%s: Extra argument '%s'\n" +msgstr "%s: Додатни аргумент „%s“\n" #. TRANSLATORS: "Parameters" are command line arguments, or settings #. from a file or the Windows registry. -#: vncviewer/vncviewer.cxx:755 vncviewer/vncviewer.cxx:756 +#: vncviewer/vncviewer.cxx:748 vncviewer/vncviewer.cxx:749 msgid "Parameters -listen and -via are incompatible" msgstr "Параметри „-listen“ и „-via“ ниÑу ÑаглаÑни" -#: vncviewer/vncviewer.cxx:770 +#: vncviewer/vncviewer.cxx:763 msgid "Unable to listen for incoming connections" msgstr "Ðе могу да оÑлушкујем долазне везе" -#: vncviewer/vncviewer.cxx:772 +#: vncviewer/vncviewer.cxx:765 #, c-format msgid "Listening on port %d" msgstr "ОÑлушкујем на прикључнику %d" -#: vncviewer/vncviewer.cxx:805 +#: vncviewer/vncviewer.cxx:794 #, c-format msgid "" "Failure waiting for incoming VNC connection:\n" @@ -991,10 +1083,38 @@ msgstr "" "\n" "%s" +#: vncviewer/vncviewer.cxx:815 +#, c-format +msgid "" +"Failure setting up encrypted tunnel:\n" +"\n" +"%s" +msgstr "" +"ÐеуÑпех поÑтављања шифрованог тунела:\n" +"\n" +"%s" + #: vncviewer/vncviewer.desktop.in.in:4 -msgid "Remote Desktop Viewer" +msgid "Remote desktop viewer" msgstr "Прегледач удаљених радних површи" +#~ msgid "Show dot when no cursor" +#~ msgstr "Прикажи тачку када нема курзора" + +#, c-format +#~ msgid "Failed to update keyboard LED state: %d" +#~ msgstr "ÐиÑам уÑпео да оÑвежим Ñтање диоде таÑтатуре: %d" + +#~ msgid "No key code specified on key press" +#~ msgstr "Ðије наведен код таÑтера на притиÑак иÑтог" + +#, c-format +#~ msgid "No symbol for key code 0x%02x (in the current state)" +#~ msgstr "Ðема Ñимбола за шифру кључа 0x%02x (у текућем Ñтању)" + +#~ msgid "Unknown parameter type" +#~ msgstr "Ðепозната врÑта параметра" + #~ msgid "VNC Viewer: Connection Options" #~ msgstr "Ð’ÐЦ прегледач: МогућноÑти повезивања" diff --git a/release/CMakeLists.txt b/release/CMakeLists.txt index 6cb14de0..02491a2b 100644 --- a/release/CMakeLists.txt +++ b/release/CMakeLists.txt @@ -15,18 +15,18 @@ endif() configure_file(tigervnc.iss.in tigervnc.iss) -add_custom_target(installer - iscc -o. ${INST_DEFS} -F${CMAKE_PROJECT_NAME}${INST_SUFFIX}-${VERSION} tigervnc.iss - DEPENDS vncviewer - SOURCES ${CMAKE_CURRENT_BINARY_DIR}/tigervnc.iss) +add_custom_command(OUTPUT ${CMAKE_PROJECT_NAME}${INST_SUFFIX}-${VERSION}.exe + COMMAND iscc -o. ${INST_DEFS} -F${CMAKE_PROJECT_NAME}${INST_SUFFIX}-${VERSION} tigervnc.iss + DEPENDS vncviewer tigervnc.iss) +add_custom_target(installer DEPENDS ${CMAKE_PROJECT_NAME}${INST_SUFFIX}-${VERSION}.exe) if(BUILD_WINVNC) configure_file(winvnc.iss.in winvnc.iss) - add_custom_target(winvnc_installer - iscc -o. ${INST_DEFS} -F${CMAKE_PROJECT_NAME}${INST_SUFFIX}-winvnc-${VERSION} winvnc.iss - DEPENDS winvnc4 wm_hooks vncconfig - SOURCES ${CMAKE_CURRENT_BINARY_DIR}/winvnc.iss) + add_custom_command(OUTPUT ${CMAKE_PROJECT_NAME}${INST_SUFFIX}-winvnc-${VERSION}.exe + COMMAND iscc -o. ${INST_DEFS} -F${CMAKE_PROJECT_NAME}${INST_SUFFIX}-winvnc-${VERSION} winvnc.iss + DEPENDS winvnc4 wm_hooks vncconfig winvnc.iss) + add_custom_target(winvnc_installer DEPENDS ${CMAKE_PROJECT_NAME}${INST_SUFFIX}-winvnc-${VERSION}.exe) endif() endif() # WIN32 @@ -41,9 +41,10 @@ if(APPLE) configure_file(makemacapp.in makemacapp) configure_file(Info.plist.in Info.plist) -add_custom_target(dmg sh makemacapp - DEPENDS vncviewer - SOURCES makemacapp) +add_custom_command(OUTPUT TigerVNC-${VERSION}.dmg + COMMAND sh makemacapp + DEPENDS vncviewer makemacapp Info.plist) +add_custom_target(dmg DEPENDS TigerVNC-${VERSION}.dmg) endif() # APPLE @@ -56,16 +57,16 @@ if(UNIX) configure_file(maketarball.in maketarball) -set(TARBALL_DEPENDS vncviewer vncpasswd vncconfig) if(BUILD_JAVA) - set(TARBALL_DEPENDS ${TARBALL_DEPENDS} java) + set(TARBALL_JAVA_DEPENDENCY java) endif() -add_custom_target(tarball bash maketarball - DEPENDS ${TARBALL_DEPENDS}) +set(PACKAGE_FILE ${CMAKE_PROJECT_NAME}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}-${VERSION}.tar.gz) +add_custom_command(OUTPUT ${PACKAGE_FILE} + COMMAND bash maketarball + DEPENDS maketarball vncviewer vncpasswd vncconfig ${TARBALL_JAVA_DEPENDENCY}) -add_custom_target(servertarball bash maketarball server - DEPENDS ${TARBALL_DEPENDS}) +add_custom_target(tarball DEPENDS ${PACKAGE_FILE}) endif() #UNIX diff --git a/release/Info.plist.in b/release/Info.plist.in index 3f166bd0..9be17e5e 100644 --- a/release/Info.plist.in +++ b/release/Info.plist.in @@ -5,13 +5,13 @@ <key>CFBundleDevelopmentRegion</key> <string>English</string> <key>CFBundleDisplayName</key> - <string>TigerVNC viewer</string> + <string>TigerVNC</string> <key>CFBundleExecutable</key> - <string>TigerVNC viewer</string> + <string>vncviewer</string> <key>NSHighResolutionCapable</key> <false/> <key>CFBundleGetInfoString</key> - <string>@VERSION@, Copyright © 1998-2025 [many holders]</string> + <string>@VERSION@, Copyright (C) 1999-2025 TigerVNC team and many others (see README.rst)</string> <key>CFBundleIconFile</key> <string>tigervnc.icns</string> <key>CFBundleIdentifier</key> @@ -19,9 +19,9 @@ <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleLongVersionString</key> - <string>TigerVNC viewer @VERSION@</string> + <string>TigerVNC @VERSION@</string> <key>CFBundleName</key> - <string>TigerVNC viewer</string> + <string>TigerVNC</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> @@ -31,6 +31,6 @@ <key>LSRequiresCarbon</key> <true/> <key>NSHumanReadableCopyright</key> - <string>Copyright © 1998-2025 [many holders]</string> + <string>Copyright (C) 1999-2025 TigerVNC team and many others (see README.rst)</string> </dict> </plist> diff --git a/release/makemacapp.in b/release/makemacapp.in index 0827715c..43441b8b 100644 --- a/release/makemacapp.in +++ b/release/makemacapp.in @@ -29,25 +29,23 @@ BUILD=@BUILD@ SRCDIR=@CMAKE_SOURCE_DIR@ BINDIR=@CMAKE_BINARY_DIR@ -cd $BINDIR - if [ -f $PACKAGE_NAME.dmg ]; then rm -f $PACKAGE_NAME.dmg fi umask 022 TMPDIR=`mktemp -d /tmp/$PACKAGE_NAME-build.XXXXXX` -APPROOT="$TMPDIR/dmg/TigerVNC viewer $VERSION.app" +APPROOT="$TMPDIR/dmg/TigerVNC.app" mkdir -p "$APPROOT/Contents/MacOS" mkdir -p "$APPROOT/Contents/Resources" -install -m 755 vncviewer/vncviewer "$APPROOT/Contents/MacOS/TigerVNC viewer" +install -m 755 $BINDIR/vncviewer/vncviewer "$APPROOT/Contents/MacOS/" install -m 644 $SRCDIR/media/icons/tigervnc.icns "$APPROOT/Contents/Resources/" -install -m 644 release/Info.plist "$APPROOT/Contents/" +install -m 644 $BINDIR/release/Info.plist "$APPROOT/Contents/" for lang in `cat "$SRCDIR/po/LINGUAS"`; do mkdir -p "$APPROOT/Contents/Resources/locale/$lang/LC_MESSAGES" - install -m 644 po/$lang.mo \ + install -m 644 $BINDIR/po/$lang.mo \ "$APPROOT/Contents/Resources/locale/$lang/LC_MESSAGES/tigervnc.mo" done diff --git a/release/maketarball.in b/release/maketarball.in index 56618934..108de92c 100644 --- a/release/maketarball.in +++ b/release/maketarball.in @@ -28,13 +28,6 @@ if [[ $CFLAGS = *-m32* ]]; then CPU=i686 fi PACKAGE_FILE=$PACKAGE_NAME-$OS-$CPU-$VERSION.tar.gz -SERVER=0 - -if [ $# -gt 0 ]; then - if [ "$1" = "server" ]; then - SERVER=1 - fi -fi cd $BINDIR @@ -47,13 +40,6 @@ mkdir -p $OUTDIR/bin mkdir -p $OUTDIR/man/man1 make DESTDIR=$TMPDIR/inst install -if [ $SERVER = 1 ]; then - install -m 755 ./xorg.build/bin/Xvnc $OUTDIR/bin/ - install -m 644 ./xorg.build/man/man1/Xvnc.1 $OUTDIR/man/man1/Xvnc.1 - install -m 644 ./xorg.build/man/man1/Xserver.1 $OUTDIR/man/man1/Xserver.1 - mkdir -p $OUTDIR/lib/dri/ - install -m 755 ./xorg.build/lib/dri/swrast_dri.so $OUTDIR/lib/dri/ -fi pushd $TMPDIR/inst tar cfz ../$PACKAGE_FILE . diff --git a/release/tigervnc.iss.in b/release/tigervnc.iss.in index de4ee317..519d232f 100644 --- a/release/tigervnc.iss.in +++ b/release/tigervnc.iss.in @@ -5,7 +5,7 @@ ArchitecturesInstallIn64BitMode=x64 AppName=TigerVNC AppVerName=TigerVNC @VERSION@ (@BUILD@) AppVersion=@VERSION@ -AppPublisher=TigerVNC project +AppPublisher=TigerVNC team AppPublisherURL=https://tigervnc.org DefaultDirName={pf}\TigerVNC DefaultGroupName=TigerVNC @@ -25,8 +25,8 @@ Source: "@CMAKE_SOURCE_DIR@\LICENCE.TXT"; DestDir: "{app}"; Flags: ignoreversion #for {LINGUAS = FileOpen("@CMAKE_SOURCE_DIR@\po\LINGUAS"); !FileEof(LINGUAS); ""} AddLanguage [Icons] -Name: "{group}\TigerVNC Viewer"; FileName: "{app}\vncviewer.exe"; -Name: "{group}\Listening TigerVNC Viewer"; FileName: "{app}\vncviewer.exe"; Parameters: "-listen"; +Name: "{group}\TigerVNC"; FileName: "{app}\vncviewer.exe"; +Name: "{group}\Listening TigerVNC"; FileName: "{app}\vncviewer.exe"; Parameters: "-listen"; Name: "{group}\License"; FileName: "write.exe"; Parameters: "LICENCE.TXT"; WorkingDir: "{app}"; Flags: "useapppaths" Name: "{group}\Read Me"; FileName: "write.exe"; Parameters: "README.rst"; WorkingDir: "{app}"; Flags: "useapppaths" diff --git a/release/winvnc.iss.in b/release/winvnc.iss.in index 773aa175..2b002983 100644 --- a/release/winvnc.iss.in +++ b/release/winvnc.iss.in @@ -5,7 +5,7 @@ ArchitecturesInstallIn64BitMode=x64 AppName=TigerVNC server AppVerName=TigerVNC server v@VERSION@ (@BUILD@) AppVersion=@VERSION@ -AppPublisher=TigerVNC project +AppPublisher=TigerVNC team AppPublisherURL=https://tigervnc.org DefaultDirName={pf}\TigerVNC server DefaultGroupName=TigerVNC server diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 0709766b..4809f4d2 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -36,6 +36,10 @@ add_executable(pixelformat pixelformat.cxx) target_link_libraries(pixelformat rfb GTest::gtest_main) gtest_discover_tests(pixelformat) +add_executable(shortcuthandler shortcuthandler.cxx ../../vncviewer/ShortcutHandler.cxx) +target_link_libraries(shortcuthandler core ${GETTEXT_LIBRARIES} GTest::gtest_main) +gtest_discover_tests(shortcuthandler) + add_executable(unicode unicode.cxx) target_link_libraries(unicode core GTest::gtest_main) gtest_discover_tests(unicode) diff --git a/tests/unit/parameters.cxx b/tests/unit/parameters.cxx index fb240c91..e120f988 100644 --- a/tests/unit/parameters.cxx +++ b/tests/unit/parameters.cxx @@ -530,6 +530,14 @@ TEST(IntListParameter, strings) strings.setParam("9,\n10,\t11,\t12"); data = {9, 10, 11, 12}; EXPECT_EQ(strings, data); + + strings.setParam(""); + data = {}; + EXPECT_EQ(strings, data); + + strings.setParam(" "); + data = {}; + EXPECT_EQ(strings, data); } TEST(IntListParameter, minmax) @@ -650,6 +658,18 @@ TEST(StringListParameter, strings) strings.setParam("9,\n10,\t11,\t12"); data = {"9", "10", "11", "12"}; EXPECT_EQ(strings, data); + + strings.setParam(""); + data = {}; + EXPECT_EQ(strings, data); + + strings.setParam(" "); + data = {}; + EXPECT_EQ(strings, data); + + strings.setParam("a, , b"); + data = {"a", "", "b"}; + EXPECT_EQ(strings, data); } TEST(StringListParameter, null) @@ -733,6 +753,14 @@ TEST(EnumListParameter, strings) strings.setParam("b,\na,\tc,\tb"); data = {"b", "a", "c", "b"}; EXPECT_EQ(strings, data); + + strings.setParam(""); + data = {}; + EXPECT_EQ(strings, data); + + strings.setParam(" "); + data = {}; + EXPECT_EQ(strings, data); } TEST(EnumListParameter, validation) diff --git a/tests/unit/shortcuthandler.cxx b/tests/unit/shortcuthandler.cxx new file mode 100644 index 00000000..aa005155 --- /dev/null +++ b/tests/unit/shortcuthandler.cxx @@ -0,0 +1,607 @@ +/* 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 + +#include <gtest/gtest.h> + +#define XK_LATIN1 +#define XK_MISCELLANY +#include <rfb/keysymdef.h> + +#include "ShortcutHandler.h" + +TEST(ShortcutHandler, noModifiers) +{ + ShortcutHandler handler; + + handler.setModifiers(0); + + EXPECT_EQ(handler.handleKeyPress(1, XK_a), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_Hyper_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(5, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(5), ShortcutHandler::KeyNormal); +} + +TEST(ShortcutHandler, singleArmed) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyUnarm); +} + +TEST(ShortcutHandler, singleDualArmed) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Control_R), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyUnarm); +} + +TEST(ShortcutHandler, singleShortcut) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, singleRightShortcut) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_R), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, singleDualShortcut) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Control_R), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, singleShortcutReordered) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyShortcut); +} + +TEST(ShortcutHandler, singleDualShortcutReordered) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyPress(2, XK_Control_R), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, singleShortcutRepeated) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, singleShortcutMultipleKeys) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyPress(3, XK_b), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyPress(4, XK_c), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, singleWedgeNormal) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_b), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_a), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); +} + +TEST(ShortcutHandler, singleWedgeModifier) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_a), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); +} + +TEST(ShortcutHandler, singleWedgeModifierArmed) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_a), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); +} + +TEST(ShortcutHandler, singleWedgeModifierFiring) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyPress(3, XK_Shift_L), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, singleUnwedge) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control); + + handler.handleKeyPress(1, XK_Shift_L); + handler.handleKeyPress(2, XK_Control_L); + handler.handleKeyRelease(1); + handler.handleKeyRelease(2); + + EXPECT_EQ(handler.handleKeyPress(2, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, multiArmed) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyUnarm); +} + +TEST(ShortcutHandler, multiRearmed) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyUnarm); +} + +TEST(ShortcutHandler, multiFailedArm) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyNormal); +} + +TEST(ShortcutHandler, multiDualArmed) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Alt_R), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyUnarm); +} + +TEST(ShortcutHandler, multiShortcut) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, multiRightShortcut) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_R), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_R), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Shift_R), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, multiDualShortcut) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Control_R), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_Alt_R), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(5, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(6, XK_Shift_R), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(7, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(7), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(6), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(5), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, multiShortcutReordered) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyShortcut); +} + +TEST(ShortcutHandler, multiDualShortcutReordered) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(5, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(7, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyPress(2, XK_Control_R), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyPress(4, XK_Alt_R), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyPress(6, XK_Shift_R), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(6), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(7), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(5), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, multiShortcutRepeated) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, multiShortcutMultipleKeys) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyPress(5, XK_b), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(5), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyPress(6, XK_c), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(6), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, multiWedgeNormal) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_b), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(5, XK_a), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(5), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); +} + +TEST(ShortcutHandler, multiWedgeModifier) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Super_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(5, XK_a), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(5), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); +} + +TEST(ShortcutHandler, multiWedgeArming) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(2, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(1, XK_b), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(5, XK_a), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(5), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); +} + +TEST(ShortcutHandler, multiWedgeModifierArming) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_Super_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyNormal); +} + +TEST(ShortcutHandler, multiWedgeModifierArmed) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_Super_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyNormal); +} + +TEST(ShortcutHandler, multiWedgeModifierFiring) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + EXPECT_EQ(handler.handleKeyPress(1, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(2, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyPress(5, XK_Super_L), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(5), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(1), ShortcutHandler::KeyIgnore); +} + +TEST(ShortcutHandler, multiUnwedge) +{ + ShortcutHandler handler; + + handler.setModifiers(ShortcutHandler::Control | + ShortcutHandler::Shift | + ShortcutHandler::Alt); + + handler.handleKeyPress(1, XK_Super_L); + handler.handleKeyPress(2, XK_Control_L); + handler.handleKeyPress(3, XK_Alt_L); + handler.handleKeyPress(4, XK_Shift_L); + handler.handleKeyRelease(1); + handler.handleKeyRelease(2); + handler.handleKeyRelease(3); + handler.handleKeyRelease(4); + + EXPECT_EQ(handler.handleKeyPress(2, XK_Control_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(3, XK_Alt_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(4, XK_Shift_L), ShortcutHandler::KeyNormal); + EXPECT_EQ(handler.handleKeyPress(5, XK_a), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(5), ShortcutHandler::KeyShortcut); + EXPECT_EQ(handler.handleKeyRelease(4), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(3), ShortcutHandler::KeyIgnore); + EXPECT_EQ(handler.handleKeyRelease(2), ShortcutHandler::KeyIgnore); +} + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/unix/vncserver/selinux/vncsession.te b/unix/vncserver/selinux/vncsession.te index 4dbf687e..2ce4fc81 100644 --- a/unix/vncserver/selinux/vncsession.te +++ b/unix/vncserver/selinux/vncsession.te @@ -34,17 +34,13 @@ allow vnc_session_t self:capability { chown dac_override dac_read_search fowner allow vnc_session_t self:process { getcap setexec setrlimit setsched }; allow vnc_session_t self:fifo_file rw_fifo_file_perms; -optional_policy(` - gen_require(` - type sysctl_fs_t; - ') - allow vnc_session_t sysctl_fs_t:dir search; - allow vnc_session_t sysctl_fs_t:file { getattr open read }; -') - allow vnc_session_t vnc_session_var_run_t:file manage_file_perms; files_pid_filetrans(vnc_session_t, vnc_session_var_run_t, file) +# Allow access to /proc/sys/fs/nr_open +# Needed when the nofile limit is set to unlimited. +kernel_read_fs_sysctls(vnc_session_t) + # Allowed to create ~/.local optional_policy(` gnome_filetrans_home_content(vnc_session_t) diff --git a/unix/x0vncserver/x0vncserver.cxx b/unix/x0vncserver/x0vncserver.cxx index b42c38df..b8b631aa 100644 --- a/unix/x0vncserver/x0vncserver.cxx +++ b/unix/x0vncserver/x0vncserver.cxx @@ -36,8 +36,10 @@ #include <core/LogWriter.h> #include <core/Timer.h> +#include <rdr/FdInStream.h> #include <rdr/FdOutStream.h> +#include <rfb/UnixPasswordValidator.h> #include <rfb/VNCServerST.h> #include <network/TcpSocket.h> @@ -334,12 +336,14 @@ int main(int argc, char** argv) exit(1); } + const char *displayName = XDisplayName(displayname); if (!(dpy = XOpenDisplay(displayname))) { // FIXME: Why not vlog.error(...)? fprintf(stderr,"%s: Unable to open display \"%s\"\r\n", - programName, XDisplayName(displayname)); + programName, displayName); exit(1); } + rfb::UnixPasswordValidator::setDisplayName(displayName); signal(SIGHUP, CleanupSignalHandler); signal(SIGINT, CleanupSignalHandler); @@ -359,6 +363,8 @@ int main(int argc, char** argv) rfb::VNCServerST server(desktopName, &desktop); + FileTcpFilter fileTcpFilter(hostsFile); + if (createSystemdListeners(&listeners) > 0) { // When systemd is in charge of listeners, do not listen to anything else vlog.info("Listening on systemd sockets"); @@ -387,7 +393,6 @@ int main(int argc, char** argv) (int)rfbport); } - FileTcpFilter fileTcpFilter(hostsFile); if (strlen(hostsFile) != 0) for (network::SocketListener* listener : listeners) listener->setFilter(&fileTcpFilter); @@ -420,15 +425,10 @@ int main(int argc, char** argv) server.getSockets(&sockets); int clients_connected = 0; for (i = sockets.begin(); i != sockets.end(); i++) { - if ((*i)->isShutdown()) { - server.removeSocket(*i); - delete (*i); - } else { - FD_SET((*i)->getFd(), &rfds); - if ((*i)->outStream().hasBufferedData()) - FD_SET((*i)->getFd(), &wfds); - clients_connected++; - } + FD_SET((*i)->getFd(), &rfds); + if ((*i)->outStream().hasBufferedData()) + FD_SET((*i)->getFd(), &wfds); + clients_connected++; } if (!clients_connected) @@ -493,6 +493,29 @@ int main(int argc, char** argv) server.processSocketReadEvent(*i); if (FD_ISSET((*i)->getFd(), &wfds)) server.processSocketWriteEvent(*i); + + // Do a graceful close by waiting for the peer to close their + // end + if ((*i)->isShutdown()) { + bool done; + + done = false; + while (true) { + try { + (*i)->inStream().skip((*i)->inStream().avail()); + if (!(*i)->inStream().hasData(1)) + break; + } catch (std::exception&) { + done = true; + break; + } + } + + if (done) { + server.removeSocket(*i); + delete (*i); + } + } } if (desktop.isRunning() && sched.goodTimeToPoll()) { diff --git a/unix/xserver/hw/vnc/RFBGlue.cc b/unix/xserver/hw/vnc/RFBGlue.cc index b7616298..f217906a 100644 --- a/unix/xserver/hw/vnc/RFBGlue.cc +++ b/unix/xserver/hw/vnc/RFBGlue.cc @@ -32,6 +32,8 @@ #include <network/TcpSocket.h> +#include <rfb/UnixPasswordValidator.h> + #include "RFBGlue.h" // Loggers used by C code must be created here @@ -132,31 +134,9 @@ const char* vncGetParamDesc(const char *name) return param->getDescription(); } -int vncIsParamBool(const char *name) -{ - core::VoidParameter* param; - core::BoolParameter* bparam; - - param = core::Configuration::getParam(name); - if (param == nullptr) - return false; - - bparam = dynamic_cast<core::BoolParameter*>(param); - if (bparam == nullptr) - return false; - - return true; -} - int vncGetParamCount(void) { - int count; - - count = 0; - for (core::VoidParameter *param: *core::Configuration::global()) - count++; - - return count; + return core::Configuration::global()->size(); } char *vncGetParamList(void) @@ -256,3 +236,10 @@ int vncIsValidUTF8(const char* str, size_t bytes) return 0; } } + +void vncSetDisplayName(const char *displayNumStr) +{ + std::string displayName(":"); + displayName += displayNumStr; + rfb::UnixPasswordValidator::setDisplayName(displayName); +} diff --git a/unix/xserver/hw/vnc/RFBGlue.h b/unix/xserver/hw/vnc/RFBGlue.h index 926f49c6..86304ad5 100644 --- a/unix/xserver/hw/vnc/RFBGlue.h +++ b/unix/xserver/hw/vnc/RFBGlue.h @@ -38,7 +38,6 @@ void vncLogDebug(const char *name, const char *format, ...) int vncSetParam(const char *name, const char *value); char* vncGetParam(const char *name); const char* vncGetParamDesc(const char *name); -int vncIsParamBool(const char *name); int vncGetParamCount(void); char *vncGetParamList(void); @@ -56,6 +55,8 @@ char* vncUTF8ToLatin1(const char* src, size_t bytes); int vncIsValidUTF8(const char* str, size_t bytes); +void vncSetDisplayName(const char *displayNumStr); + #ifdef __cplusplus } #endif diff --git a/unix/xserver/hw/vnc/XserverDesktop.cc b/unix/xserver/hw/vnc/XserverDesktop.cc index d88ef874..1a7a06db 100644 --- a/unix/xserver/hw/vnc/XserverDesktop.cc +++ b/unix/xserver/hw/vnc/XserverDesktop.cc @@ -40,6 +40,7 @@ #include <core/Configuration.h> #include <core/LogWriter.h> +#include <rdr/FdInStream.h> #include <rdr/FdOutStream.h> #include <network/Socket.h> @@ -363,6 +364,31 @@ bool XserverDesktop::handleSocketEvent(int fd, if (write) sockserv->processSocketWriteEvent(*i); + // Do a graceful close by waiting for the peer to close their end + if ((*i)->isShutdown()) { + bool done; + + done = false; + while (true) { + try { + (*i)->inStream().skip((*i)->inStream().avail()); + if (!(*i)->inStream().hasData(1)) + break; + } catch (std::exception&) { + done = true; + break; + } + } + + if (done) { + vlog.debug("Client gone, sock %d",fd); + vncRemoveNotifyFd(fd); + sockserv->removeSocket(*i); + vncClientGone(fd); + delete (*i); + } + } + return true; } @@ -380,16 +406,8 @@ void XserverDesktop::blockHandler(int* timeout) server->getSockets(&sockets); for (i = sockets.begin(); i != sockets.end(); i++) { int fd = (*i)->getFd(); - if ((*i)->isShutdown()) { - vlog.debug("Client gone, sock %d",fd); - vncRemoveNotifyFd(fd); - server->removeSocket(*i); - vncClientGone(fd); - delete (*i); - } else { - /* Update existing NotifyFD to listen for write (or not) */ - vncSetNotifyFd(fd, screenIndex, true, (*i)->outStream().hasBufferedData()); - } + /* Update existing NotifyFD to listen for write (or not) */ + vncSetNotifyFd(fd, screenIndex, true, (*i)->outStream().hasBufferedData()); } // We are responsible for propagating mouse movement between clients diff --git a/unix/xserver/hw/vnc/vncModule.c b/unix/xserver/hw/vnc/vncModule.c index 5f0886a3..bff317b5 100644 --- a/unix/xserver/hw/vnc/vncModule.c +++ b/unix/xserver/hw/vnc/vncModule.c @@ -50,7 +50,7 @@ ExtensionModule vncExt = static XF86ModuleVersionInfo vncVersRec = { "vnc", - "TigerVNC project", + "TigerVNC", MODINFOSTRING1, MODINFOSTRING2, VENDOR_RELEASE, diff --git a/unix/xserver/hw/vnc/xvnc.c b/unix/xserver/hw/vnc/xvnc.c index a13168c4..5cf673aa 100644 --- a/unix/xserver/hw/vnc/xvnc.c +++ b/unix/xserver/hw/vnc/xvnc.c @@ -110,7 +110,6 @@ static VncScreenInfo vncScreenInfo = { static Bool vncPixmapDepths[33]; static Bool Render = TRUE; -static Bool displaySpecified = FALSE; static char displayNumStr[16]; static int vncVerbose = 0; @@ -187,6 +186,9 @@ AbortDDX(enum ExitCode error) void OsVendorInit(void) { + /* At this point, display has been set, so we can use it to + * initialize UnixPasswordValidator */ + vncSetDisplayName(display); } void @@ -278,7 +280,7 @@ ddxProcessArgument(int argc, char *argv[], int i) } if (argv[i][0] == ':') - displaySpecified = TRUE; + return 0; #if XORG_OLDER_THAN(1, 21, 1) #define CHECK_FOR_REQUIRED_ARGUMENTS(num) \ @@ -386,7 +388,7 @@ ddxProcessArgument(int argc, char *argv[], int i) dup2(nullfd, 2); close(nullfd); - if (!displaySpecified) { + if (!explicit_display) { int port = vncGetSocketPort(vncInetdSock); int displayNum = port - 5900; @@ -400,9 +402,9 @@ ddxProcessArgument(int argc, char *argv[], int i) FatalError ("Xvnc error: No free display number for -inetd\n"); } - - display = displayNumStr; sprintf(displayNumStr, "%d", displayNum); + display = displayNumStr; + explicit_display = TRUE; } return 1; @@ -450,26 +452,7 @@ ddxProcessArgument(int argc, char *argv[], int i) exit(0); } - /* We need to resolve an ambiguity for booleans */ - if (argv[i][0] == '-' && i + 1 < argc && vncIsParamBool(&argv[i][1])) { - 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)) { - vncSetParam(&argv[i][1], argv[i + 1]); - return 2; - } - } - - int ret; - - ret = vncHandleParamArg(argc, argv, i); - if (ret != 0) - return ret; - - return 0; + return vncHandleParamArg(argc, argv, i); } static Bool diff --git a/vncviewer/CConn.cxx b/vncviewer/CConn.cxx index ba4876f2..4a15f853 100644 --- a/vncviewer/CConn.cxx +++ b/vncviewer/CConn.cxx @@ -30,6 +30,7 @@ #include <core/LogWriter.h> #include <core/Timer.h> #include <core/string.h> +#include <core/time.h> #include <rdr/FdInStream.h> #include <rdr/FdOutStream.h> @@ -130,6 +131,8 @@ CConn::CConn(const char* vncServerName, network::Socket* socket=nullptr) CConn::~CConn() { + struct timeval now; + close(); OptionsDialog::removeCallback(handleOptions); @@ -138,6 +141,36 @@ CConn::~CConn() if (desktop) delete desktop; + sock->shutdown(); + + // Do a graceful close by waiting for the peer (up to 250 ms) + // FIXME: should do this asynchronously + gettimeofday(&now, nullptr); + while (core::msSince(&now) < 250) { + bool done; + + done = false; + while (true) { + try { + sock->inStream().skip(sock->inStream().avail()); + if (!sock->inStream().hasData(1)) + break; + } catch (std::exception&) { + done = true; + break; + } + } + + if (done) + break; + +#ifdef WIN32 + Sleep(10); +#else + usleep(10000); +#endif + } + if (sock) Fl::remove_fd(sock->getFd()); delete sock; @@ -322,7 +355,7 @@ void CConn::setExtendedDesktopSize(unsigned reason, unsigned result, void CConn::setName(const char* name) { CConnection::setName(name); - desktop->setName(); + desktop->updateCaption(); } // framebufferUpdateStart() is called at the beginning of an update. diff --git a/vncviewer/CMakeLists.txt b/vncviewer/CMakeLists.txt index d32ad2ea..8aba3cae 100644 --- a/vncviewer/CMakeLists.txt +++ b/vncviewer/CMakeLists.txt @@ -6,13 +6,13 @@ add_executable(vncviewer fltk/Fl_Monitor_Arrangement.cxx fltk/Fl_Navigation.cxx fltk/theme.cxx - menukey.cxx BaseTouchHandler.cxx CConn.cxx DesktopWindow.cxx EmulateMB.cxx UserDialog.cxx ServerDialog.cxx + ShortcutHandler.cxx Surface.cxx OptionsDialog.cxx PlatformPixelBuffer.cxx diff --git a/vncviewer/DesktopWindow.cxx b/vncviewer/DesktopWindow.cxx index 2ab6ec14..831bb107 100644 --- a/vncviewer/DesktopWindow.cxx +++ b/vncviewer/DesktopWindow.cxx @@ -26,6 +26,8 @@ #include <assert.h> #include <stdio.h> #include <string.h> +#include <time.h> +#include <unistd.h> #include <sys/time.h> #include <core/LogWriter.h> @@ -73,6 +75,9 @@ static int edge_scroll_size_y = 96; // default: roughly 60 fps for smooth motion #define EDGE_SCROLL_SECONDS_PER_FRAME 0.016666 +// Time before we show an overlay tip again +const time_t OVERLAY_REPEAT_TIMEOUT = 600; + static core::LogWriter vlog("DesktopWindow"); // Global due to http://www.fltk.org/str.php?L2177 and the similar @@ -80,11 +85,11 @@ static core::LogWriter vlog("DesktopWindow"); static std::set<DesktopWindow *> instances; DesktopWindow::DesktopWindow(int w, int h, CConn* cc_) - : Fl_Window(w, h), cc(cc_), offscreen(nullptr), overlay(nullptr), + : Fl_Window(w, h), cc(cc_), offscreen(nullptr), firstUpdate(true), delayedFullscreen(false), sentDesktopSize(false), pendingRemoteResize(false), lastResize({0, 0}), - keyboardGrabbed(false), mouseGrabbed(false), + keyboardGrabbed(false), mouseGrabbed(false), regrabOnFocus(false), statsLastUpdates(0), statsLastPixels(0), statsLastPosition(0), statsGraph(nullptr) { @@ -108,7 +113,7 @@ DesktopWindow::DesktopWindow(int w, int h, CConn* cc_) callback(handleClose, this); - setName(); + updateCaption(); OptionsDialog::addCallback(handleOptions, this); @@ -227,8 +232,16 @@ DesktopWindow::DesktopWindow(int w, int h, CConn* cc_) 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 @@ -249,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; @@ -282,7 +296,7 @@ const rfb::PixelFormat &DesktopWindow::getPreferredPF() } -void DesktopWindow::setName() +void DesktopWindow::updateCaption() { const size_t maxLen = 100; std::string windowName; @@ -292,7 +306,10 @@ void DesktopWindow::setName() // FIXME: All of this consideres bytes, not characters - labelFormat = "%s - TigerVNC"; + 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 @@ -533,9 +550,12 @@ void DesktopWindow::draw() } // Overlay (if active) - if (overlay) { + if (!overlays.empty()) { int ox, oy, ow, oh; int sx, sy, sw, sh; + struct Overlay overlay; + + overlay = overlays.front(); // Make sure it's properly seen by adjusting it relative to the // primary screen rather than the entire window @@ -573,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); } } @@ -698,34 +720,55 @@ void DesktopWindow::resize(int x, int y, int w, int h) repositionWidgets(); } - - // Some systems require a grab after the window size has been changed. - // Otherwise they might hold on to displays, resulting in them being unusable. - maybeGrabKeyboard(); } - -void DesktopWindow::menuOverlay(void* data) +void DesktopWindow::addOverlayTip(const char* text, ...) { - DesktopWindow *self; + va_list ap; + char textbuf[1024]; - self = (DesktopWindow*)data; + std::map<std::string, time_t>::iterator iter; - // Empty string means None, for backward compatibility - if ((menuKey != "") && (menuKey != "None")) { - self->setOverlay(_("Press %s to open the context menu"), - menuKey.getValueStr().c_str()); + va_start(ap, text); + vsnprintf(textbuf, sizeof(textbuf), text, ap); + textbuf[sizeof(textbuf)-1] = '\0'; + va_end(ap); + + // 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; @@ -739,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 @@ -755,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; @@ -768,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; @@ -804,39 +841,53 @@ void DesktopWindow::setOverlay(const char* text, ...) delete imageText; - overlay = new Surface(image); - overlayAlpha = 0; - gettimeofday(&overlayStart, nullptr); + overlay.surface = new Surface(image); + overlay.alpha = 0; + memset(&overlay.start, 0, sizeof(overlay.start)); + overlays.push_back(overlay); delete image; delete [] buffer; - Fl::add_timeout(1.0/60, updateOverlay, this); + if (overlays.size() == 1) + Fl::add_timeout(0.5, updateOverlay, this); } void DesktopWindow::updateOverlay(void *data) { DesktopWindow *self; + struct Overlay* overlay; unsigned elapsed; self = (DesktopWindow*)data; - elapsed = core::msSince(&self->overlayStart); + if (self->overlays.empty()) + return; + + overlay = &self->overlays.front(); + + if (overlay->start.tv_sec == 0) + gettimeofday(&overlay->start, nullptr); + + elapsed = core::msSince(&overlay->start); if (elapsed < 500) { - self->overlayAlpha = (unsigned)255 * elapsed / 500; + overlay->alpha = (unsigned)255 * elapsed / 500; Fl::add_timeout(1.0/60, updateOverlay, self); } else if (elapsed < 3500) { - self->overlayAlpha = 255; + overlay->alpha = 255; Fl::add_timeout(3.0, updateOverlay, self); } else if (elapsed < 4000) { - self->overlayAlpha = (unsigned)255 * (4000 - elapsed) / 500; + overlay->alpha = (unsigned)255 * (4000 - elapsed) / 500; Fl::add_timeout(1.0/60, updateOverlay, self); } else { - delete self->overlay; - self->overlay = nullptr; + delete overlay->surface; + self->overlays.pop_front(); + if (!self->overlays.empty()) + Fl::add_timeout(0.5, updateOverlay, self); } + // FIXME: Only damage relevant area self->damage(FL_DAMAGE_USER1); } @@ -850,10 +901,36 @@ int DesktopWindow::handle(int event) // Update scroll bars repositionWidgets(); - if (fullscreen_active()) - maybeGrabKeyboard(); - else - ungrabKeyboard(); + // Show how to get out of full screen + if (fullscreen_active()) { + unsigned modifierMask; + + modifierMask = 0; + for (core::EnumListEntry key : shortcutModifiers) + modifierMask |= ShortcutHandler::parseModifier(key.getValueStr().c_str()); + + if (modifierMask) + addOverlayTip(_("Press %sEnter to leave full-screen mode"), + ShortcutHandler::modifierPrefix(modifierMask)); + } + +#ifdef __APPLE__ + // Complain to the user if we won't have permission to grab keyboard + if (fullscreenSystemKeys && fullscreen_active()) { + // FIXME: There is some race during initial full screen where we + // fail to give focus to the popup, but we can work around + // it using a timer + Fl::add_timeout(0, [](void*) { cocoa_is_trusted(true); }, nullptr); + } +#endif + + // Automatically toggle keyboard grab? + if (fullscreenSystemKeys) { + if (fullscreen_active()) + grabKeyboard(); + else + ungrabKeyboard(); + } // The window manager respected our full screen request, so stop // waiting and delaying the session resize @@ -945,16 +1022,21 @@ int DesktopWindow::fltkDispatch(int event, Fl_Window *win) if (dw) { switch (event) { // Focus might not stay with us just because we have grabbed the - // keyboard. E.g. we might have sub windows, or we're not using - // all monitors and the user clicked on another application. - // Make sure we update our grabs with the focus changes. + // keyboard. E.g. we might have sub windows, or the user clicked on + // another application. Make sure we update our grabs with the focus + // changes. case FL_FOCUS: - dw->maybeGrabKeyboard(); + if (dw->regrabOnFocus || + (fullscreenSystemKeys && dw->fullscreen_active())) + dw->grabKeyboard(); + dw->regrabOnFocus = false; break; case FL_UNFOCUS: - if (fullscreenSystemKeys) { - dw->ungrabKeyboard(); - } + // If the grab is active when we lose focus, the user likely wants + // the grab to remain once we regain focus + if (dw->keyboardGrabbed) + dw->regrabOnFocus = true; + dw->ungrabKeyboard(); break; case FL_SHOW: @@ -995,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); } @@ -1078,20 +1152,8 @@ void DesktopWindow::fullscreen_on() } } -#ifdef __APPLE__ - // This is a workaround for a bug in FLTK, see: https://github.com/fltk/fltk/pull/277 - int savedLevel = -1; - if (shown()) - savedLevel = cocoa_get_level(this); -#endif + fullscreen_screens(top, bottom, left, right); -#ifdef __APPLE__ - // This is a workaround for a bug in FLTK, see: https://github.com/fltk/fltk/pull/277 - if (savedLevel != -1) { - if (cocoa_get_level(this) != savedLevel) - cocoa_set_level(this, savedLevel); - } -#endif if (!fullscreen_active()) fullscreen(); @@ -1111,34 +1173,38 @@ 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 @@ -1148,14 +1214,25 @@ void DesktopWindow::grabKeyboard() 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; + } + } + + if (ret) { + vlog.error(_("Failure grabbing control of the keyboard")); + addOverlayError(_("Failure grabbing control of the keyboard")); + return; } - return; } #endif @@ -1163,21 +1240,31 @@ 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()) @@ -1214,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() { @@ -1547,11 +1624,6 @@ void DesktopWindow::handleOptions(void *data) { DesktopWindow *self = (DesktopWindow*)data; - if (fullscreenSystemKeys) - self->maybeGrabKeyboard(); - else - self->ungrabKeyboard(); - // Call fullscreen_on even if active since it handles // fullScreenMode if (fullScreen) diff --git a/vncviewer/DesktopWindow.h b/vncviewer/DesktopWindow.h index 19c41fe1..ca4cf53a 100644 --- a/vncviewer/DesktopWindow.h +++ b/vncviewer/DesktopWindow.h @@ -1,5 +1,5 @@ /* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. - * Copyright 2011 Pierre Ossman <ossman@cendio.se> for Cendio AB + * Copyright 2011-2025 Pierre Ossman <ossman@cendio.se> for Cendio AB * * This is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,7 +20,9 @@ #ifndef __DESKTOPWINDOW_H__ #define __DESKTOPWINDOW_H__ +#include <list> #include <map> +#include <string> #include <sys/time.h> @@ -47,7 +49,7 @@ public: void updateWindow(); // Updated session title - void setName(); + void updateCaption(); // Resize the current framebuffer, but retain the contents void resizeFramebuffer(int new_w, int new_h); @@ -78,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); @@ -90,14 +97,9 @@ private: bool hasFocus(); - void maybeGrabKeyboard(); - void grabKeyboard(); - void ungrabKeyboard(); void grabPointer(); void ungrabPointer(); - static void handleGrab(void *data); - void maximizeWindow(); static void handleResizeTimeout(void *data); @@ -123,9 +125,15 @@ private: Fl_Scrollbar *hscroll, *vscroll; Viewport *viewport; Surface *offscreen; - Surface *overlay; - unsigned char overlayAlpha; - struct timeval overlayStart; + + struct Overlay { + Surface *surface; + unsigned char alpha; + struct timeval start; + }; + + std::list<Overlay> overlays; + std::map<std::string, time_t> overlayTimes; bool firstUpdate; bool delayedFullscreen; @@ -137,6 +145,8 @@ private: bool keyboardGrabbed; bool mouseGrabbed; + bool regrabOnFocus; + struct statsEntry { unsigned ups; unsigned pps; diff --git a/vncviewer/Keyboard.h b/vncviewer/Keyboard.h index 78c82787..aeab4e71 100644 --- a/vncviewer/Keyboard.h +++ b/vncviewer/Keyboard.h @@ -21,6 +21,8 @@ #include <stdint.h> +#include <list> + class KeyboardHandler { public: @@ -38,6 +40,7 @@ public: 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 d7626b67..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 @@ -38,6 +36,7 @@ public: 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; @@ -48,8 +47,7 @@ protected: 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 29bc74f6..599612ec 100644 --- a/vncviewer/KeyboardMacOS.mm +++ b/vncviewer/KeyboardMacOS.mm @@ -22,6 +22,8 @@ #include <assert.h> +#include <algorithm> + #import <Cocoa/Cocoa.h> #import <Carbon/Carbon.h> @@ -54,7 +56,7 @@ extern const unsigned int code_map_osx_to_qnum_len; static core::LogWriter vlog("KeyboardMacOS"); -static const int kvk_map[][2] = { +static const unsigned kvk_map[][2] = { { kVK_Return, XK_Return }, { kVK_Tab, XK_Tab }, { kVK_Space, XK_space }, @@ -174,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); @@ -196,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; @@ -339,30 +401,35 @@ uint32_t KeyboardMacOS::translateSystemKeyCode(int systemKeyCode) return code_map_osx_to_qnum[systemKeyCode]; } -NSString* KeyboardMacOS::keyTranslate(unsigned keyCode, - unsigned modifierFlags) +uint32_t KeyboardMacOS::translateToKeySym(unsigned keyCode, + unsigned modifierFlags) { const UCKeyboardLayout *layout; OSStatus err; - layout = nullptr; - TISInputSourceRef keyboard; CFDataRef uchr; + UInt32 dead_state; + UniCharCount max_len, actual_len; + UniChar string[255]; + + // Start with keys that either don't generate a symbol, or + // generate the same symbol as some other key. + for (size_t i = 0;i < sizeof(kvk_map)/sizeof(kvk_map[0]);i++) { + if (keyCode == kvk_map[i][0]) + return kvk_map[i][1]; + } + keyboard = TISCopyCurrentKeyboardLayoutInputSource(); uchr = (CFDataRef)TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); if (uchr == nullptr) - return nil; + return NoSymbol; layout = (const UCKeyboardLayout*)CFDataGetBytePtr(uchr); if (layout == nullptr) - return nil; - - UInt32 dead_state; - UniCharCount max_len, actual_len; - UniChar string[255]; + return NoSymbol; dead_state = 0; max_len = sizeof(string)/sizeof(*string); @@ -373,10 +440,12 @@ NSString* KeyboardMacOS::keyTranslate(unsigned keyCode, LMGetKbdType(), 0, &dead_state, max_len, &actual_len, string); if (err != noErr) - return nil; + return NoSymbol; // Dead key? if (dead_state != 0) { + unsigned combining; + // We have no fool proof way of asking what dead key this is. // Assume we get a spacing equivalent if we press the // same key again, and try to deduce something from that. @@ -384,34 +453,28 @@ NSString* KeyboardMacOS::keyTranslate(unsigned keyCode, LMGetKbdType(), 0, &dead_state, max_len, &actual_len, string); if (err != noErr) - return nil; - } - - return [NSString stringWithCharacters:string length:actual_len]; -} - -uint32_t KeyboardMacOS::translateEventKeysym(const NSEvent* nsevent) -{ - UInt16 key_code; - size_t i; + return NoSymbol; - NSString *chars; - UInt32 modifiers; + // FIXME: Some dead keys are given as NBSP + combining character + if (actual_len != 1) + return NoSymbol; - key_code = [nsevent keyCode]; + combining = ucs2combining(string[0]); + if (combining == (unsigned)-1) + return NoSymbol; - // Start with keys that either don't generate a symbol, or - // generate the same symbol as some other key. - for (i = 0;i < sizeof(kvk_map)/sizeof(kvk_map[0]);i++) { - if (key_code == kvk_map[i][0]) - return kvk_map[i][1]; + return ucs2keysym(combining); } + // Sanity check + if (actual_len != 1) + return NoSymbol; + // OS X always sends the same key code for the decimal key on the // num pad, but X11 wants different keysyms depending on if it should // be a comma or full stop. - if (key_code == 0x41) { - switch ([[nsevent charactersIgnoringModifiers] UTF8String][0]) { + if (keyCode == 0x41) { + switch (string[0]) { case ',': return XK_KP_Separator; case '.': @@ -421,33 +484,7 @@ uint32_t KeyboardMacOS::translateEventKeysym(const NSEvent* nsevent) } } - // We want a "normal" symbol out of the event, which basically means - // we only respect the shift and alt/altgr modifiers. Cocoa can help - // us if we only wanted shift, but as we also want alt/altgr, we'll - // have to do some lookup ourselves. This matches our behaviour on - // other platforms. - - modifiers = 0; - if ([nsevent modifierFlags] & NSAlphaShiftKeyMask) - modifiers |= alphaLock; - if ([nsevent modifierFlags] & NSShiftKeyMask) - modifiers |= shiftKey; - if ([nsevent modifierFlags] & NSAlternateKeyMask) - modifiers |= optionKey; - - chars = keyTranslate(key_code, modifiers); - if (chars == nil) - return NoSymbol; - - // FIXME: Some dead keys are given as NBSP + combining character - if ([chars length] != 1) - return NoSymbol; - - // Dead key? - if ([[nsevent characters] length] == 0) - return ucs2keysym(ucs2combining([chars characterAtIndex:0])); - - return ucs2keysym([chars characterAtIndex:0]); + return ucs2keysym(string[0]); } int KeyboardMacOS::openHID(unsigned int* ioc) diff --git a/vncviewer/KeyboardWin32.cxx b/vncviewer/KeyboardWin32.cxx index 76286217..095927f1 100644 --- a/vncviewer/KeyboardWin32.cxx +++ b/vncviewer/KeyboardWin32.cxx @@ -24,6 +24,8 @@ #include <assert.h> +#include <algorithm> + // Missing in at least some versions of MinGW #ifndef MAPVK_VK_TO_CHAR #define MAPVK_VK_TO_CHAR 2 @@ -139,6 +141,8 @@ static const UINT vkey_map[][3] = { { VK_MEDIA_STOP, NoSymbol, XF86XK_AudioStop }, { VK_MEDIA_PLAY_PAUSE, NoSymbol, XF86XK_AudioPlay }, { VK_LAUNCH_MAIL, NoSymbol, XF86XK_Mail }, + { VK_LAUNCH_MEDIA_SELECT, NoSymbol, XF86XK_AudioMedia }, + { VK_LAUNCH_APP1, NoSymbol, XF86XK_MyComputer }, { VK_LAUNCH_APP2, NoSymbol, XF86XK_Calculator }, }; @@ -203,6 +207,7 @@ bool KeyboardWin32::handleEvent(const void* event) bool isExtended; int systemKeyCode, keyCode; uint32_t keySym; + BYTE state[256]; vKey = msg->wParam; isExtended = (msg->lParam & (1 << 24)) != 0; @@ -257,7 +262,23 @@ bool KeyboardWin32::handleEvent(const void* event) keyCode = translateSystemKeyCode(systemKeyCode); - keySym = translateVKey(vKey, isExtended); + GetKeyboardState(state); + + // Pressing Ctrl wreaks havoc with the symbol lookup, so turn + // that off. But AltGr shows up as Ctrl+Alt in Windows, so keep + // Ctrl if Alt is active. + if (!(state[VK_LCONTROL] & 0x80) || !(state[VK_RMENU] & 0x80)) + state[VK_CONTROL] = state[VK_LCONTROL] = state[VK_RCONTROL] = 0; + + keySym = translateVKey(vKey, isExtended, state); + + if (keySym == NoSymbol) { + // Most Ctrl+Alt combinations will fail to produce a symbol, so + // try it again with Ctrl unconditionally disabled. + state[VK_CONTROL] = state[VK_LCONTROL] = state[VK_RCONTROL] = 0; + keySym = translateVKey(vKey, isExtended, state); + } + if (keySym == NoSymbol) { if (isExtended) vlog.error(_("No symbol for extended virtual key 0x%02x"), (int)vKey); @@ -355,6 +376,114 @@ bool KeyboardWin32::handleEvent(const void* event) return false; } +std::list<uint32_t> KeyboardWin32::translateToKeySyms(int systemKeyCode) +{ + unsigned vkey; + bool extended; + + std::list<uint32_t> keySyms; + unsigned mods; + + BYTE state[256]; + + uint32_t ks; + + UINT ch; + + extended = systemKeyCode & 0x80; + if (extended) + systemKeyCode = 0xe0 | (systemKeyCode & 0x7f); + + vkey = MapVirtualKey(systemKeyCode, MAPVK_VSC_TO_VK_EX); + if (vkey == 0) + return keySyms; + + // Start with no modifiers + memset(state, 0, sizeof(state)); + ks = translateVKey(vkey, extended, state); + if (ks != NoSymbol) + keySyms.push_back(ks); + + // Next just a single modifier at a time + for (mods = 1; mods < 16; mods <<= 1) { + std::list<uint32_t>::const_iterator iter; + + memset(state, 0, sizeof(state)); + if (mods & 0x1) + state[VK_CONTROL] = state[VK_LCONTROL] = 0x80; + if (mods & 0x2) + state[VK_SHIFT] = state[VK_LSHIFT] = 0x80; + if (mods & 0x4) + state[VK_MENU] = state[VK_LMENU] = 0x80; + if (mods & 0x8) { + state[VK_CONTROL] = state[VK_LCONTROL] = 0x80; + state[VK_MENU] = state[VK_RMENU] = 0x80; + } + + ks = translateVKey(vkey, extended, state); + if (ks == NoSymbol) + continue; + + iter = std::find(keySyms.begin(), keySyms.end(), ks); + if (iter != keySyms.end()) + continue; + + keySyms.push_back(ks); + } + + // Finally everything + for (mods = 0; mods < 16; mods++) { + std::list<uint32_t>::const_iterator iter; + + memset(state, 0, sizeof(state)); + if (mods & 0x1) + state[VK_CONTROL] = state[VK_LCONTROL] = 0x80; + if (mods & 0x2) + state[VK_SHIFT] = state[VK_LSHIFT] = 0x80; + if (mods & 0x4) + state[VK_MENU] = state[VK_LMENU] = 0x80; + if (mods & 0x8) { + state[VK_CONTROL] = state[VK_LCONTROL] = 0x80; + state[VK_MENU] = state[VK_RMENU] = 0x80; + } + + ks = translateVKey(vkey, extended, state); + if (ks == NoSymbol) + continue; + + iter = std::find(keySyms.begin(), keySyms.end(), ks); + if (iter != keySyms.end()) + continue; + + keySyms.push_back(ks); + } + + // As a final resort we use MapVirtualKey() as that gives us a Latin + // character even on non-Latin keyboards, which is useful for + // shortcuts + // + // FIXME: Can this give us anything but ASCII? + + ch = MapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR); + if (ch != 0) { + if (ch & 0x80000000) + ch = ucs2combining(ch & 0xffff); + else + ch = ch & 0xffff; + + ks = ucs2keysym(ch); + if (ks != NoSymbol) { + std::list<uint32_t>::const_iterator iter; + + iter = std::find(keySyms.begin(), keySyms.end(), ks); + if (iter == keySyms.end()) + keySyms.push_back(ks); + } + } + + return keySyms; +} + void KeyboardWin32::reset() { altGrArmed = false; @@ -466,12 +595,12 @@ uint32_t KeyboardWin32::lookupVKeyMap(unsigned vkey, bool extended, return NoSymbol; } -uint32_t KeyboardWin32::translateVKey(unsigned vkey, bool extended) +uint32_t KeyboardWin32::translateVKey(unsigned vkey, bool extended, + const unsigned char state[256]) { HKL layout; WORD lang, primary_lang; - BYTE state[256]; int ret; WCHAR wstr[10]; @@ -525,25 +654,10 @@ uint32_t KeyboardWin32::translateVKey(unsigned vkey, bool extended) // does what we want though. Unfortunately it keeps state, so // we have to be careful around dead characters. - GetKeyboardState(state); - - // Pressing Ctrl wreaks havoc with the symbol lookup, so turn - // that off. But AltGr shows up as Ctrl+Alt in Windows, so keep - // Ctrl if Alt is active. - if (!(state[VK_LCONTROL] & 0x80) || !(state[VK_RMENU] & 0x80)) - state[VK_CONTROL] = state[VK_LCONTROL] = state[VK_RCONTROL] = 0; - // FIXME: Multi character results, like U+0644 U+0627 // on Arabic layout ret = ToUnicode(vkey, 0, state, wstr, sizeof(wstr)/sizeof(wstr[0]), 0); - if (ret == 0) { - // Most Ctrl+Alt combinations will fail to produce a symbol, so - // try it again with Ctrl unconditionally disabled. - state[VK_CONTROL] = state[VK_LCONTROL] = state[VK_RCONTROL] = 0; - ret = ToUnicode(vkey, 0, state, wstr, sizeof(wstr)/sizeof(wstr[0]), 0); - } - if (ret == 1) return ucs2keysym(wstr[0]); diff --git a/vncviewer/KeyboardWin32.h b/vncviewer/KeyboardWin32.h index 336fe6da..ecab9268 100644 --- a/vncviewer/KeyboardWin32.h +++ b/vncviewer/KeyboardWin32.h @@ -28,6 +28,7 @@ public: virtual ~KeyboardWin32(); bool handleEvent(const void* event) override; + std::list<uint32_t> translateToKeySyms(int systemKeyCode) override; void reset() override; @@ -38,7 +39,8 @@ protected: uint32_t translateSystemKeyCode(int systemKeyCode); uint32_t lookupVKeyMap(unsigned vkey, bool extended, const UINT map[][3], size_t size); - uint32_t translateVKey(unsigned vkey, bool extended); + uint32_t translateVKey(unsigned vkey, bool extended, + const unsigned char state[256]); bool hasAltGr(); static void handleAltGrTimeout(void *data); diff --git a/vncviewer/KeyboardX11.cxx b/vncviewer/KeyboardX11.cxx index 8a91c2d0..587d9fc5 100644 --- a/vncviewer/KeyboardX11.cxx +++ b/vncviewer/KeyboardX11.cxx @@ -22,6 +22,7 @@ #include <assert.h> +#include <algorithm> #include <stdexcept> #include <X11/XKBlib.h> @@ -87,6 +88,32 @@ 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; @@ -95,9 +122,22 @@ bool KeyboardX11::isKeyboardReset(const void* event) if (xevent->type == FocusOut) { if (xevent->xfocus.mode == NotifyGrab) { - // Something grabbed the keyboard, but we don't know who. Might be - // us, but might be the window manager. Be cautious and assume the - // latter and report that the keyboard state was reset. + 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; } } @@ -116,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); @@ -127,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; } @@ -134,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; @@ -237,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 a4cbea04..b3b8d0a0 100644 --- a/vncviewer/KeyboardX11.h +++ b/vncviewer/KeyboardX11.h @@ -30,6 +30,7 @@ public: 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; @@ -38,6 +39,13 @@ protected: unsigned getModifierMask(uint32_t keysym); private: + void translateToKeySyms(int systemKeyCode, unsigned char group, + std::list<uint32_t>* keySyms); + void translateToKeySyms(int systemKeyCode, + unsigned char group, unsigned char mods, + std::list<uint32_t>* keySyms); + +private: int code_map_keycode_to_qnum[256]; }; diff --git a/vncviewer/OptionsDialog.cxx b/vncviewer/OptionsDialog.cxx index 9ff3285c..3ba6fba1 100644 --- a/vncviewer/OptionsDialog.cxx +++ b/vncviewer/OptionsDialog.cxx @@ -1,4 +1,4 @@ -/* Copyright 2011-2021 Pierre Ossman <ossman@cendio.se> for Cendio AB +/* Copyright 2011-2025 Pierre Ossman <ossman@cendio.se> for Cendio AB * * This is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,6 +24,8 @@ #include <stdlib.h> #include <list> +#include <core/string.h> + #include <rfb/encodings.h> #if defined(HAVE_GNUTLS) || defined(HAVE_NETTLE) @@ -35,8 +37,8 @@ #endif #include "OptionsDialog.h" +#include "ShortcutHandler.h" #include "i18n.h" -#include "menukey.h" #include "parameters.h" #include "fltk/layout.h" @@ -44,12 +46,18 @@ #include "fltk/Fl_Monitor_Arrangement.h" #include "fltk/Fl_Navigation.h" +#ifdef __APPLE__ +#include "cocoa.h" +#endif + #include <FL/Fl.H> +#include <FL/Fl_Box.H> #include <FL/Fl_Tabs.H> #include <FL/Fl_Button.H> #include <FL/Fl_Check_Button.H> #include <FL/Fl_Return_Button.H> #include <FL/Fl_Round_Button.H> +#include <FL/Fl_Toggle_Button.H> #include <FL/Fl_Int_Input.H> #include <FL/Fl_Choice.H> @@ -82,6 +90,7 @@ OptionsDialog::OptionsDialog() createCompressionPage(tx, ty, tw, th); createSecurityPage(tx, ty, tw, th); createInputPage(tx, ty, tw, th); + createShortcutsPage(tx, ty, tw, th); createDisplayPage(tx, ty, tw, th); createMiscPage(tx, ty, tw, th); } @@ -311,11 +320,19 @@ void OptionsDialog::loadOptions(void) #endif systemKeysCheckbox->value(fullscreenSystemKeys); - menuKeyChoice->value(0); + /* Keyboard shortcuts */ + unsigned modifierMask; + + modifierMask = 0; + for (core::EnumListEntry key : shortcutModifiers) + modifierMask |= ShortcutHandler::parseModifier(key.getValueStr().c_str()); - for (int idx = 0; idx < getMenuKeySymbolCount(); idx++) - if (menuKey == getMenuKeySymbols()[idx].name) - menuKeyChoice->value(idx + 1); + ctrlButton->value(modifierMask & ShortcutHandler::Control); + shiftButton->value(modifierMask & ShortcutHandler::Shift); + altButton->value(modifierMask & ShortcutHandler::Alt); + superButton->value(modifierMask & ShortcutHandler::Super); + + handleModifier(nullptr, this); /* Display */ if (!fullScreen) { @@ -452,11 +469,23 @@ void OptionsDialog::storeOptions(void) #endif fullscreenSystemKeys.setParam(systemKeysCheckbox->value()); - if (menuKeyChoice->value() == 0) - menuKey.setParam("None"); - else { - menuKey.setParam(menuKeyChoice->text()); - } + /* Keyboard shortcuts */ + std::list<std::string> modifierList; + + if (ctrlButton->value()) + modifierList.push_back( + ShortcutHandler::modifierString(ShortcutHandler::Control)); + if (shiftButton->value()) + modifierList.push_back( + ShortcutHandler::modifierString(ShortcutHandler::Shift)); + if (altButton->value()) + modifierList.push_back( + ShortcutHandler::modifierString(ShortcutHandler::Alt)); + if (superButton->value()) + modifierList.push_back( + ShortcutHandler::modifierString(ShortcutHandler::Super)); + + shortcutModifiers.setParam(modifierList); /* Display */ if (windowedButton->value()) { @@ -879,21 +908,11 @@ void OptionsDialog::createInputPage(int tx, int ty, int tw, int th) tx += INDENT; ty += TIGHT_MARGIN; - systemKeysCheckbox = new Fl_Check_Button(LBLRIGHT(tx, ty, - CHECK_MIN_WIDTH, - CHECK_HEIGHT, - _("Pass system keys directly to server (full screen)"))); + systemKeysCheckbox = new Fl_Check_Button( + LBLRIGHT(tx, ty, CHECK_MIN_WIDTH, CHECK_HEIGHT, + _("Always send all keyboard input in full screen"))); + systemKeysCheckbox->callback(handleSystemKeys, this); ty += CHECK_HEIGHT + TIGHT_MARGIN; - - menuKeyChoice = new Fl_Choice(LBLLEFT(tx, ty, 150, CHOICE_HEIGHT, _("Menu key"))); - - fltk_menu_add(menuKeyChoice, _("None"), 0, nullptr, nullptr, FL_MENU_DIVIDER); - for (int idx = 0; idx < getMenuKeySymbolCount(); idx++) - fltk_menu_add(menuKeyChoice, getMenuKeySymbols()[idx].name, 0, nullptr, nullptr, 0); - - fltk_adjust_choice(menuKeyChoice); - - ty += CHOICE_HEIGHT + TIGHT_MARGIN; } ty -= TIGHT_MARGIN; @@ -962,6 +981,76 @@ void OptionsDialog::createInputPage(int tx, int ty, int tw, int th) } +void OptionsDialog::createShortcutsPage(int tx, int ty, int tw, int th) +{ + Fl_Group *group = new Fl_Group(tx, ty, tw, th, _("Keyboard shortcuts")); + + tx += OUTER_MARGIN; + ty += OUTER_MARGIN; + + Fl_Box *intro = new Fl_Box(tx, ty, tw - OUTER_MARGIN * 2, INPUT_HEIGHT); + intro->align(FL_ALIGN_TOP_LEFT|FL_ALIGN_INSIDE); + intro->label(_("Modifier keys for keyboard shortcuts:")); + + ty += INPUT_HEIGHT + INNER_MARGIN; + + int width; + + width = (tw - OUTER_MARGIN * 2 - INNER_MARGIN * 3) / 4; + + ctrlButton = new Fl_Toggle_Button(tx, ty, + /* + * TRANSLATORS: This refers to the + * keyboard key + * */ + width, BUTTON_HEIGHT, _("Ctrl")); + ctrlButton->selection_color(FL_SELECTION_COLOR); + ctrlButton->callback(handleModifier, this); + shiftButton = new Fl_Toggle_Button(tx + width + INNER_MARGIN, ty, + /* + * TRANSLATORS: This refers to the + * keyboard key + * */ + width, BUTTON_HEIGHT, _("Shift")); + shiftButton->selection_color(FL_SELECTION_COLOR); + shiftButton->callback(handleModifier, this); + altButton = new Fl_Toggle_Button(tx + width * 2 + INNER_MARGIN * 2, ty, + /* + * TRANSLATORS: This refers to the + * keyboard key + * */ + width, BUTTON_HEIGHT, _("Alt")); + altButton->selection_color(FL_SELECTION_COLOR); + altButton->callback(handleModifier, this); + superButton = new Fl_Toggle_Button(tx + width * 3 + INNER_MARGIN * 3, ty, + /* + * TRANSLATORS: This refers to the + * keyboard key + * */ + width, BUTTON_HEIGHT, _("Win")); + superButton->selection_color(FL_SELECTION_COLOR); + superButton->callback(handleModifier, this); + +#ifdef __APPLE__ + /* TRANSLATORS: This refers to the keyboard key */ + ctrlButton->label(_("⌃ Ctrl")); + /* TRANSLATORS: This refers to the keyboard key */ + shiftButton->label(_("⇧ Shift")); + /* TRANSLATORS: This refers to the keyboard key */ + altButton->label(_("⌥ Option")); + /* TRANSLATORS: This refers to the keyboard key */ + superButton->label(_("⌘ Cmd")); +#endif + + ty += BUTTON_HEIGHT + INNER_MARGIN; + + shortcutsText = new Fl_Box(tx, ty, tw - OUTER_MARGIN * 2, th - ty - OUTER_MARGIN); + shortcutsText->align(FL_ALIGN_TOP_LEFT|FL_ALIGN_INSIDE|FL_ALIGN_WRAP); + + group->end(); +} + + void OptionsDialog::createDisplayPage(int tx, int ty, int tw, int th) { Fl_Group *group = new Fl_Group(tx, ty, tw, th, _("Display")); @@ -1130,6 +1219,20 @@ void OptionsDialog::handleRSAAES(Fl_Widget* /*widget*/, void *data) } +void OptionsDialog::handleSystemKeys(Fl_Widget* /*widget*/, void* data) +{ +#ifdef __APPLE__ + OptionsDialog* dialog = (OptionsDialog*)data; + + // Pop up the access dialog if needed + if (dialog->systemKeysCheckbox->value()) + cocoa_is_trusted(true); +#else + (void)data; +#endif +} + + void OptionsDialog::handleClipboard(Fl_Widget* /*widget*/, void *data) { (void)data; @@ -1147,6 +1250,61 @@ void OptionsDialog::handleClipboard(Fl_Widget* /*widget*/, void *data) #endif } +void OptionsDialog::handleModifier(Fl_Widget* /*widget*/, void *data) +{ + OptionsDialog *dialog = (OptionsDialog*)data; + unsigned mask; + + mask = 0; + if (dialog->ctrlButton->value()) + mask |= ShortcutHandler::Control; + if (dialog->shiftButton->value()) + mask |= ShortcutHandler::Shift; + if (dialog->altButton->value()) + mask |= ShortcutHandler::Alt; + if (dialog->superButton->value()) + mask |= ShortcutHandler::Super; + + if (mask == 0) { + dialog->shortcutsText->copy_label( + _("All keyboard shortcuts are disabled.")); + } else { + char prefix[256]; + char prefix_noplus[256]; + + std::string label; + + strcpy(prefix, ShortcutHandler::modifierPrefix(mask)); + strcpy(prefix_noplus, ShortcutHandler::modifierPrefix(mask, true)); + + label += core::format( + _("To release keyboard control from the session, press %s."), + prefix_noplus); + label += "\n\n"; + + label += core::format( + _("To pass all keyboard input to the session, press %sG."), + prefix); + label += "\n\n"; + + label += core::format( + _("To toggle full-screen mode, press %sEnter."), prefix); + label += "\n\n"; + + label += core::format( + _("To open the session context menu, press %sM."), prefix); + label += "\n\n"; + + label += core::format( + _("To send a key combination that includes %s directly to the " + "session, press %sSpace, release the space bar without " + "releasing %s, and press the desired key."), + prefix_noplus, prefix, prefix_noplus); + + dialog->shortcutsText->copy_label(label.c_str()); + } +} + void OptionsDialog::handleFullScreenMode(Fl_Widget* /*widget*/, void *data) { OptionsDialog *dialog = (OptionsDialog*)data; diff --git a/vncviewer/OptionsDialog.h b/vncviewer/OptionsDialog.h index 86a1423a..daa9f3e8 100644 --- a/vncviewer/OptionsDialog.h +++ b/vncviewer/OptionsDialog.h @@ -24,9 +24,11 @@ #include <FL/Fl_Window.H> class Fl_Widget; +class Fl_Box; class Fl_Group; class Fl_Check_Button; class Fl_Round_Button; +class Fl_Toggle_Button; class Fl_Input; class Fl_Int_Input; class Fl_Choice; @@ -54,6 +56,7 @@ protected: void createCompressionPage(int tx, int ty, int tw, int th); void createSecurityPage(int tx, int ty, int tw, int th); void createInputPage(int tx, int ty, int tw, int th); + void createShortcutsPage(int tx, int ty, int tw, int th); void createDisplayPage(int tx, int ty, int tw, int th); void createMiscPage(int tx, int ty, int tw, int th); @@ -65,8 +68,12 @@ protected: static void handleX509(Fl_Widget *widget, void *data); static void handleRSAAES(Fl_Widget *widget, void *data); + static void handleSystemKeys(Fl_Widget *widget, void *data); + static void handleClipboard(Fl_Widget *widget, void *data); + static void handleModifier(Fl_Widget *widget, void *data); + static void handleFullScreenMode(Fl_Widget *widget, void *data); static void handleCancel(Fl_Widget *widget, void *data); @@ -120,7 +127,6 @@ protected: Fl_Choice *cursorTypeChoice; Fl_Group *keyboardGroup; Fl_Check_Button *systemKeysCheckbox; - Fl_Choice *menuKeyChoice; Fl_Group *clipboardGroup; Fl_Check_Button *acceptClipboardCheckbox; #if !defined(WIN32) && !defined(__APPLE__) @@ -131,6 +137,14 @@ protected: Fl_Check_Button *sendPrimaryCheckbox; #endif + /* Keyboard shortcuts */ + Fl_Toggle_Button *ctrlButton; + Fl_Toggle_Button *altButton; + Fl_Toggle_Button *shiftButton; + Fl_Toggle_Button *superButton; + + Fl_Box *shortcutsText; + /* Display */ Fl_Group *displayModeGroup; Fl_Round_Button *windowedButton; diff --git a/vncviewer/ServerDialog.cxx b/vncviewer/ServerDialog.cxx index b7adabe7..3011e948 100644 --- a/vncviewer/ServerDialog.cxx +++ b/vncviewer/ServerDialog.cxx @@ -60,7 +60,7 @@ static core::LogWriter vlog("ServerDialog"); const char* SERVER_HISTORY="tigervnc.history"; ServerDialog::ServerDialog() - : Fl_Window(450, 0, _("VNC viewer: Connection details")) + : Fl_Window(450, 0, "TigerVNC") { int x, y, x2; Fl_Button *button; diff --git a/vncviewer/ShortcutHandler.cxx b/vncviewer/ShortcutHandler.cxx new file mode 100644 index 00000000..aa17a6d1 --- /dev/null +++ b/vncviewer/ShortcutHandler.cxx @@ -0,0 +1,275 @@ +/* Copyright 2021-2025 Pierre Ossman for Cendio AB + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#define XK_MISCELLANY +#include <rfb/keysymdef.h> + +#include "ShortcutHandler.h" +#include "i18n.h" + +ShortcutHandler::ShortcutHandler() : + modifierMask(0), state(Idle) +{ +} + +void ShortcutHandler::setModifiers(unsigned mask) +{ + modifierMask = mask; + reset(); +} + +ShortcutHandler::KeyAction ShortcutHandler::handleKeyPress(int keyCode, + uint32_t keySym) +{ + unsigned modifier, pressedMask; + std::map<int, uint32_t>::const_iterator iter; + + pressedKeys[keyCode] = keySym; + + if (modifierMask == 0) + return KeyNormal; + + modifier = keySymToModifier(keySym); + + pressedMask = 0; + for (iter = pressedKeys.begin(); iter != pressedKeys.end(); ++iter) + pressedMask |= keySymToModifier(iter->second); + + switch (state) { + case Idle: + case Arming: + case Rearming: + if (pressedMask == modifierMask) { + // All triggering modifier keys are pressed + state = Armed; + } if (modifier && ((modifier & modifierMask) == modifier)) { + // The new key is part of the triggering set + if (state == Idle) + state = Arming; + } else { + // The new key was something else + state = Wedged; + } + return KeyNormal; + case Armed: + if (modifier && ((modifier & modifierMask) == modifier)) { + // The new key is part of the triggering set + return KeyNormal; + } else if (modifier) { + // The new key is some other modifier + state = Wedged; + return KeyNormal; + } else { + // The new key was something else + state = Firing; + firedKeys.insert(keyCode); + return KeyShortcut; + } + break; + case Firing: + if (modifier) { + // The new key is a modifier (may or may not be part of the + // triggering set) + return KeyIgnore; + } else { + // The new key was something else + firedKeys.insert(keyCode); + return KeyShortcut; + } + default: + break; + } + + return KeyNormal; +} + +ShortcutHandler::KeyAction ShortcutHandler::handleKeyRelease(int keyCode) +{ + bool firedKey; + unsigned pressedMask; + std::map<int, uint32_t>::const_iterator iter; + KeyAction action; + + firedKey = firedKeys.count(keyCode) != 0; + + firedKeys.erase(keyCode); + pressedKeys.erase(keyCode); + + pressedMask = 0; + for (iter = pressedKeys.begin(); iter != pressedKeys.end(); ++iter) + pressedMask |= keySymToModifier(iter->second); + + switch (state) { + case Arming: + action = KeyNormal; + break; + case Armed: + if (pressedKeys.empty()) + action = KeyUnarm; + else if (pressedMask == modifierMask) + action = KeyNormal; + else { + action = KeyNormal; + state = Rearming; + } + break; + case Rearming: + if (pressedKeys.empty()) + action = KeyUnarm; + else + action = KeyNormal; + break; + case Firing: + if (firedKey) + action = KeyShortcut; + else + action = KeyIgnore; + break; + default: + action = KeyNormal; + } + + if (pressedKeys.empty()) + state = Idle; + + return action; +} + +void ShortcutHandler::reset() +{ + state = Idle; + firedKeys.clear(); + pressedKeys.clear(); +} + +// Keep list of valid values in sync with shortcutModifiers +unsigned ShortcutHandler::parseModifier(const char* key) +{ + if (strcasecmp(key, "Ctrl") == 0) + return Control; + else if (strcasecmp(key, "Shift") == 0) + return Shift; + else if (strcasecmp(key, "Alt") == 0) + return Alt; + else if (strcasecmp(key, "Win") == 0) + return Super; + else if (strcasecmp(key, "Super") == 0) + return Super; + else if (strcasecmp(key, "Option") == 0) + return Alt; + else if (strcasecmp(key, "Cmd") == 0) + return Super; + else + return 0; +} + +const char* ShortcutHandler::modifierString(unsigned key) +{ + if (key == Control) + return "Ctrl"; + if (key == Shift) + return "Shift"; + if (key == Alt) + return "Alt"; + if (key == Super) + return "Super"; + + return ""; +} + +const char* ShortcutHandler::modifierPrefix(unsigned mask, + bool justPrefix) +{ + static char prefix[256]; + + prefix[0] = '\0'; + if (mask & Control) { +#ifdef __APPLE__ + strcat(prefix, "⌃"); +#else + strcat(prefix, _("Ctrl")); + strcat(prefix, "+"); +#endif + } + if (mask & Shift) { +#ifdef __APPLE__ + strcat(prefix, "⇧"); +#else + strcat(prefix, _("Shift")); + strcat(prefix, "+"); +#endif + } + if (mask & Alt) { +#ifdef __APPLE__ + strcat(prefix, "⌥"); +#else + strcat(prefix, _("Alt")); + strcat(prefix, "+"); +#endif + } + if (mask & Super) { +#ifdef __APPLE__ + strcat(prefix, "⌘"); +#else + strcat(prefix, _("Win")); + strcat(prefix, "+"); +#endif + } + + if (prefix[0] == '\0') + return ""; + + if (justPrefix) { +#ifndef __APPLE__ + prefix[strlen(prefix)-1] = '\0'; +#endif + return prefix; + } + +#ifdef __APPLE__ + strcat(prefix, "\xc2\xa0"); // U+00A0 NO-BREAK SPACE +#endif + + return prefix; +} + +unsigned ShortcutHandler::keySymToModifier(uint32_t keySym) +{ + switch (keySym) { + case XK_Control_L: + case XK_Control_R: + return Control; + case XK_Shift_L: + case XK_Shift_R: + return Shift; + case XK_Alt_L: + case XK_Alt_R: + return Alt; + case XK_Super_L: + case XK_Super_R: + case XK_Hyper_L: + case XK_Hyper_R: + return Super; + } + + return 0; +} diff --git a/vncviewer/ShortcutHandler.h b/vncviewer/ShortcutHandler.h new file mode 100644 index 00000000..bb6497a9 --- /dev/null +++ b/vncviewer/ShortcutHandler.h @@ -0,0 +1,79 @@ +/* Copyright 2021-2025 Pierre Ossman for Cendio AB + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifndef __SHORTCUTHANDLER__ +#define __SHORTCUTHANDLER__ + +#include <set> +#include <map> + +#include <stdint.h> + +class ShortcutHandler { +public: + ShortcutHandler(); + + void setModifiers(unsigned mask); + + enum KeyAction { + KeyNormal, + KeyUnarm, + KeyShortcut, + KeyIgnore, + }; + + KeyAction handleKeyPress(int keyCode, uint32_t keySym); + KeyAction handleKeyRelease(int keyCode); + + void reset(); + +public: + enum Modifier { + Control = (1<<0), + Shift = (1<<1), + Alt = (1<<2), + Super = (1<<3), + }; + + static unsigned parseModifier(const char* key); + static const char* modifierString(unsigned key); + + static const char* modifierPrefix(unsigned mask, + bool justPrefix=false); + +private: + unsigned keySymToModifier(uint32_t keySym); + +private: + unsigned modifierMask; + + enum State { + Idle, + Arming, + Armed, + Rearming, + Firing, + Wedged, + }; + State state; + + std::set<int> firedKeys; + std::map<int, uint32_t> pressedKeys; +}; + +#endif diff --git a/vncviewer/Viewport.cxx b/vncviewer/Viewport.cxx index 2cffc7be..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 @@ -32,14 +32,20 @@ #include <rfb/CMsgWriter.h> #include <rfb/Cursor.h> +#include <rfb/KeysymStr.h> #include <rfb/ledStates.h> // FLTK can pull in the X11 headers on some systems #ifndef XK_VoidSymbol +#define XK_LATIN1 #define XK_MISCELLANY #include <rfb/keysymdef.h> #endif +#ifndef NoSymbol +#define NoSymbol 0 +#endif + #include "fltk/layout.h" #include "fltk/util.h" #include "Viewport.h" @@ -48,7 +54,6 @@ #include "DesktopWindow.h" #include "i18n.h" #include "parameters.h" -#include "menukey.h" #include "vncviewer.h" #include "PlatformPixelBuffer.h" @@ -77,7 +82,7 @@ static core::LogWriter vlog("Viewport"); // Menu constants enum { ID_DISCONNECT, ID_FULLSCREEN, ID_MINIMIZE, ID_RESIZE, - ID_CTRL, ID_ALT, ID_MENUKEY, ID_CTRLALTDEL, + ID_CTRL, ID_ALT, ID_CTRLALTDEL, ID_REFRESH, ID_OPTIONS, ID_INFO, ID_ABOUT }; // Used for fake key presses from the menu @@ -91,7 +96,7 @@ static const int FAKE_KEY_CODE = 0xffff; Viewport::Viewport(int w, int h, CConn* cc_) : Fl_Widget(0, 0, w, h), cc(cc_), frameBuffer(nullptr), lastPointerPos(0, 0), lastButtonMask(0), - keyboard(nullptr), + keyboard(nullptr), shortcutBypass(false), shortcutActive(false), firstLEDState(true), pendingClientClipboard(false), menuCtrlKey(false), menuAltKey(false), cursor(nullptr), cursorIsBlank(false) @@ -130,7 +135,11 @@ Viewport::Viewport(int w, int h, 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); @@ -673,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; @@ -704,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; @@ -766,16 +922,6 @@ void Viewport::initContextMenu() 0, nullptr, (void*)ID_ALT, FL_MENU_TOGGLE | (menuAltKey?FL_MENU_VALUE:0)); - if (menuKeySym) { - char sendMenuKey[64]; - snprintf(sendMenuKey, 64, p_("ContextMenu|", "Send %s"), - menuKey.getValueStr().c_str()); - fltk_menu_add(contextMenu, sendMenuKey, 0, nullptr, (void*)ID_MENUKEY, 0); - fltk_menu_add(contextMenu, "Secret shortcut menu key", - menuKeyFLTK, nullptr, - (void*)ID_MENUKEY, FL_MENU_INVISIBLE); - } - fltk_menu_add(contextMenu, p_("ContextMenu|", "Send Ctrl-Alt-&Del"), 0, nullptr, (void*)ID_CTRLALTDEL, FL_MENU_DIVIDER); @@ -786,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 @@ -809,11 +955,11 @@ void Viewport::popupContextMenu() window()->cursor(FL_CURSOR_DEFAULT); // FLTK also doesn't switch focus properly for menus - handle(FL_UNFOCUS); + Fl::handle(FL_UNFOCUS, window()); m = contextMenu->popup(); - handle(FL_FOCUS); + Fl::handle(FL_FOCUS, window()); // Back to our proper mouse pointer. if (Fl::belowmouse() == this) @@ -860,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); @@ -892,18 +1034,17 @@ void Viewport::popupContextMenu() } } - -void Viewport::setMenuKey() -{ - getMenuKey(&menuKeyFLTK, &menuKeyCode, &menuKeySym); -} - - void Viewport::handleOptions(void *data) { Viewport *self = (Viewport*)data; + unsigned modifierMask; + + modifierMask = 0; + for (core::EnumListEntry key : shortcutModifiers) + modifierMask |= ShortcutHandler::parseModifier(key.getValueStr().c_str()); + + self->shortcutHandler.setModifiers(modifierMask); - self->setMenuKey(); if (Fl::belowmouse() == self) self->showCursor(); } diff --git a/vncviewer/Viewport.h b/vncviewer/Viewport.h index af74d390..8e3f473e 100644 --- a/vncviewer/Viewport.h +++ b/vncviewer/Viewport.h @@ -26,6 +26,7 @@ #include "EmulateMB.h" #include "Keyboard.h" +#include "ShortcutHandler.h" class Fl_Menu_Button; class Fl_RGB_Image; @@ -99,8 +100,6 @@ private: void initContextMenu(); void popupContextMenu(); - void setMenuKey(); - static void handleOptions(void *data); private: @@ -112,6 +111,10 @@ private: uint16_t lastButtonMask; Keyboard* keyboard; + ShortcutHandler shortcutHandler; + bool shortcutBypass; + bool shortcutActive; + std::set<int> pressedKeys; bool firstLEDState; @@ -119,8 +122,6 @@ private: int clipboardSource; - uint32_t menuKeySym; - int menuKeyCode, menuKeyFLTK; Fl_Menu_Button *contextMenu; bool menuCtrlKey; diff --git a/vncviewer/cocoa.h b/vncviewer/cocoa.h index b3a8326c..09db9a45 100644 --- a/vncviewer/cocoa.h +++ b/vncviewer/cocoa.h @@ -23,11 +23,10 @@ class Fl_Window; void cocoa_prevent_native_fullscreen(Fl_Window *win); -int cocoa_get_level(Fl_Window *win); -void cocoa_set_level(Fl_Window *win, int level); +bool cocoa_is_trusted(bool prompt=false); -int cocoa_capture_displays(Fl_Window *win); -void cocoa_release_displays(Fl_Window *win); +bool cocoa_tap_keyboard(); +void cocoa_untap_keyboard(); typedef struct CGColorSpace *CGColorSpaceRef; diff --git a/vncviewer/cocoa.mm b/vncviewer/cocoa.mm index 0675c429..4d9908dd 100644 --- a/vncviewer/cocoa.mm +++ b/vncviewer/cocoa.mm @@ -20,15 +20,19 @@ #include <config.h> #endif -#include <FL/Fl.H> +#include <assert.h> +#include <dlfcn.h> + #include <FL/Fl_Window.H> #include <FL/x.H> #import <Cocoa/Cocoa.h> +#import <ApplicationServices/ApplicationServices.h> -#include <core/Rect.h> +#include "cocoa.h" -static bool captured = false; +static CFMachPortRef event_tap; +static CFRunLoopSourceRef tap_source; void cocoa_prevent_native_fullscreen(Fl_Window *win) { @@ -40,102 +44,183 @@ void cocoa_prevent_native_fullscreen(Fl_Window *win) #endif } -int cocoa_get_level(Fl_Window *win) +bool cocoa_is_trusted(bool prompt) { - NSWindow *nsw; - nsw = (NSWindow*)fl_xid(win); - assert(nsw); - return [nsw level]; -} + CFStringRef keys[1]; + CFBooleanRef values[1]; + CFDictionaryRef options; -void cocoa_set_level(Fl_Window *win, int level) -{ - NSWindow *nsw; - nsw = (NSWindow*)fl_xid(win); - assert(nsw); - [nsw setLevel:level]; -} + Boolean trusted; -int cocoa_capture_displays(Fl_Window *win) -{ - NSWindow *nsw; - - nsw = (NSWindow*)fl_xid(win); - assert(nsw); +#if !defined(MAC_OS_X_VERSION_10_9) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9 + // FIXME: Raise system requirements so this isn't needed + void *lib; + typedef Boolean (*AXIsProcessTrustedWithOptionsRef)(CFDictionaryRef); + AXIsProcessTrustedWithOptionsRef AXIsProcessTrustedWithOptions; + CFStringRef kAXTrustedCheckOptionPrompt; - CGDisplayCount count; - CGDirectDisplayID displays[16]; + lib = dlopen(nullptr, 0); + if (lib == nullptr) + return false; - int sx, sy, sw, sh; - core::Rect windows_rect, screen_rect; + AXIsProcessTrustedWithOptions = + (AXIsProcessTrustedWithOptionsRef)dlsym(lib, "AXIsProcessTrustedWithOptions"); - windows_rect.setXYWH(win->x(), win->y(), win->w(), win->h()); + dlclose(lib); - if (CGGetActiveDisplayList(16, displays, &count) != kCGErrorSuccess) - return 1; + if (AXIsProcessTrustedWithOptions == nullptr) + return false; - if (count != (unsigned)Fl::screen_count()) - return 1; + kAXTrustedCheckOptionPrompt = CFSTR("AXTrustedCheckOptionPrompt"); +#endif - for (int i = 0; i < Fl::screen_count(); i++) { - Fl::screen_xywh(sx, sy, sw, sh, i); + keys[0] = kAXTrustedCheckOptionPrompt; + values[0] = prompt ? kCFBooleanTrue : kCFBooleanFalse; + options = CFDictionaryCreate(kCFAllocatorDefault, + (const void**)keys, + (const void**)values, 1, + &kCFCopyStringDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + if (options == nullptr) + return false; + + trusted = AXIsProcessTrustedWithOptions(options); + CFRelease(options); + + // For some reason, the authentication popups isn't set as active and + // is hidden behind our window(s). Try to find it and manually switch + // to it. + if (!trusted && prompt) { + long long pid; + + pid = 0; + for (int attempt = 0; attempt < 5; attempt++) { + CFArrayRef windowList; + + windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, + kCGNullWindowID); + for (int i = 0; i < CFArrayGetCount(windowList); i++) { + CFDictionaryRef window; + CFStringRef owner; + CFNumberRef cfpid; + + window = (CFDictionaryRef)CFArrayGetValueAtIndex(windowList, i); + assert(window != nullptr); + owner = (CFStringRef)CFDictionaryGetValue(window, + kCGWindowOwnerName); + if (owner == nullptr) + continue; + + // FIXME: Unknown how stable this identifier is + CFStringRef authOwner = CFSTR("universalAccessAuthWarn"); + if (CFStringCompare(owner, authOwner, 0) != kCFCompareEqualTo) + continue; + + cfpid = (CFNumberRef)CFDictionaryGetValue(window, + kCGWindowOwnerPID); + if (cfpid == nullptr) + continue; + + CFNumberGetValue(cfpid, kCFNumberLongLongType, &pid); + break; + } + + CFRelease(windowList); + + if (pid != 0) + break; + + usleep(100000); + } - screen_rect.setXYWH(sx, sy, sw, sh); - if (screen_rect.enclosed_by(windows_rect)) { - if (CGDisplayCapture(displays[i]) != kCGErrorSuccess) - return 1; + if (pid != 0) { + NSRunningApplication* authApp; - } else { - // A display might have been captured with the previous - // monitor selection. In that case we don't want to keep - // it when its no longer inside the window_rect. - CGDisplayRelease(displays[i]); + authApp = [NSRunningApplication runningApplicationWithProcessIdentifier:pid]; + if (authApp != nil) { + // Seems to work fine even without yieldActivationToApplication, + // or NSApplicationActivateIgnoringOtherApps + [authApp activateWithOptions:0]; + } } } - captured = true; + return trusted; +} - if ([nsw level] == CGShieldingWindowLevel()) - return 0; +static CGEventRef cocoa_event_tap(CGEventTapProxy /*proxy*/, + CGEventType type, CGEventRef event, + void* /*refcon*/) +{ + ProcessSerialNumber psn; + OSErr err; + + // We should just be getting these events, but just in case + if ((type != kCGEventKeyDown) && + (type != kCGEventKeyUp) && + (type != kCGEventFlagsChanged)) + return event; + + // Redirect the event to us, no matter the original target + // (note that this will loop if kCGAnnotatedSessionEventTap is used) + err = GetCurrentProcess(&psn); + if (err != noErr) + return event; + + // FIXME: CGEventPostToPid() in macOS 10.11+ + CGEventPostToPSN(&psn, event); + + // Stop delivery to original target + return nullptr; +} - [nsw setLevel:CGShieldingWindowLevel()]; +bool cocoa_tap_keyboard() +{ + CGEventMask mask; - // We're not getting put in front of the shielding window in many - // cases on macOS 13, despite setLevel: being documented as also - // pushing the window to the front. So let's explicitly move it. - [nsw orderFront:nsw]; + if (event_tap != nullptr) + return true; - return 0; -} + if (!cocoa_is_trusted()) + return false; -void cocoa_release_displays(Fl_Window *win) -{ - NSWindow *nsw; - int newlevel; + mask = CGEventMaskBit(kCGEventKeyDown) | + CGEventMaskBit(kCGEventKeyUp) | + CGEventMaskBit(kCGEventFlagsChanged); - if (captured) - CGReleaseAllDisplays(); + // Cannot be kCGAnnotatedSessionEventTap as window manager intercepts + // before that (e.g. Ctrl+Up) + event_tap = CGEventTapCreate(kCGSessionEventTap, + kCGHeadInsertEventTap, + kCGEventTapOptionDefault, + mask, cocoa_event_tap, nullptr); + if (event_tap == nullptr) + return false; - captured = false; + tap_source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, + event_tap, 0); + CFRunLoopAddSource(CFRunLoopGetCurrent(), tap_source, + kCFRunLoopCommonModes); - nsw = (NSWindow*)fl_xid(win); - assert(nsw); + return true; +} - // Someone else has already changed the level of this window - if ([nsw level] != CGShieldingWindowLevel()) +void cocoa_untap_keyboard() +{ + if (event_tap == nullptr) return; - // FIXME: Store the previous level somewhere so we don't have to hard - // code a level here. - if (win->fullscreen_active() && win->contains(Fl::focus())) - newlevel = NSStatusWindowLevel; - else - newlevel = NSNormalWindowLevel; - - // Only change if different as the level change also moves the window - // to the top of that level. - if ([nsw level] != newlevel) - [nsw setLevel:newlevel]; + // Need to explicitly disable the tap first, or we get a short delay + // where all events are dropped + CGEventTapEnable(event_tap, false); + + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), tap_source, + kCFRunLoopCommonModes); + CFRelease(tap_source); + tap_source = nullptr; + + CFRelease(event_tap); + event_tap = nullptr; } CGColorSpaceRef cocoa_win_color_space(Fl_Window *win) diff --git a/vncviewer/fltk/Fl_Navigation.cxx b/vncviewer/fltk/Fl_Navigation.cxx index d3117aae..be258ce5 100644 --- a/vncviewer/fltk/Fl_Navigation.cxx +++ b/vncviewer/fltk/Fl_Navigation.cxx @@ -31,6 +31,7 @@ #include <FL/Fl_Button.H> #include <FL/Fl_Scroll.H> +#include <FL/fl_draw.H> #include "Fl_Navigation.h" @@ -154,14 +155,21 @@ void Fl_Navigation::update_labels() for (i = 0;i < pages->children();i++) { Fl_Widget *page; Fl_Button *btn; + int w, h; page = pages->child(i); + w = labels->w() - page->labelsize() * 2; + fl_font(page->labelfont(), page->labelsize()); + fl_measure(page->label(), w, h); + h += page->labelsize() * 2; + btn = new Fl_Button(labels->x(), labels->y() + offset, - labels->w(), page->labelsize() * 3, + labels->w(), h, page->label()); btn->box(FL_FLAT_BOX); btn->type(FL_RADIO_BUTTON); + btn->align(btn->align() | FL_ALIGN_WRAP); btn->color(FL_BACKGROUND2_COLOR); btn->selection_color(FL_SELECTION_COLOR); btn->labelsize(page->labelsize()); @@ -171,7 +179,7 @@ void Fl_Navigation::update_labels() btn->callback(label_pressed, this); labels->add(btn); - offset += page->labelsize() * 3; + offset += h; } labels->size(labels->w(), offset); diff --git a/vncviewer/menukey.cxx b/vncviewer/menukey.cxx deleted file mode 100644 index 59e1daa1..00000000 --- a/vncviewer/menukey.cxx +++ /dev/null @@ -1,83 +0,0 @@ -/* Copyright 2011 Martin Koegler <mkoegler@auto.tuwien.ac.at> - * Copyright 2011 Pierre Ossman <ossman@cendio.se> for Cendio AB - * - * This is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this software; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, - * USA. - */ - -#ifdef HAVE_CONFIG_H -#include <config.h> -#endif - -#include <string.h> -#include <FL/Fl.H> - -// FLTK can pull in the X11 headers on some systems -#ifndef XK_VoidSymbol -#define XK_MISCELLANY -#include <rfb/keysymdef.h> -#endif - -#include "menukey.h" -#include "parameters.h" - -static const MenuKeySymbol menuSymbols[] = { - {"F1", FL_F + 1, 0x3b, XK_F1}, - {"F2", FL_F + 2, 0x3c, XK_F2}, - {"F3", FL_F + 3, 0x3d, XK_F3}, - {"F4", FL_F + 4, 0x3e, XK_F4}, - {"F5", FL_F + 5, 0x3f, XK_F5}, - {"F6", FL_F + 6, 0x40, XK_F6}, - {"F7", FL_F + 7, 0x41, XK_F7}, - {"F8", FL_F + 8, 0x42, XK_F8}, - {"F9", FL_F + 9, 0x43, XK_F9}, - {"F10", FL_F + 10, 0x44, XK_F10}, - {"F11", FL_F + 11, 0x57, XK_F11}, - {"F12", FL_F + 12, 0x58, XK_F12}, - {"Pause", FL_Pause, 0xc6, XK_Pause}, - {"Scroll_Lock", FL_Scroll_Lock, 0x46, XK_Scroll_Lock}, - {"Escape", FL_Escape, 0x01, XK_Escape}, - {"Insert", FL_Insert, 0xd2, XK_Insert}, - {"Delete", FL_Delete, 0xd3, XK_Delete}, - {"Home", FL_Home, 0xc7, XK_Home}, - {"Page_Up", FL_Page_Up, 0xc9, XK_Page_Up}, - {"Page_Down", FL_Page_Down, 0xd1, XK_Page_Down}, -}; - -int getMenuKeySymbolCount() -{ - return sizeof(menuSymbols)/sizeof(menuSymbols[0]); -} - -const MenuKeySymbol* getMenuKeySymbols() -{ - return menuSymbols; -} - -void getMenuKey(int *fltkcode, int *keycode, uint32_t *keysym) -{ - for(int i = 0; i < getMenuKeySymbolCount(); i++) { - if (menuKey == menuSymbols[i].name) { - *fltkcode = menuSymbols[i].fltkcode; - *keycode = menuSymbols[i].keycode; - *keysym = menuSymbols[i].keysym; - return; - } - } - - *fltkcode = 0; - *keycode = 0; - *keysym = 0; -} diff --git a/vncviewer/menukey.h b/vncviewer/menukey.h deleted file mode 100644 index 50106955..00000000 --- a/vncviewer/menukey.h +++ /dev/null @@ -1,34 +0,0 @@ -/* Copyright 2011 Martin Koegler <mkoegler@auto.tuwien.ac.at> - * - * This is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this software; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, - * USA. - */ -#ifndef __KEYSYM_H__ -#define __KEYSYM_H__ - -#include <stdint.h> - -typedef struct { - const char* name; - int fltkcode; - int keycode; - uint32_t keysym; -} MenuKeySymbol; - -void getMenuKey(int *fltkcode, int *keycode, uint32_t *keysym); -int getMenuKeySymbolCount(); -const MenuKeySymbol* getMenuKeySymbols(); - -#endif diff --git a/vncviewer/org.tigervnc.vncviewer.metainfo.xml.in b/vncviewer/org.tigervnc.vncviewer.metainfo.xml.in index 363c12fa..207f9707 100644 --- a/vncviewer/org.tigervnc.vncviewer.metainfo.xml.in +++ b/vncviewer/org.tigervnc.vncviewer.metainfo.xml.in @@ -10,7 +10,7 @@ <id>org.tigervnc.vncviewer</id> <metadata_license>FSFAP</metadata_license> <project_license>GPL-2.0-or-later</project_license> - <name>TigerVNC Viewer</name> + <name>TigerVNC</name> <summary>Connect to VNC server and display remote desktop</summary> <content_rating type="oars-1.1"/> <description> @@ -30,15 +30,15 @@ <launchable type="desktop-id">vncviewer.desktop</launchable> <screenshots> <screenshot type="default"> - <caption>TigerVNC viewer connection to a CentOS machine</caption> + <caption>TigerVNC connection to a CentOS machine</caption> <image>https://raw.githubusercontent.com/TigerVNC/tigervnc/741d3edbfab65eda6f033078bc06347fe244ea6a/vncviewer/metainfo/tigervnc-connection-linux.jpg</image> </screenshot> <screenshot> - <caption>TigerVNC viewer connection to a macOS machine</caption> + <caption>TigerVNC connection to a macOS machine</caption> <image>https://raw.githubusercontent.com/TigerVNC/tigervnc/741d3edbfab65eda6f033078bc06347fe244ea6a/vncviewer/metainfo/tigervnc-connection-macos.jpg</image> </screenshot> <screenshot> - <caption>TigerVNC viewer connection to a Windows machine</caption> + <caption>TigerVNC connection to a Windows machine</caption> <image>https://raw.githubusercontent.com/TigerVNC/tigervnc/741d3edbfab65eda6f033078bc06347fe244ea6a/vncviewer/metainfo/tigervnc-connection-windows.jpg</image> </screenshot> </screenshots> diff --git a/vncviewer/parameters.cxx b/vncviewer/parameters.cxx index 957838a7..d2000181 100644 --- a/vncviewer/parameters.cxx +++ b/vncviewer/parameters.cxx @@ -217,19 +217,21 @@ core::BoolParameter true); core::StringParameter display("display", - "Specifies the X display on which the VNC viewer window " + "Specifies the X display on which the TigerVNC window " "should appear.", ""); #endif -// Empty string means None, for backward compatibility -core::EnumParameter - menuKey("MenuKey", - "The key which brings up the popup menu", - {"", "None", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", - "F9", "F10", "F11", "F12", "Pause", "Scroll_Lock", "Escape", - "Insert", "Delete", "Home", "Page_Up", "Page_Down"}, - "F8"); +// Keep list of valid values in sync with ShortcutHandler +core::EnumListParameter + shortcutModifiers("ShortcutModifiers", + "The combination of modifier keys that triggers " + "special actions in the viewer instead of being " + "sent to the remote session. Possible values are a " + "combination of Ctrl, Shift, Alt, and Super.", + {"Ctrl", "Shift", "Alt", "Super", + "Win", "Option", "Cmd"}, + {"Ctrl", "Alt"}); core::BoolParameter fullscreenSystemKeys("FullscreenSystemKeys", @@ -282,8 +284,9 @@ static core::VoidParameter* parameterArray[] = { &sendPrimary, &setPrimary, #endif - &menuKey, - &fullscreenSystemKeys + &fullscreenSystemKeys, + /* Keyboard shortcuts */ + &shortcutModifiers, }; static core::VoidParameter* readOnlyParameterArray[] = { diff --git a/vncviewer/parameters.h b/vncviewer/parameters.h index 4dafdaa0..4dc30db6 100644 --- a/vncviewer/parameters.h +++ b/vncviewer/parameters.h @@ -73,7 +73,7 @@ extern core::BoolParameter sendPrimary; extern core::StringParameter display; #endif -extern core::EnumParameter menuKey; +extern core::EnumListParameter shortcutModifiers; extern core::BoolParameter fullscreenSystemKeys; extern core::BoolParameter alertOnFatalError; diff --git a/vncviewer/vncviewer.cxx b/vncviewer/vncviewer.cxx index 3db9bd64..382119b8 100644 --- a/vncviewer/vncviewer.cxx +++ b/vncviewer/vncviewer.cxx @@ -100,7 +100,7 @@ static const char *about_text() // encodings, so we need to make sure we get a fresh string every // time. snprintf(buffer, sizeof(buffer), - _("TigerVNC viewer v%s\n" + _("TigerVNC v%s\n" "Built on: %s\n" "Copyright (C) 1999-%d TigerVNC team and many others (see README.rst)\n" "See https://www.tigervnc.org for information on TigerVNC."), @@ -170,7 +170,7 @@ bool should_disconnect() void about_vncviewer() { - fl_message_title(_("About TigerVNC Viewer")); + fl_message_title(_("About TigerVNC")); fl_message("%s", about_text()); } @@ -241,7 +241,7 @@ static void new_connection_cb(Fl_Widget* /*widget*/, void* /*data*/) pid = fork(); if (pid == -1) { - vlog.error(_("Error starting new TigerVNC Viewer: %s"), strerror(errno)); + vlog.error(_("Error starting new connection: %s"), strerror(errno)); return; } @@ -253,7 +253,7 @@ static void new_connection_cb(Fl_Widget* /*widget*/, void* /*data*/) execvp(argv[0], (char * const *)argv); - vlog.error(_("Error starting new TigerVNC Viewer: %s"), strerror(errno)); + vlog.error(_("Error starting new connection: %s"), strerror(errno)); _exit(1); } #endif @@ -262,7 +262,7 @@ static void CleanupSignalHandler(int sig) { // CleanupSignalHandler allows C++ object cleanup to happen because it calls // exit() rather than the default which is to abort. - vlog.info(_("Termination signal %d has been received. TigerVNC viewer will now exit."), sig); + vlog.info(_("Termination signal %d has been received. TigerVNC will now exit."), sig); exit(1); } @@ -387,7 +387,7 @@ static void init_fltk() fl_message_hotspot(false); // Avoid empty titles for popups - fl_message_title_default(_("TigerVNC viewer")); + fl_message_title_default("TigerVNC"); // FLTK exposes these so that we can translate them. fl_no = _("No"); @@ -464,7 +464,7 @@ static void usage(const char *programName) fprintf(stderr, _("\n" "Options:\n\n" " -display Xdisplay - Specifies the X display for the viewer window\n" - " -geometry geometry - Initial position of the main VNC viewer window. See the\n" + " -geometry geometry - Initial position of the main TigerVNC window. See the\n" " man page for details.\n")); #endif diff --git a/vncviewer/vncviewer.desktop.in.in b/vncviewer/vncviewer.desktop.in.in index 1a91755c..705845d9 100644 --- a/vncviewer/vncviewer.desktop.in.in +++ b/vncviewer/vncviewer.desktop.in.in @@ -1,5 +1,5 @@ [Desktop Entry] -Name=TigerVNC viewer +Name=TigerVNC GenericName=Remote desktop viewer Comment=Connect to VNC server and display remote desktop Exec=@CMAKE_INSTALL_FULL_BINDIR@/vncviewer diff --git a/vncviewer/vncviewer.man b/vncviewer/vncviewer.man index 208858f9..7597c1a6 100644 --- a/vncviewer/vncviewer.man +++ b/vncviewer/vncviewer.man @@ -77,24 +77,40 @@ safely. Automatic selection can be turned off by setting the \fBAutoSelect\fP parameter to false, or from the options dialog. -.SH POPUP MENU -The viewer has a popup menu containing entries which perform various actions. -It is usually brought up by pressing F8, but this can be configured with the -MenuKey parameter. Actions which the popup menu can perform include: -.RS 2 -.IP * 2 -switching in and out of full-screen mode -.IP * -quitting the viewer -.IP * -generating key events, e.g. sending ctrl-alt-del -.IP * -accessing the options dialog and various other dialogs -.RE -.PP -By default, key presses in the popup menu get sent to the VNC server and -dismiss the popup. So to get an F8 through to the VNC server simply press it -twice. +.SH KEYBOARD SHORTCUTS + +The viewer can be controlled using certain key combinations, invoking +special actions instead of passing the keyboard events on to the remote +session. By default pressing Ctrl+Alt and something else will be +interpreted as a keyboard shortcut for the viewer, but this can be +changed witht the \fBShortcutModifiers\fP parameter. + +The possible keyboard shortcuts are: + +.TP +Ctrl+Alt +Releases control of the keyboard and allows system keys to be used +locally. +. +.TP +Ctrl+Alt+G +Grabs control of the keyboard and allows system keys (like Alt+Tab) to +be sent to the remote session. +. +.TP +Ctrl+Alt+Enter +Toggles full-screen mode. +. +.TP +Ctrl+Alt+M +Opens a popup menu that can perform various extra actions, such as +quitting the viewer or opening the options dialog. +. +.TP +Ctrl+Alt+Space +Temporarily bypasses the keyboard shortcuts, allowing the same key +combinations to be sent to the remote session. +. .SH FULL-SCREEN MODE A full-screen mode is supported. This is particularly useful when connecting @@ -154,7 +170,7 @@ the SetDesktopSize message then the screen will retain the original size. . .TP .B \-display \fIXdisplay\fP -Specifies the X display on which the VNC viewer window should appear. +Specifies the X display on which the TigerVNC window should appear. . .TP .B \-DotWhenNoCursor (DEPRECATED) @@ -196,12 +212,12 @@ The default is "1". . .TP .B \-FullscreenSystemKeys -Pass special keys (like Alt+Tab) directly to the server when in full-screen -mode. +Automatically grab all input from the keyboard when entering full-screen +and pass special keys (like Alt+Tab) directly to the server. . .TP .B \-geometry \fIgeometry\fP -Initial position of the main VNC viewer window. The format is +Initial position of the main TigerVNC window. The format is .B \fIwidth\fPx\fIheight\fP+\fIxoffset\fP+\fIyoffset\fP , where `+' signs can be replaced with `\-' signs to specify offsets from the right and/or from the bottom of the screen. Offsets are optional and the @@ -246,13 +262,6 @@ Default is \fB262144\fP. Maximize viewer window. . .TP -.B \-MenuKey \fIkeysym-name\fP -This option specifies the key which brings up the popup menu, or None to -disable the menu. The currently supported list is: F1, F2, F3, F4, F5, -F6, F7, F8, F9, F10, F11, F12, Pause, Scroll_Lock, Escape, Insert, -Delete, Home, Page_Up, Page_Down). Default is F8. -. -.TP .B \-NoJpeg Disable lossy JPEG compression in Tight encoding. Default is off. . @@ -319,6 +328,13 @@ normally closed. This option requests that they be left open, allowing you to share the desktop with someone already using it. . .TP +.B \-ShortcutModifiers \fIkeys\fP +The combination of modifier keys that triggers special actions in the +viewer instead of being sent to the remote session. Possible values are +a combination of \fBCtrl\fP, \fBShift\fP, \fBAlt\fP, and \fBSuper\fP. +Default is \fBCtrl,Alt\fP. +. +.TP .B \-UseIPv4 Use IPv4 for incoming and outgoing connections. Default is on. . diff --git a/vncviewer/vncviewer.rc.in b/vncviewer/vncviewer.rc.in index 43e44da3..375da7af 100644 --- a/vncviewer/vncviewer.rc.in +++ b/vncviewer/vncviewer.rc.in @@ -42,9 +42,9 @@ BEGIN BLOCK "080904b0" BEGIN VALUE "Comments", "\0" - VALUE "CompanyName", "TigerVNC project\0" - VALUE "FileDescription", "TigerVNC client\0" - VALUE "ProductName", "TigerVNC client\0" + VALUE "CompanyName", "TigerVNC team\0" + VALUE "FileDescription", "TigerVNC\0" + VALUE "ProductName", "TigerVNC\0" VALUE "FileVersion", "@RCVERSION@\0" VALUE "InternalName", "vncviewer\0" VALUE "LegalCopyright", "Copyright (C) 1999-2025 TigerVNC team and many others (see README.rst)\0" diff --git a/vncviewer/win32.c b/vncviewer/win32.c index b0a3813c..c649f783 100644 --- a/vncviewer/win32.c +++ b/vncviewer/win32.c @@ -27,39 +27,50 @@ static HANDLE thread; static DWORD thread_id; static HHOOK hook = 0; +static BYTE kbd_state[256]; static HWND target_wnd = 0; -static int is_system_hotkey(int vkCode) { - switch (vkCode) { - case VK_LWIN: - case VK_RWIN: - case VK_SNAPSHOT: - return 1; - case VK_TAB: - if (GetAsyncKeyState(VK_MENU) & 0x8000) - return 1; - break; - case VK_ESCAPE: - if (GetAsyncKeyState(VK_MENU) & 0x8000) - return 1; - if (GetAsyncKeyState(VK_CONTROL) & 0x8000) - return 1; - break; - } - return 0; -} - static LRESULT CALLBACK keyboard_hook(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { KBDLLHOOKSTRUCT* msgInfo = (KBDLLHOOKSTRUCT*)lParam; - // Grabbing everything seems to mess up some keyboard state that - // FLTK relies on, so just grab the keys that we normally cannot. - if (is_system_hotkey(msgInfo->vkCode)) { - PostMessage(target_wnd, wParam, msgInfo->vkCode, - (msgInfo->scanCode & 0xff) << 16 | - (msgInfo->flags & 0xff) << 24); + BYTE vkey; + BYTE scanCode; + BYTE flags; + + vkey = msgInfo->vkCode; + scanCode = msgInfo->scanCode; + flags = msgInfo->flags; + + // We get the low level vkeys here, but the application code + // expects this to have been translated to the generic ones + switch (vkey) { + case VK_LSHIFT: + case VK_RSHIFT: + vkey = VK_SHIFT; + // The extended bit is also always missing for right shift + flags &= ~0x01; + break; + case VK_LCONTROL: + case VK_RCONTROL: + vkey = VK_CONTROL; + break; + case VK_LMENU: + case VK_RMENU: + vkey = VK_MENU; + break; + } + + // If the key was pressed before the grab was activated, then we + // need to avoid intercepting the release event or Windows will get + // confused about the state of the key + if (((wParam == WM_KEYUP) || (wParam == WM_SYSKEYUP)) && + (kbd_state[msgInfo->vkCode] & 0x80)) { + kbd_state[msgInfo->vkCode] &= ~0x80; + } else { + PostMessage(target_wnd, wParam, vkey, + scanCode << 16 | flags << 24); return 1; } } @@ -76,6 +87,9 @@ static DWORD WINAPI keyboard_thread(LPVOID data) // Make sure a message queue is created PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE | PM_NOYIELD); + // We need to know which keys are currently pressed + GetKeyboardState(kbd_state); + hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_hook, GetModuleHandle(0), 0); // If something goes wrong then there is not much we can do. // Just sit around and wait for WM_QUIT... diff --git a/win/rfb_win32/SecurityPage.cxx b/win/rfb_win32/SecurityPage.cxx index f9321160..94a88492 100644 --- a/win/rfb_win32/SecurityPage.cxx +++ b/win/rfb_win32/SecurityPage.cxx @@ -123,9 +123,6 @@ SecurityPage::onOk() { bool vnc_loaded = false; list<uint32_t> secTypes; - /* Keep same priorities as in common/rfb/SecurityClient::secTypes */ - secTypes.push_back(secTypeVeNCrypt); - #ifdef HAVE_GNUTLS /* X509Plain */ if (authMethodEnabled(IDC_ENC_X509, IDC_AUTH_PLAIN)) { diff --git a/win/vncconfig/vncconfig.rc b/win/vncconfig/vncconfig.rc index f4b856dd..e8b50ed1 100644 --- a/win/vncconfig/vncconfig.rc +++ b/win/vncconfig/vncconfig.rc @@ -459,7 +459,7 @@ BEGIN BLOCK "080904b0" BEGIN VALUE "Comments", "\0" - VALUE "CompanyName", "TigerVNC project\0" + VALUE "CompanyName", "TigerVNC team\0" #ifdef WIN64 VALUE "FileDescription", "TigerVNC server configuration applet for Win64\0" VALUE "ProductName", "TigerVNC server configuration applet for Win64\0" diff --git a/win/winvnc/ControlPanel.cxx b/win/winvnc/ControlPanel.cxx index 6c593c45..9041d81f 100644 --- a/win/winvnc/ControlPanel.cxx +++ b/win/winvnc/ControlPanel.cxx @@ -31,9 +31,9 @@ void ControlPanel::initDialog() SendCommand(4, -1); } -bool ControlPanel::onCommand(int cmd) +bool ControlPanel::onCommand(int item, int /*cmd*/) { - switch (cmd) { + switch (item) { case IDC_PROPERTIES: SendMessage(m_hSTIcon, WM_COMMAND, ID_OPTIONS, 0); return false; @@ -122,7 +122,7 @@ BOOL ControlPanel::dialogProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM /*lPara EndDialog(hwnd, 0); return TRUE; default: - return onCommand(LOWORD(wParam)); + return onCommand(LOWORD(wParam), HIWORD(wParam)); } } return FALSE; diff --git a/win/winvnc/ControlPanel.h b/win/winvnc/ControlPanel.h index 23aff0a5..3b994a59 100644 --- a/win/winvnc/ControlPanel.h +++ b/win/winvnc/ControlPanel.h @@ -26,7 +26,7 @@ namespace winvnc { }; virtual bool showDialog(); void initDialog() override; - virtual bool onCommand(int cmd); + bool onCommand(int item, int cmd) override; void UpdateListView(ListConnInfo* LCInfo); HWND GetHandle() {return handle;}; void SendCommand(DWORD command, int data); diff --git a/win/winvnc/QueryConnectDialog.cxx b/win/winvnc/QueryConnectDialog.cxx index 5d609898..9a95e23e 100644 --- a/win/winvnc/QueryConnectDialog.cxx +++ b/win/winvnc/QueryConnectDialog.cxx @@ -48,7 +48,8 @@ QueryConnectDialog::QueryConnectDialog(network::Socket* sock_, const char* userName_, VNCServerWin32* s) : Dialog(GetModuleHandle(nullptr)), - sock(sock_), peerIp(sock->getPeerAddress()), userName(userName_), + sock(sock_), peerIp(sock->getPeerAddress()), + userName(userName_?userName_:""), approve(false), server(s) { } diff --git a/win/winvnc/winvnc.rc b/win/winvnc/winvnc.rc index 0c756054..acaa0dbd 100644 --- a/win/winvnc/winvnc.rc +++ b/win/winvnc/winvnc.rc @@ -76,7 +76,7 @@ BEGIN BLOCK "080904b0" BEGIN VALUE "Comments", "\0" - VALUE "CompanyName", "TigerVNC project\0" + VALUE "CompanyName", "TigerVNC team\0" VALUE "FileDescription", "TigerVNC server\0" VALUE "ProductName", "TigerVNC server\0" VALUE "FileVersion", __RCVERSIONSTR diff --git a/win/wm_hooks/wm_hooks.rc b/win/wm_hooks/wm_hooks.rc index ae56b314..2bf38f3d 100644 --- a/win/wm_hooks/wm_hooks.rc +++ b/win/wm_hooks/wm_hooks.rc @@ -72,12 +72,12 @@ BEGIN BLOCK "080904b0" BEGIN VALUE "Comments", "\0" - VALUE "CompanyName", "TigerVNC project\0" + VALUE "CompanyName", "TigerVNC team\0" VALUE "FileDescription", "TigerVNC server hooking DLL\0" VALUE "ProductName", "TigerVNC server hooking DLL\0" VALUE "FileVersion", __RCVERSIONSTR VALUE "InternalName", "\0" - VALUE "LegalCopyright", "Copyright (C) 1999-2005 [many holders]\0" + VALUE "LegalCopyright", "Copyright (C) 1999-2025 TigerVNC team and many others (see README.rst)\0" VALUE "LegalTrademarks", "TigerVNC\0" VALUE "OriginalFilename", "wm_hooks.dll\0" VALUE "PrivateBuild", "\0" |