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