aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml10
-rw-r--r--BUILDING.txt4
-rw-r--r--CMakeLists.txt35
-rw-r--r--common/core/Configuration.cxx4
-rw-r--r--common/core/Configuration.h2
-rw-r--r--common/core/string.cxx3
-rw-r--r--common/network/Socket.cxx2
-rw-r--r--common/network/TcpSocket.cxx2
-rw-r--r--common/rdr/CMakeLists.txt1
-rw-r--r--common/rdr/InStream.h8
-rw-r--r--common/rdr/TLSException.cxx23
-rw-r--r--common/rdr/TLSException.h6
-rw-r--r--common/rdr/TLSInStream.cxx91
-rw-r--r--common/rdr/TLSInStream.h18
-rw-r--r--common/rdr/TLSOutStream.cxx77
-rw-r--r--common/rdr/TLSOutStream.h17
-rw-r--r--common/rdr/TLSSocket.cxx228
-rw-r--r--common/rdr/TLSSocket.h81
-rw-r--r--common/rfb/CMakeLists.txt2
-rw-r--r--common/rfb/CSecurityTLS.cxx168
-rw-r--r--common/rfb/CSecurityTLS.h7
-rw-r--r--common/rfb/SSecurityPlain.cxx5
-rw-r--r--common/rfb/SSecurityPlain.h12
-rw-r--r--common/rfb/SSecurityRSAAES.cxx5
-rw-r--r--common/rfb/SSecurityTLS.cxx58
-rw-r--r--common/rfb/SSecurityTLS.h7
-rw-r--r--common/rfb/UnixPasswordValidator.cxx107
-rw-r--r--common/rfb/UnixPasswordValidator.h14
-rw-r--r--common/rfb/WinPasswdValidator.cxx3
-rw-r--r--common/rfb/WinPasswdValidator.h6
-rw-r--r--common/rfb/pam.c95
-rw-r--r--common/rfb/pam.h34
-rw-r--r--contrib/packages/deb/ubuntu-focal/debian/rules5
-rw-r--r--contrib/packages/deb/ubuntu-jammy/debian/rules5
-rw-r--r--contrib/packages/deb/ubuntu-noble/debian/rules5
-rw-r--r--contrib/packages/rpm/el8/SPECS/tigervnc.spec6
-rw-r--r--contrib/packages/rpm/el9/SPECS/tigervnc.spec6
-rw-r--r--java/com/tigervnc/vncviewer/MANIFEST.MF2
-rw-r--r--java/com/tigervnc/vncviewer/OptionsDialog.java2
-rw-r--r--java/com/tigervnc/vncviewer/ServerDialog.java4
-rw-r--r--java/com/tigervnc/vncviewer/Viewport.java2
-rw-r--r--java/com/tigervnc/vncviewer/VncViewer.java4
-rw-r--r--po/CMakeLists.txt1
-rw-r--r--po/sr.po686
-rw-r--r--release/CMakeLists.txt35
-rw-r--r--release/Info.plist.in12
-rw-r--r--release/makemacapp.in10
-rw-r--r--release/maketarball.in14
-rw-r--r--release/tigervnc.iss.in6
-rw-r--r--release/winvnc.iss.in2
-rw-r--r--tests/unit/CMakeLists.txt4
-rw-r--r--tests/unit/parameters.cxx28
-rw-r--r--tests/unit/shortcuthandler.cxx607
-rw-r--r--unix/vncserver/selinux/vncsession.te12
-rw-r--r--unix/x0vncserver/x0vncserver.cxx45
-rw-r--r--unix/xserver/hw/vnc/RFBGlue.cc33
-rw-r--r--unix/xserver/hw/vnc/RFBGlue.h3
-rw-r--r--unix/xserver/hw/vnc/XserverDesktop.cc38
-rw-r--r--unix/xserver/hw/vnc/vncModule.c2
-rw-r--r--unix/xserver/hw/vnc/xvnc.c33
-rw-r--r--vncviewer/CConn.cxx35
-rw-r--r--vncviewer/CMakeLists.txt2
-rw-r--r--vncviewer/DesktopWindow.cxx312
-rw-r--r--vncviewer/DesktopWindow.h36
-rw-r--r--vncviewer/Keyboard.h3
-rw-r--r--vncviewer/KeyboardMacOS.h6
-rw-r--r--vncviewer/KeyboardMacOS.mm157
-rw-r--r--vncviewer/KeyboardWin32.cxx150
-rw-r--r--vncviewer/KeyboardWin32.h4
-rw-r--r--vncviewer/KeyboardX11.cxx113
-rw-r--r--vncviewer/KeyboardX11.h8
-rw-r--r--vncviewer/OptionsDialog.cxx208
-rw-r--r--vncviewer/OptionsDialog.h16
-rw-r--r--vncviewer/ServerDialog.cxx2
-rw-r--r--vncviewer/ShortcutHandler.cxx275
-rw-r--r--vncviewer/ShortcutHandler.h79
-rw-r--r--vncviewer/Viewport.cxx219
-rw-r--r--vncviewer/Viewport.h9
-rw-r--r--vncviewer/cocoa.h7
-rw-r--r--vncviewer/cocoa.mm233
-rw-r--r--vncviewer/fltk/Fl_Navigation.cxx12
-rw-r--r--vncviewer/menukey.cxx83
-rw-r--r--vncviewer/menukey.h34
-rw-r--r--vncviewer/org.tigervnc.vncviewer.metainfo.xml.in8
-rw-r--r--vncviewer/parameters.cxx25
-rw-r--r--vncviewer/parameters.h2
-rw-r--r--vncviewer/vncviewer.cxx14
-rw-r--r--vncviewer/vncviewer.desktop.in.in2
-rw-r--r--vncviewer/vncviewer.man74
-rw-r--r--vncviewer/vncviewer.rc.in6
-rw-r--r--vncviewer/win32.c66
-rw-r--r--win/rfb_win32/SecurityPage.cxx3
-rw-r--r--win/vncconfig/vncconfig.rc2
-rw-r--r--win/winvnc/ControlPanel.cxx6
-rw-r--r--win/winvnc/ControlPanel.h2
-rw-r--r--win/winvnc/QueryConnectDialog.cxx3
-rw-r--r--win/winvnc/winvnc.rc2
-rw-r--r--win/wm_hooks/wm_hooks.rc4
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_
diff --git a/po/sr.po b/po/sr.po
index fceaa6f3..ccef6737 100644
--- a/po/sr.po
+++ b/po/sr.po
@@ -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"