diff options
Diffstat (limited to 'vncviewer/DesktopWindow.cxx')
-rw-r--r-- | vncviewer/DesktopWindow.cxx | 423 |
1 files changed, 229 insertions, 194 deletions
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) |