1 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
2  * Copyright 2011-2019 Pierre Ossman for Cendio AB
3  *
4  * This is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This software is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this software; if not, write to the Free Software
16  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
17  * USA.
18  */
19 
20 #ifdef HAVE_CONFIG_H
21 #include <config.h>
22 #endif
23 
24 #include <assert.h>
25 #include <stdio.h>
26 #include <string.h>
27 
28 #include <rfb/CMsgWriter.h>
29 #include <rfb/LogWriter.h>
30 #include <rfb/Exception.h>
31 #include <rfb/ledStates.h>
32 
33 // FLTK can pull in the X11 headers on some systems
34 #ifndef XK_VoidSymbol
35 #define XK_LATIN1
36 #define XK_MISCELLANY
37 #define XK_XKB_KEYS
38 #include <rfb/keysymdef.h>
39 #endif
40 
41 #ifndef XF86XK_ModeLock
42 #include <rfb/XF86keysym.h>
43 #endif
44 
45 #if ! (defined(WIN32) || defined(__APPLE__))
46 #include <X11/XKBlib.h>
47 #endif
48 
49 #ifndef NoSymbol
50 #define NoSymbol 0
51 #endif
52 
53 // Missing in at least some versions of MinGW
54 #ifndef MAPVK_VK_TO_VSC
55 #define MAPVK_VK_TO_VSC 0
56 #endif
57 
58 #include "Viewport.h"
59 #include "CConn.h"
60 #include "OptionsDialog.h"
61 #include "DesktopWindow.h"
62 #include "i18n.h"
63 #include "fltk_layout.h"
64 #include "parameters.h"
65 #include "keysym2ucs.h"
66 #include "menukey.h"
67 #include "vncviewer.h"
68 
69 #include "PlatformPixelBuffer.h"
70 
71 #include <FL/fl_draw.H>
72 #include <FL/fl_ask.H>
73 
74 #include <FL/Fl_Menu.H>
75 #include <FL/Fl_Menu_Button.H>
76 
77 #if !defined(WIN32) && !defined(__APPLE__)
78 #include <X11/XKBlib.h>
79 extern const struct _code_map_xkb_to_qnum {
80   const char * from;
81   const unsigned short to;
82 } code_map_xkb_to_qnum[];
83 extern const unsigned int code_map_xkb_to_qnum_len;
84 
85 static int code_map_keycode_to_qnum[256];
86 #endif
87 
88 #ifdef __APPLE__
89 #include "cocoa.h"
90 extern const unsigned short code_map_osx_to_qnum[];
91 extern const unsigned int code_map_osx_to_qnum_len;
92 #endif
93 
94 #ifdef WIN32
95 #include "win32.h"
96 #endif
97 
98 
99 using namespace rfb;
100 using namespace rdr;
101 
102 static rfb::LogWriter vlog("Viewport");
103 
104 // Menu constants
105 
106 enum { ID_DISCONNECT, ID_FULLSCREEN, ID_MINIMIZE, ID_RESIZE,
107        ID_CTRL, ID_ALT, ID_MENUKEY, ID_CTRLALTDEL,
108        ID_REFRESH, ID_OPTIONS, ID_INFO, ID_ABOUT };
109 
110 // Used to detect fake input (0xaa is not a real key)
111 #ifdef WIN32
112 static const WORD SCAN_FAKE = 0xaa;
113 #endif
114 
Viewport(int w,int h,const rfb::PixelFormat & serverPF,CConn * cc_)115 Viewport::Viewport(int w, int h, const rfb::PixelFormat& serverPF, CConn* cc_)
116   : Fl_Widget(0, 0, w, h), cc(cc_), frameBuffer(NULL),
117     lastPointerPos(0, 0), lastButtonMask(0),
118 #ifdef WIN32
119     altGrArmed(false),
120 #endif
121     firstLEDState(true),
122     pendingServerClipboard(false), pendingClientClipboard(false),
123     menuCtrlKey(false), menuAltKey(false), cursor(NULL)
124 {
125 #if !defined(WIN32) && !defined(__APPLE__)
126   XkbDescPtr xkb;
127   Status status;
128 
129   xkb = XkbGetMap(fl_display, 0, XkbUseCoreKbd);
130   if (!xkb)
131     throw rfb::Exception("XkbGetMap");
132 
133   status = XkbGetNames(fl_display, XkbKeyNamesMask, xkb);
134   if (status != Success)
135     throw rfb::Exception("XkbGetNames");
136 
137   memset(code_map_keycode_to_qnum, 0, sizeof(code_map_keycode_to_qnum));
138   for (KeyCode keycode = xkb->min_key_code;
139        keycode < xkb->max_key_code;
140        keycode++) {
141     const char *keyname = xkb->names->keys[keycode].name;
142     unsigned short rfbcode;
143 
144     if (keyname[0] == '\0')
145       continue;
146 
147     rfbcode = 0;
148     for (unsigned i = 0;i < code_map_xkb_to_qnum_len;i++) {
149         if (strncmp(code_map_xkb_to_qnum[i].from,
150                     keyname, XkbKeyNameLength) == 0) {
151             rfbcode = code_map_xkb_to_qnum[i].to;
152             break;
153         }
154     }
155     if (rfbcode != 0)
156         code_map_keycode_to_qnum[keycode] = rfbcode;
157     else
158         vlog.debug("No key mapping for key %.4s", keyname);
159   }
160 
161   XkbFreeKeyboard(xkb, 0, True);
162 #endif
163 
164   Fl::add_clipboard_notify(handleClipboardChange, this);
165 
166   // We need to intercept keyboard events early
167   Fl::add_system_handler(handleSystemEvent, this);
168 
169   frameBuffer = new PlatformPixelBuffer(w, h);
170   assert(frameBuffer);
171   cc->setFramebuffer(frameBuffer);
172 
173   contextMenu = new Fl_Menu_Button(0, 0, 0, 0);
174   // Setting box type to FL_NO_BOX prevents it from trying to draw the
175   // button component (which we don't want)
176   contextMenu->box(FL_NO_BOX);
177 
178   // The (invisible) button associated with this widget can mess with
179   // things like Fl_Scroll so we need to get rid of any parents.
180   // Unfortunately that's not possible because of STR #2654, but
181   // reparenting to the current window works for most cases.
182   window()->add(contextMenu);
183 
184   setMenuKey();
185 
186   OptionsDialog::addCallback(handleOptions, this);
187 
188   // Make sure we have an initial blank cursor set
189   setCursor(0, 0, rfb::Point(0, 0), NULL);
190 }
191 
192 
~Viewport()193 Viewport::~Viewport()
194 {
195   // Unregister all timeouts in case they get a change tro trigger
196   // again later when this object is already gone.
197   Fl::remove_timeout(handlePointerTimeout, this);
198 #ifdef WIN32
199   Fl::remove_timeout(handleAltGrTimeout, this);
200 #endif
201 
202   Fl::remove_system_handler(handleSystemEvent);
203 
204   Fl::remove_clipboard_notify(handleClipboardChange);
205 
206   OptionsDialog::removeCallback(handleOptions);
207 
208   if (cursor) {
209     if (!cursor->alloc_array)
210       delete [] cursor->array;
211     delete cursor;
212   }
213 
214   // FLTK automatically deletes all child widgets, so we shouldn't touch
215   // them ourselves here
216 }
217 
218 
getPreferredPF()219 const rfb::PixelFormat &Viewport::getPreferredPF()
220 {
221   return frameBuffer->getPF();
222 }
223 
224 
225 // Copy the areas of the framebuffer that have been changed (damaged)
226 // to the displayed window.
227 
updateWindow()228 void Viewport::updateWindow()
229 {
230   Rect r;
231 
232   r = frameBuffer->getDamage();
233   damage(FL_DAMAGE_USER1, r.tl.x + x(), r.tl.y + y(), r.width(), r.height());
234 }
235 
236 static const char * dotcursor_xpm[] = {
237   "5 5 2 1",
238   ".	c #000000",
239   " 	c #FFFFFF",
240   "     ",
241   " ... ",
242   " ... ",
243   " ... ",
244   "     "};
245 
setCursor(int width,int height,const Point & hotspot,const rdr::U8 * data)246 void Viewport::setCursor(int width, int height, const Point& hotspot,
247                          const rdr::U8* data)
248 {
249   int i;
250 
251   if (cursor) {
252     if (!cursor->alloc_array)
253       delete [] cursor->array;
254     delete cursor;
255   }
256 
257   for (i = 0; i < width*height; i++)
258     if (data[i*4 + 3] != 0) break;
259 
260   if ((i == width*height) && dotWhenNoCursor) {
261     vlog.debug("cursor is empty - using dot");
262 
263     Fl_Pixmap pxm(dotcursor_xpm);
264     cursor = new Fl_RGB_Image(&pxm);
265     cursorHotspot.x = cursorHotspot.y = 2;
266   } else {
267     if ((width == 0) || (height == 0)) {
268       U8 *buffer = new U8[4];
269       memset(buffer, 0, 4);
270       cursor = new Fl_RGB_Image(buffer, 1, 1, 4);
271       cursorHotspot.x = cursorHotspot.y = 0;
272     } else {
273       U8 *buffer = new U8[width * height * 4];
274       memcpy(buffer, data, width * height * 4);
275       cursor = new Fl_RGB_Image(buffer, width, height, 4);
276       cursorHotspot = hotspot;
277     }
278   }
279 
280   if (Fl::belowmouse() == this)
281     window()->cursor(cursor, cursorHotspot.x, cursorHotspot.y);
282 }
283 
handleClipboardRequest()284 void Viewport::handleClipboardRequest()
285 {
286   Fl::paste(*this, clipboardSource);
287 }
288 
handleClipboardAnnounce(bool available)289 void Viewport::handleClipboardAnnounce(bool available)
290 {
291   if (!acceptClipboard)
292     return;
293 
294   if (!available) {
295     vlog.debug("Clipboard is no longer available on server");
296     pendingServerClipboard = false;
297     return;
298   }
299 
300   pendingClientClipboard = false;
301 
302   if (!hasFocus()) {
303     vlog.debug("Got notification of new clipboard on server whilst not focused, will request data later");
304     pendingServerClipboard = true;
305     return;
306   }
307 
308   vlog.debug("Got notification of new clipboard on server, requesting data");
309   cc->requestClipboard();
310 }
311 
handleClipboardData(const char * data)312 void Viewport::handleClipboardData(const char* data)
313 {
314   size_t len;
315 
316   if (!hasFocus())
317     return;
318 
319   len = strlen(data);
320 
321   vlog.debug("Got clipboard data (%d bytes)", (int)len);
322 
323   // RFB doesn't have separate selection and clipboard concepts, so we
324   // dump the data into both variants.
325 #if !defined(WIN32) && !defined(__APPLE__)
326   if (setPrimary)
327     Fl::copy(data, len, 0);
328 #endif
329   Fl::copy(data, len, 1);
330 }
331 
setLEDState(unsigned int state)332 void Viewport::setLEDState(unsigned int state)
333 {
334   vlog.debug("Got server LED state: 0x%08x", state);
335 
336   // The first message is just considered to be the server announcing
337   // support for this extension. We will push our state to sync up the
338   // server when we get focus. If we already have focus we need to push
339   // it here though.
340   if (firstLEDState) {
341     firstLEDState = false;
342     if (hasFocus())
343       pushLEDState();
344     return;
345   }
346 
347   if (!hasFocus())
348     return;
349 
350 #if defined(WIN32)
351   INPUT input[6];
352   UINT count;
353   UINT ret;
354 
355   memset(input, 0, sizeof(input));
356   count = 0;
357 
358   if (!!(state & ledCapsLock) != !!(GetKeyState(VK_CAPITAL) & 0x1)) {
359     input[count].type = input[count+1].type = INPUT_KEYBOARD;
360     input[count].ki.wVk = input[count+1].ki.wVk = VK_CAPITAL;
361     input[count].ki.wScan = input[count+1].ki.wScan = SCAN_FAKE;
362     input[count].ki.dwFlags = 0;
363     input[count+1].ki.dwFlags = KEYEVENTF_KEYUP;
364     count += 2;
365   }
366 
367   if (!!(state & ledNumLock) != !!(GetKeyState(VK_NUMLOCK) & 0x1)) {
368     input[count].type = input[count+1].type = INPUT_KEYBOARD;
369     input[count].ki.wVk = input[count+1].ki.wVk = VK_NUMLOCK;
370     input[count].ki.wScan = input[count+1].ki.wScan = SCAN_FAKE;
371     input[count].ki.dwFlags = KEYEVENTF_EXTENDEDKEY;
372     input[count+1].ki.dwFlags = KEYEVENTF_KEYUP | KEYEVENTF_EXTENDEDKEY;
373     count += 2;
374   }
375 
376   if (!!(state & ledScrollLock) != !!(GetKeyState(VK_SCROLL) & 0x1)) {
377     input[count].type = input[count+1].type = INPUT_KEYBOARD;
378     input[count].ki.wVk = input[count+1].ki.wVk = VK_SCROLL;
379     input[count].ki.wScan = input[count+1].ki.wScan = SCAN_FAKE;
380     input[count].ki.dwFlags = 0;
381     input[count+1].ki.dwFlags = KEYEVENTF_KEYUP;
382     count += 2;
383   }
384 
385   if (count == 0)
386     return;
387 
388   ret = SendInput(count, input, sizeof(*input));
389   if (ret < count)
390     vlog.error(_("Failed to update keyboard LED state: %lu"), GetLastError());
391 #elif defined(__APPLE__)
392   int ret;
393 
394   ret = cocoa_set_caps_lock_state(state & ledCapsLock);
395   if (ret != 0) {
396     vlog.error(_("Failed to update keyboard LED state: %d"), ret);
397     return;
398   }
399 
400   ret = cocoa_set_num_lock_state(state & ledNumLock);
401   if (ret != 0) {
402     vlog.error(_("Failed to update keyboard LED state: %d"), ret);
403     return;
404   }
405 
406   // No support for Scroll Lock //
407 
408 #else
409   unsigned int affect, values;
410   unsigned int mask;
411 
412   Bool ret;
413 
414   affect = values = 0;
415 
416   affect |= LockMask;
417   if (state & ledCapsLock)
418     values |= LockMask;
419 
420   mask = getModifierMask(XK_Num_Lock);
421   affect |= mask;
422   if (state & ledNumLock)
423     values |= mask;
424 
425   mask = getModifierMask(XK_Scroll_Lock);
426   affect |= mask;
427   if (state & ledScrollLock)
428     values |= mask;
429 
430   ret = XkbLockModifiers(fl_display, XkbUseCoreKbd, affect, values);
431   if (!ret)
432     vlog.error(_("Failed to update keyboard LED state"));
433 #endif
434 }
435 
pushLEDState()436 void Viewport::pushLEDState()
437 {
438   unsigned int state;
439 
440   // Server support?
441   if (cc->server.ledState() == ledUnknown)
442     return;
443 
444   state = 0;
445 
446 #if defined(WIN32)
447   if (GetKeyState(VK_CAPITAL) & 0x1)
448     state |= ledCapsLock;
449   if (GetKeyState(VK_NUMLOCK) & 0x1)
450     state |= ledNumLock;
451   if (GetKeyState(VK_SCROLL) & 0x1)
452     state |= ledScrollLock;
453 #elif defined(__APPLE__)
454   int ret;
455   bool on;
456 
457   ret = cocoa_get_caps_lock_state(&on);
458   if (ret != 0) {
459     vlog.error(_("Failed to get keyboard LED state: %d"), ret);
460     return;
461   }
462   if (on)
463     state |= ledCapsLock;
464 
465   ret = cocoa_get_num_lock_state(&on);
466   if (ret != 0) {
467     vlog.error(_("Failed to get keyboard LED state: %d"), ret);
468     return;
469   }
470   if (on)
471     state |= ledNumLock;
472 
473   // No support for Scroll Lock //
474   state |= (cc->server.ledState() & ledScrollLock);
475 
476 #else
477   unsigned int mask;
478 
479   Status status;
480   XkbStateRec xkbState;
481 
482   status = XkbGetState(fl_display, XkbUseCoreKbd, &xkbState);
483   if (status != Success) {
484     vlog.error(_("Failed to get keyboard LED state: %d"), status);
485     return;
486   }
487 
488   if (xkbState.locked_mods & LockMask)
489     state |= ledCapsLock;
490 
491   mask = getModifierMask(XK_Num_Lock);
492   if (xkbState.locked_mods & mask)
493     state |= ledNumLock;
494 
495   mask = getModifierMask(XK_Scroll_Lock);
496   if (xkbState.locked_mods & mask)
497     state |= ledScrollLock;
498 #endif
499 
500   if ((state & ledCapsLock) != (cc->server.ledState() & ledCapsLock)) {
501     vlog.debug("Inserting fake CapsLock to get in sync with server");
502     handleKeyPress(0x3a, XK_Caps_Lock);
503     handleKeyRelease(0x3a);
504   }
505   if ((state & ledNumLock) != (cc->server.ledState() & ledNumLock)) {
506     vlog.debug("Inserting fake NumLock to get in sync with server");
507     handleKeyPress(0x45, XK_Num_Lock);
508     handleKeyRelease(0x45);
509   }
510   if ((state & ledScrollLock) != (cc->server.ledState() & ledScrollLock)) {
511     vlog.debug("Inserting fake ScrollLock to get in sync with server");
512     handleKeyPress(0x46, XK_Scroll_Lock);
513     handleKeyRelease(0x46);
514   }
515 }
516 
517 
draw(Surface * dst)518 void Viewport::draw(Surface* dst)
519 {
520   int X, Y, W, H;
521 
522   // Check what actually needs updating
523   fl_clip_box(x(), y(), w(), h(), X, Y, W, H);
524   if ((W == 0) || (H == 0))
525     return;
526 
527   frameBuffer->draw(dst, X - x(), Y - y(), X, Y, W, H);
528 }
529 
530 
draw()531 void Viewport::draw()
532 {
533   int X, Y, W, H;
534 
535   // Check what actually needs updating
536   fl_clip_box(x(), y(), w(), h(), X, Y, W, H);
537   if ((W == 0) || (H == 0))
538     return;
539 
540   frameBuffer->draw(X - x(), Y - y(), X, Y, W, H);
541 }
542 
543 
resize(int x,int y,int w,int h)544 void Viewport::resize(int x, int y, int w, int h)
545 {
546   if ((w != frameBuffer->width()) || (h != frameBuffer->height())) {
547     vlog.debug("Resizing framebuffer from %dx%d to %dx%d",
548                frameBuffer->width(), frameBuffer->height(), w, h);
549 
550     frameBuffer = new PlatformPixelBuffer(w, h);
551     assert(frameBuffer);
552     cc->setFramebuffer(frameBuffer);
553   }
554 
555   Fl_Widget::resize(x, y, w, h);
556 }
557 
558 
handle(int event)559 int Viewport::handle(int event)
560 {
561   char *filtered;
562   int buttonMask, wheelMask;
563   DownMap::const_iterator iter;
564 
565   switch (event) {
566   case FL_PASTE:
567     filtered = convertLF(Fl::event_text(), Fl::event_length());
568 
569     vlog.debug("Sending clipboard data (%d bytes)", (int)strlen(filtered));
570 
571     try {
572       cc->sendClipboardData(filtered);
573     } catch (rdr::Exception& e) {
574       vlog.error("%s", e.str());
575       abort_connection(_("An unexpected error occurred when "
576                          "communicating with the server:\n\n%s"),
577                        e.str());
578     }
579 
580     strFree(filtered);
581 
582     return 1;
583 
584   case FL_ENTER:
585     window()->cursor(cursor, cursorHotspot.x, cursorHotspot.y);
586     // Yes, we would like some pointer events please!
587     return 1;
588 
589   case FL_LEAVE:
590     window()->cursor(FL_CURSOR_DEFAULT);
591     // We want a last move event to help trigger edge stuff
592     handlePointerEvent(Point(Fl::event_x() - x(), Fl::event_y() - y()), 0);
593     return 1;
594 
595   case FL_PUSH:
596   case FL_RELEASE:
597   case FL_DRAG:
598   case FL_MOVE:
599   case FL_MOUSEWHEEL:
600     buttonMask = 0;
601     if (Fl::event_button1())
602       buttonMask |= 1;
603     if (Fl::event_button2())
604       buttonMask |= 2;
605     if (Fl::event_button3())
606       buttonMask |= 4;
607 
608     if (event == FL_MOUSEWHEEL) {
609       wheelMask = 0;
610       if (Fl::event_dy() < 0)
611         wheelMask |= 8;
612       if (Fl::event_dy() > 0)
613         wheelMask |= 16;
614       if (Fl::event_dx() < 0)
615         wheelMask |= 32;
616       if (Fl::event_dx() > 0)
617         wheelMask |= 64;
618 
619       // A quick press of the wheel "button", followed by a immediate
620       // release below
621       handlePointerEvent(Point(Fl::event_x() - x(), Fl::event_y() - y()),
622                          buttonMask | wheelMask);
623     }
624 
625     handlePointerEvent(Point(Fl::event_x() - x(), Fl::event_y() - y()), buttonMask);
626     return 1;
627 
628   case FL_FOCUS:
629     Fl::disable_im();
630 
631     flushPendingClipboard();
632 
633     // We may have gotten our lock keys out of sync with the server
634     // whilst we didn't have focus. Try to sort this out.
635     pushLEDState();
636 
637     // Resend Ctrl/Alt if needed
638     if (menuCtrlKey)
639       handleKeyPress(0x1d, XK_Control_L);
640     if (menuAltKey)
641       handleKeyPress(0x38, XK_Alt_L);
642 
643     // Yes, we would like some focus please!
644     return 1;
645 
646   case FL_UNFOCUS:
647     // Release all keys that were pressed as that generally makes most
648     // sense (e.g. Alt+Tab where we only see the Alt press)
649     while (!downKeySym.empty())
650       handleKeyRelease(downKeySym.begin()->first);
651     Fl::enable_im();
652     return 1;
653 
654   case FL_KEYDOWN:
655   case FL_KEYUP:
656     // Just ignore these as keys were handled in the event handler
657     return 1;
658   }
659 
660   return Fl_Widget::handle(event);
661 }
662 
sendPointerEvent(const rfb::Point & pos,int buttonMask)663 void Viewport::sendPointerEvent(const rfb::Point& pos, int buttonMask)
664 {
665   if (viewOnly)
666       return;
667 
668   if ((pointerEventInterval == 0) || (buttonMask != lastButtonMask)) {
669     try {
670       cc->writer()->writePointerEvent(pos, buttonMask);
671     } catch (rdr::Exception& e) {
672       vlog.error("%s", e.str());
673       abort_connection(_("An unexpected error occurred when "
674                          "communicating with the server:\n\n%s"),
675                        e.str());
676     }
677   } else {
678     if (!Fl::has_timeout(handlePointerTimeout, this))
679       Fl::add_timeout((double)pointerEventInterval/1000.0,
680                       handlePointerTimeout, this);
681   }
682   lastPointerPos = pos;
683   lastButtonMask = buttonMask;
684 }
685 
hasFocus()686 bool Viewport::hasFocus()
687 {
688   Fl_Widget* focus;
689 
690   focus = Fl::grab();
691   if (!focus)
692     focus = Fl::focus();
693 
694   return focus == this;
695 }
696 
697 #if ! (defined(WIN32) || defined(__APPLE__))
getModifierMask(unsigned int keysym)698 unsigned int Viewport::getModifierMask(unsigned int keysym)
699 {
700   XkbDescPtr xkb;
701   unsigned int mask, keycode;
702   XkbAction *act;
703 
704   mask = 0;
705 
706   xkb = XkbGetMap(fl_display, XkbAllComponentsMask, XkbUseCoreKbd);
707   if (xkb == NULL)
708     return 0;
709 
710   for (keycode = xkb->min_key_code; keycode <= xkb->max_key_code; keycode++) {
711     unsigned int state_out;
712     KeySym ks;
713 
714     XkbTranslateKeyCode(xkb, keycode, 0, &state_out, &ks);
715     if (ks == NoSymbol)
716       continue;
717 
718     if (ks == keysym)
719       break;
720   }
721 
722   // KeySym not mapped?
723   if (keycode > xkb->max_key_code)
724     goto out;
725 
726   act = XkbKeyAction(xkb, keycode, 0);
727   if (act == NULL)
728     goto out;
729   if (act->type != XkbSA_LockMods)
730     goto out;
731 
732   if (act->mods.flags & XkbSA_UseModMapMods)
733     mask = xkb->map->modmap[keycode];
734   else
735     mask = act->mods.mask;
736 
737 out:
738   XkbFreeKeyboard(xkb, XkbAllComponentsMask, True);
739 
740   return mask;
741 }
742 #endif
743 
744 
handleClipboardChange(int source,void * data)745 void Viewport::handleClipboardChange(int source, void *data)
746 {
747   Viewport *self = (Viewport *)data;
748 
749   assert(self);
750 
751   if (!sendClipboard)
752     return;
753 
754 #if !defined(WIN32) && !defined(__APPLE__)
755   if (!sendPrimary && (source == 0))
756     return;
757 #endif
758 
759   self->clipboardSource = source;
760 
761   self->pendingServerClipboard = false;
762 
763   if (!self->hasFocus()) {
764     vlog.debug("Local clipboard changed whilst not focused, will notify server later");
765     self->pendingClientClipboard = true;
766     // Clear any older client clipboard from the server
767     self->cc->announceClipboard(false);
768     return;
769   }
770 
771   vlog.debug("Local clipboard changed, notifying server");
772   try {
773     self->cc->announceClipboard(true);
774   } catch (rdr::Exception& e) {
775     vlog.error("%s", e.str());
776     abort_connection(_("An unexpected error occurred when "
777                        "communicating with the server:\n\n%s"),
778                      e.str());
779   }
780 }
781 
782 
flushPendingClipboard()783 void Viewport::flushPendingClipboard()
784 {
785   if (pendingServerClipboard) {
786     vlog.debug("Focus regained after remote clipboard change, requesting data");
787     try {
788       cc->requestClipboard();
789     } catch (rdr::Exception& e) {
790       vlog.error("%s", e.str());
791       abort_connection(_("An unexpected error occurred when "
792                          "communicating with the server:\n\n%s"),
793                        e.str());
794     }
795   }
796   if (pendingClientClipboard) {
797     vlog.debug("Focus regained after local clipboard change, notifying server");
798     try {
799       cc->announceClipboard(true);
800     } catch (rdr::Exception& e) {
801       vlog.error("%s", e.str());
802       abort_connection(_("An unexpected error occurred when "
803                          "communicating with the server:\n\n%s"),
804                        e.str());
805     }
806   }
807 
808   pendingServerClipboard = false;
809   pendingClientClipboard = false;
810 }
811 
812 
handlePointerEvent(const rfb::Point & pos,int buttonMask)813 void Viewport::handlePointerEvent(const rfb::Point& pos, int buttonMask)
814 {
815   filterPointerEvent(pos, buttonMask);
816 }
817 
818 
handlePointerTimeout(void * data)819 void Viewport::handlePointerTimeout(void *data)
820 {
821   Viewport *self = (Viewport *)data;
822 
823   assert(self);
824 
825   try {
826     self->cc->writer()->writePointerEvent(self->lastPointerPos,
827                                           self->lastButtonMask);
828   } catch (rdr::Exception& e) {
829     vlog.error("%s", e.str());
830     abort_connection(_("An unexpected error occurred when "
831                        "communicating with the server:\n\n%s"),
832                      e.str());
833   }
834 }
835 
836 
handleKeyPress(int keyCode,rdr::U32 keySym)837 void Viewport::handleKeyPress(int keyCode, rdr::U32 keySym)
838 {
839   static bool menuRecursion = false;
840 
841   // Prevent recursion if the menu wants to send its own
842   // activation key.
843   if (menuKeySym && (keySym == menuKeySym) && !menuRecursion) {
844     menuRecursion = true;
845     popupContextMenu();
846     menuRecursion = false;
847     return;
848   }
849 
850   if (viewOnly)
851     return;
852 
853   if (keyCode == 0) {
854     vlog.error(_("No key code specified on key press"));
855     return;
856   }
857 
858 #ifdef __APPLE__
859   // Alt on OS X behaves more like AltGr on other systems, and to get
860   // sane behaviour we should translate things in that manner for the
861   // remote VNC server. However that means we lose the ability to use
862   // Alt as a shortcut modifier. Do what RealVNC does and hijack the
863   // left command key as an Alt replacement.
864   switch (keySym) {
865   case XK_Super_L:
866     keySym = XK_Alt_L;
867     break;
868   case XK_Super_R:
869     keySym = XK_Super_L;
870     break;
871   case XK_Alt_L:
872     keySym = XK_Mode_switch;
873     break;
874   case XK_Alt_R:
875     keySym = XK_ISO_Level3_Shift;
876     break;
877   }
878 #endif
879 
880   // Because of the way keyboards work, we cannot expect to have the same
881   // symbol on release as when pressed. This breaks the VNC protocol however,
882   // so we need to keep track of what keysym a key _code_ generated on press
883   // and send the same on release.
884   downKeySym[keyCode] = keySym;
885 
886 #if defined(WIN32) || defined(__APPLE__)
887   vlog.debug("Key pressed: 0x%04x => 0x%04x", keyCode, keySym);
888 #else
889   vlog.debug("Key pressed: 0x%04x => XK_%s (0x%04x)",
890              keyCode, XKeysymToString(keySym), keySym);
891 #endif
892 
893   try {
894     // Fake keycode?
895     if (keyCode > 0xff)
896       cc->writer()->writeKeyEvent(keySym, 0, true);
897     else
898       cc->writer()->writeKeyEvent(keySym, keyCode, true);
899   } catch (rdr::Exception& e) {
900     vlog.error("%s", e.str());
901     abort_connection(_("An unexpected error occurred when "
902                        "communicating with the server:\n\n%s"),
903                      e.str());
904   }
905 }
906 
907 
handleKeyRelease(int keyCode)908 void Viewport::handleKeyRelease(int keyCode)
909 {
910   DownMap::iterator iter;
911 
912   if (viewOnly)
913     return;
914 
915   iter = downKeySym.find(keyCode);
916   if (iter == downKeySym.end()) {
917     // These occur somewhat frequently so let's not spam them unless
918     // logging is turned up.
919     vlog.debug("Unexpected release of key code %d", keyCode);
920     return;
921   }
922 
923 #if defined(WIN32) || defined(__APPLE__)
924   vlog.debug("Key released: 0x%04x => 0x%04x", keyCode, iter->second);
925 #else
926   vlog.debug("Key released: 0x%04x => XK_%s (0x%04x)",
927              keyCode, XKeysymToString(iter->second), iter->second);
928 #endif
929 
930   try {
931     if (keyCode > 0xff)
932       cc->writer()->writeKeyEvent(iter->second, 0, false);
933     else
934       cc->writer()->writeKeyEvent(iter->second, keyCode, false);
935   } catch (rdr::Exception& e) {
936     vlog.error("%s", e.str());
937     abort_connection(_("An unexpected error occurred when "
938                        "communicating with the server:\n\n%s"),
939                      e.str());
940   }
941 
942   downKeySym.erase(iter);
943 }
944 
945 
handleSystemEvent(void * event,void * data)946 int Viewport::handleSystemEvent(void *event, void *data)
947 {
948   Viewport *self = (Viewport *)data;
949 
950   assert(self);
951 
952   if (!self->hasFocus())
953     return 0;
954 
955   assert(event);
956 
957 #if defined(WIN32)
958   MSG *msg = (MSG*)event;
959 
960   if ((msg->message == WM_MOUSEMOVE) ||
961       (msg->message == WM_LBUTTONDOWN) ||
962       (msg->message == WM_LBUTTONUP) ||
963       (msg->message == WM_RBUTTONDOWN) ||
964       (msg->message == WM_RBUTTONUP) ||
965       (msg->message == WM_MBUTTONDOWN) ||
966       (msg->message == WM_MBUTTONUP) ||
967       (msg->message == WM_MOUSEWHEEL) ||
968       (msg->message == WM_MOUSEHWHEEL)) {
969     // We can't get a mouse event in the middle of an AltGr sequence, so
970     // abort that detection
971     if (self->altGrArmed)
972       self->resolveAltGrDetection(false);
973 
974     return 0; // We didn't really consume the mouse event
975   } else if ((msg->message == WM_KEYDOWN) || (msg->message == WM_SYSKEYDOWN)) {
976     UINT vKey;
977     bool isExtended;
978     int keyCode;
979     rdr::U32 keySym;
980 
981     vKey = msg->wParam;
982     isExtended = (msg->lParam & (1 << 24)) != 0;
983 
984     keyCode = ((msg->lParam >> 16) & 0xff);
985 
986     // Windows' touch keyboard doesn't set a scan code for the Alt
987     // portion of the AltGr sequence, so we need to help it out
988     if (!isExtended && (keyCode == 0x00) && (vKey == VK_MENU)) {
989       isExtended = true;
990       keyCode = 0x38;
991     }
992 
993     // Windows doesn't have a proper AltGr, but handles it using fake
994     // Ctrl+Alt. However the remote end might not be Windows, so we need
995     // to merge those in to a single AltGr event. We detect this case
996     // by seeing the two key events directly after each other with a very
997     // short time between them (<50ms) and supress the Ctrl event.
998     if (self->altGrArmed) {
999       bool altPressed = isExtended &&
1000                         (keyCode == 0x38) &&
1001                         (vKey == VK_MENU) &&
1002                         ((msg->time - self->altGrCtrlTime) < 50);
1003       self->resolveAltGrDetection(altPressed);
1004     }
1005 
1006     if (keyCode == SCAN_FAKE) {
1007       vlog.debug("Ignoring fake key press (virtual key 0x%02x)", vKey);
1008       return 1;
1009     }
1010 
1011     // Windows sets the scan code to 0x00 for multimedia keys, so we
1012     // have to do a reverse lookup based on the vKey.
1013     if (keyCode == 0x00) {
1014       keyCode = MapVirtualKey(vKey, MAPVK_VK_TO_VSC);
1015       if (keyCode == 0x00) {
1016         if (isExtended)
1017           vlog.error(_("No scan code for extended virtual key 0x%02x"), (int)vKey);
1018         else
1019           vlog.error(_("No scan code for virtual key 0x%02x"), (int)vKey);
1020         return 1;
1021       }
1022     }
1023 
1024     if (keyCode & ~0x7f) {
1025       vlog.error(_("Invalid scan code 0x%02x"), (int)keyCode);
1026       return 1;
1027     }
1028 
1029     if (isExtended)
1030       keyCode |= 0x80;
1031 
1032 
1033     // Fortunately RFB and Windows use the same scan code set (mostly),
1034     // so there is no conversion needed
1035     // (as long as we encode the extended keys with the high bit)
1036 
1037     // However Pause sends a code that conflicts with NumLock, so use
1038     // the code most RFB implementations use (part of the sequence for
1039     // Ctrl+Pause, i.e. Break)
1040     if (keyCode == 0x45)
1041       keyCode = 0xc6;
1042 
1043     // And NumLock incorrectly has the extended bit set
1044     if (keyCode == 0xc5)
1045       keyCode = 0x45;
1046 
1047     // And Alt+PrintScreen (i.e. SysRq) sends a different code than
1048     // PrintScreen
1049     if (keyCode == 0xb7)
1050       keyCode = 0x54;
1051 
1052     keySym = win32_vkey_to_keysym(vKey, isExtended);
1053     if (keySym == NoSymbol) {
1054       if (isExtended)
1055         vlog.error(_("No symbol for extended virtual key 0x%02x"), (int)vKey);
1056       else
1057         vlog.error(_("No symbol for virtual key 0x%02x"), (int)vKey);
1058     }
1059 
1060     // Windows sends the same vKey for both shifts, so we need to look
1061     // at the scan code to tell them apart
1062     if ((keySym == XK_Shift_L) && (keyCode == 0x36))
1063       keySym = XK_Shift_R;
1064 
1065     // AltGr handling (see above)
1066     if (win32_has_altgr()) {
1067       if ((keyCode == 0xb8) && (keySym == XK_Alt_R))
1068         keySym = XK_ISO_Level3_Shift;
1069 
1070       // Possible start of AltGr sequence?
1071       if ((keyCode == 0x1d) && (keySym == XK_Control_L)) {
1072         self->altGrArmed = true;
1073         self->altGrCtrlTime = msg->time;
1074         Fl::add_timeout(0.1, handleAltGrTimeout, self);
1075         return 1;
1076       }
1077     }
1078 
1079     self->handleKeyPress(keyCode, keySym);
1080 
1081     // We don't get reliable WM_KEYUP for these
1082     switch (keySym) {
1083     case XK_Zenkaku_Hankaku:
1084     case XK_Eisu_toggle:
1085     case XK_Katakana:
1086     case XK_Hiragana:
1087     case XK_Romaji:
1088       self->handleKeyRelease(keyCode);
1089     }
1090 
1091     return 1;
1092   } else if ((msg->message == WM_KEYUP) || (msg->message == WM_SYSKEYUP)) {
1093     UINT vKey;
1094     bool isExtended;
1095     int keyCode;
1096 
1097     vKey = msg->wParam;
1098     isExtended = (msg->lParam & (1 << 24)) != 0;
1099 
1100     keyCode = ((msg->lParam >> 16) & 0xff);
1101 
1102     // Touch keyboard AltGr (see above)
1103     if (!isExtended && (keyCode == 0x00) && (vKey == VK_MENU)) {
1104       isExtended = true;
1105       keyCode = 0x38;
1106     }
1107 
1108     // We can't get a release in the middle of an AltGr sequence, so
1109     // abort that detection
1110     if (self->altGrArmed)
1111       self->resolveAltGrDetection(false);
1112 
1113     if (keyCode == SCAN_FAKE) {
1114       vlog.debug("Ignoring fake key release (virtual key 0x%02x)", vKey);
1115       return 1;
1116     }
1117 
1118     if (keyCode == 0x00)
1119       keyCode = MapVirtualKey(vKey, MAPVK_VK_TO_VSC);
1120     if (isExtended)
1121       keyCode |= 0x80;
1122     if (keyCode == 0x45)
1123       keyCode = 0xc6;
1124     if (keyCode == 0xc5)
1125       keyCode = 0x45;
1126     if (keyCode == 0xb7)
1127       keyCode = 0x54;
1128 
1129     self->handleKeyRelease(keyCode);
1130 
1131     // Windows has a rather nasty bug where it won't send key release
1132     // events for a Shift button if the other Shift is still pressed
1133     if ((keyCode == 0x2a) || (keyCode == 0x36)) {
1134       if (self->downKeySym.count(0x2a))
1135         self->handleKeyRelease(0x2a);
1136       if (self->downKeySym.count(0x36))
1137         self->handleKeyRelease(0x36);
1138     }
1139 
1140     return 1;
1141   }
1142 #elif defined(__APPLE__)
1143   if (cocoa_is_keyboard_event(event)) {
1144     int keyCode;
1145 
1146     keyCode = cocoa_event_keycode(event);
1147     if ((unsigned)keyCode >= code_map_osx_to_qnum_len)
1148       keyCode = 0;
1149     else
1150       keyCode = code_map_osx_to_qnum[keyCode];
1151 
1152     if (cocoa_is_key_press(event)) {
1153       rdr::U32 keySym;
1154 
1155       keySym = cocoa_event_keysym(event);
1156       if (keySym == NoSymbol) {
1157         vlog.error(_("No symbol for key code 0x%02x (in the current state)"),
1158                    (int)keyCode);
1159       }
1160 
1161       self->handleKeyPress(keyCode, keySym);
1162 
1163       // We don't get any release events for CapsLock, so we have to
1164       // send the release right away.
1165       if (keySym == XK_Caps_Lock)
1166         self->handleKeyRelease(keyCode);
1167     } else {
1168       self->handleKeyRelease(keyCode);
1169     }
1170 
1171     return 1;
1172   }
1173 #else
1174   XEvent *xevent = (XEvent*)event;
1175 
1176   if (xevent->type == KeyPress) {
1177     int keycode;
1178     char str;
1179     KeySym keysym;
1180 
1181     keycode = code_map_keycode_to_qnum[xevent->xkey.keycode];
1182 
1183     // Generate a fake keycode just for tracking if we can't figure
1184     // out the proper one
1185     if (keycode == 0)
1186         keycode = 0x100 | xevent->xkey.keycode;
1187 
1188     XLookupString(&xevent->xkey, &str, 1, &keysym, NULL);
1189     if (keysym == NoSymbol) {
1190       vlog.error(_("No symbol for key code %d (in the current state)"),
1191                  (int)xevent->xkey.keycode);
1192     }
1193 
1194     switch (keysym) {
1195     // For the first few years, there wasn't a good consensus on what the
1196     // Windows keys should be mapped to for X11. So we need to help out a
1197     // bit and map all variants to the same key...
1198     case XK_Hyper_L:
1199       keysym = XK_Super_L;
1200       break;
1201     case XK_Hyper_R:
1202       keysym = XK_Super_R;
1203       break;
1204     // There has been several variants for Shift-Tab over the years.
1205     // RFB states that we should always send a normal tab.
1206     case XK_ISO_Left_Tab:
1207       keysym = XK_Tab;
1208       break;
1209     }
1210 
1211     self->handleKeyPress(keycode, keysym);
1212     return 1;
1213   } else if (xevent->type == KeyRelease) {
1214     int keycode = code_map_keycode_to_qnum[xevent->xkey.keycode];
1215     if (keycode == 0)
1216         keycode = 0x100 | xevent->xkey.keycode;
1217     self->handleKeyRelease(keycode);
1218     return 1;
1219   }
1220 #endif
1221 
1222   return 0;
1223 }
1224 
1225 #ifdef WIN32
handleAltGrTimeout(void * data)1226 void Viewport::handleAltGrTimeout(void *data)
1227 {
1228   Viewport *self = (Viewport *)data;
1229 
1230   assert(self);
1231 
1232   self->altGrArmed = false;
1233   self->handleKeyPress(0x1d, XK_Control_L);
1234 }
1235 
resolveAltGrDetection(bool isAltGrSequence)1236 void Viewport::resolveAltGrDetection(bool isAltGrSequence)
1237 {
1238   altGrArmed = false;
1239   Fl::remove_timeout(handleAltGrTimeout);
1240   // when it's not an AltGr sequence we can't supress the Ctrl anymore
1241   if (!isAltGrSequence)
1242     handleKeyPress(0x1d, XK_Control_L);
1243 }
1244 #endif
1245 
initContextMenu()1246 void Viewport::initContextMenu()
1247 {
1248   contextMenu->clear();
1249 
1250   fltk_menu_add(contextMenu, p_("ContextMenu|", "Dis&connect"),
1251                 0, NULL, (void*)ID_DISCONNECT, FL_MENU_DIVIDER);
1252 
1253   fltk_menu_add(contextMenu, p_("ContextMenu|", "&Full screen"),
1254                 0, NULL, (void*)ID_FULLSCREEN,
1255                 FL_MENU_TOGGLE | (window()->fullscreen_active()?FL_MENU_VALUE:0));
1256   fltk_menu_add(contextMenu, p_("ContextMenu|", "Minimi&ze"),
1257                 0, NULL, (void*)ID_MINIMIZE, 0);
1258   fltk_menu_add(contextMenu, p_("ContextMenu|", "Resize &window to session"),
1259                 0, NULL, (void*)ID_RESIZE,
1260                 (window()->fullscreen_active()?FL_MENU_INACTIVE:0) |
1261                 FL_MENU_DIVIDER);
1262 
1263   fltk_menu_add(contextMenu, p_("ContextMenu|", "&Ctrl"),
1264                 0, NULL, (void*)ID_CTRL,
1265                 FL_MENU_TOGGLE | (menuCtrlKey?FL_MENU_VALUE:0));
1266   fltk_menu_add(contextMenu, p_("ContextMenu|", "&Alt"),
1267                 0, NULL, (void*)ID_ALT,
1268                 FL_MENU_TOGGLE | (menuAltKey?FL_MENU_VALUE:0));
1269 
1270   if (menuKeySym) {
1271     char sendMenuKey[64];
1272     snprintf(sendMenuKey, 64, p_("ContextMenu|", "Send %s"), (const char *)menuKey);
1273     fltk_menu_add(contextMenu, sendMenuKey, 0, NULL, (void*)ID_MENUKEY, 0);
1274     fltk_menu_add(contextMenu, "Secret shortcut menu key", menuKeyFLTK, NULL,
1275                   (void*)ID_MENUKEY, FL_MENU_INVISIBLE);
1276   }
1277 
1278   fltk_menu_add(contextMenu, p_("ContextMenu|", "Send Ctrl-Alt-&Del"),
1279                 0, NULL, (void*)ID_CTRLALTDEL, FL_MENU_DIVIDER);
1280 
1281   fltk_menu_add(contextMenu, p_("ContextMenu|", "&Refresh screen"),
1282                 0, NULL, (void*)ID_REFRESH, FL_MENU_DIVIDER);
1283 
1284   fltk_menu_add(contextMenu, p_("ContextMenu|", "&Options..."),
1285                 0, NULL, (void*)ID_OPTIONS, 0);
1286   fltk_menu_add(contextMenu, p_("ContextMenu|", "Connection &info..."),
1287                 0, NULL, (void*)ID_INFO, 0);
1288   fltk_menu_add(contextMenu, p_("ContextMenu|", "About &TigerVNC viewer..."),
1289                 0, NULL, (void*)ID_ABOUT, 0);
1290 }
1291 
1292 
popupContextMenu()1293 void Viewport::popupContextMenu()
1294 {
1295   const Fl_Menu_Item *m;
1296   char buffer[1024];
1297 
1298   // Make sure the menu is reset to its initial state between goes or
1299   // it will start up highlighting the previously selected entry.
1300   contextMenu->value(-1);
1301 
1302   // initialize context menu before display
1303   initContextMenu();
1304 
1305   // Unfortunately FLTK doesn't reliably restore the mouse pointer for
1306   // menus, so we have to help it out.
1307   if (Fl::belowmouse() == this)
1308     window()->cursor(FL_CURSOR_DEFAULT);
1309 
1310   // FLTK also doesn't switch focus properly for menus
1311   handle(FL_UNFOCUS);
1312 
1313   m = contextMenu->popup();
1314 
1315   handle(FL_FOCUS);
1316 
1317   // Back to our proper mouse pointer.
1318   if (Fl::belowmouse())
1319     window()->cursor(cursor, cursorHotspot.x, cursorHotspot.y);
1320 
1321   if (m == NULL)
1322     return;
1323 
1324   switch (m->argument()) {
1325   case ID_DISCONNECT:
1326     disconnect();
1327     break;
1328   case ID_FULLSCREEN:
1329     if (window()->fullscreen_active())
1330       window()->fullscreen_off();
1331     else
1332       ((DesktopWindow*)window())->fullscreen_on();
1333     break;
1334   case ID_MINIMIZE:
1335     window()->iconize();
1336     break;
1337   case ID_RESIZE:
1338     if (window()->fullscreen_active())
1339       break;
1340     window()->size(w(), h());
1341     break;
1342   case ID_CTRL:
1343     if (m->value())
1344       handleKeyPress(0x1d, XK_Control_L);
1345     else
1346       handleKeyRelease(0x1d);
1347     menuCtrlKey = !menuCtrlKey;
1348     break;
1349   case ID_ALT:
1350     if (m->value())
1351       handleKeyPress(0x38, XK_Alt_L);
1352     else
1353       handleKeyRelease(0x38);
1354     menuAltKey = !menuAltKey;
1355     break;
1356   case ID_MENUKEY:
1357     handleKeyPress(menuKeyCode, menuKeySym);
1358     handleKeyRelease(menuKeyCode);
1359     break;
1360   case ID_CTRLALTDEL:
1361     handleKeyPress(0x1d, XK_Control_L);
1362     handleKeyPress(0x38, XK_Alt_L);
1363     handleKeyPress(0xd3, XK_Delete);
1364 
1365     handleKeyRelease(0xd3);
1366     handleKeyRelease(0x38);
1367     handleKeyRelease(0x1d);
1368     break;
1369   case ID_REFRESH:
1370     cc->refreshFramebuffer();
1371     break;
1372   case ID_OPTIONS:
1373     OptionsDialog::showDialog();
1374     break;
1375   case ID_INFO:
1376     if (fltk_escape(cc->connectionInfo(), buffer, sizeof(buffer)) < sizeof(buffer)) {
1377       fl_message_title(_("VNC connection info"));
1378       fl_message("%s", buffer);
1379     }
1380     break;
1381   case ID_ABOUT:
1382     about_vncviewer();
1383     break;
1384   }
1385 }
1386 
1387 
setMenuKey()1388 void Viewport::setMenuKey()
1389 {
1390   getMenuKey(&menuKeyFLTK, &menuKeyCode, &menuKeySym);
1391 }
1392 
1393 
handleOptions(void * data)1394 void Viewport::handleOptions(void *data)
1395 {
1396   Viewport *self = (Viewport*)data;
1397 
1398   self->setMenuKey();
1399   // FIXME: Need to recheck cursor for dotWhenNoCursor
1400 }
1401