Add client support for LED state sync
diff --git a/vncviewer/CConn.cxx b/vncviewer/CConn.cxx
index 2e97ec2..07e7841 100644
--- a/vncviewer/CConn.cxx
+++ b/vncviewer/CConn.cxx
@@ -92,6 +92,8 @@
   cp.supportsExtendedDesktopSize = true;
   cp.supportsDesktopRename = true;
 
+  cp.supportsLEDState = true;
+
   if (customCompressLevel)
     cp.compressLevel = compressLevel;
   else
@@ -503,6 +505,13 @@
   }
 }
 
+void CConn::setLEDState(unsigned int state)
+{
+  CConnection::setLEDState(state);
+
+  desktop->setLEDState(state);
+}
+
 
 ////////////////////// Internal methods //////////////////////
 
diff --git a/vncviewer/CConn.h b/vncviewer/CConn.h
index 93cc278..426bd1e 100644
--- a/vncviewer/CConn.h
+++ b/vncviewer/CConn.h
@@ -74,6 +74,8 @@
 
   void fence(rdr::U32 flags, unsigned len, const char data[]);
 
+  void setLEDState(unsigned int state);
+
 private:
 
   void resizeFramebuffer();
diff --git a/vncviewer/CMakeLists.txt b/vncviewer/CMakeLists.txt
index 2aecda2..56313e9 100644
--- a/vncviewer/CMakeLists.txt
+++ b/vncviewer/CMakeLists.txt
@@ -53,7 +53,9 @@
 endif()
 
 if(APPLE)
-  target_link_libraries(vncviewer "-framework Cocoa" "-framework Carbon")
+  target_link_libraries(vncviewer "-framework Cocoa")
+  target_link_libraries(vncviewer "-framework Carbon")
+  target_link_libraries(vncviewer "-framework IOKit")
 endif()
 
 install(TARGETS vncviewer DESTINATION ${BIN_DIR})
diff --git a/vncviewer/DesktopWindow.cxx b/vncviewer/DesktopWindow.cxx
index 408efd1..3973cd6 100644
--- a/vncviewer/DesktopWindow.cxx
+++ b/vncviewer/DesktopWindow.cxx
@@ -395,6 +395,12 @@
 }
 
 
+void DesktopWindow::setLEDState(unsigned int state)
+{
+  viewport->setLEDState(state);
+}
+
+
 void DesktopWindow::resize(int x, int y, int w, int h)
 {
   bool resizing;
diff --git a/vncviewer/DesktopWindow.h b/vncviewer/DesktopWindow.h
index 4224699..f1bf312 100644
--- a/vncviewer/DesktopWindow.h
+++ b/vncviewer/DesktopWindow.h
@@ -66,6 +66,9 @@
   void setCursor(int width, int height, const rfb::Point& hotspot,
                  const rdr::U8* data);
 
+  // Change client LED state
+  void setLEDState(unsigned int state);
+
   // Fl_Window callback methods
   void draw();
   void resize(int x, int y, int w, int h);
diff --git a/vncviewer/Viewport.cxx b/vncviewer/Viewport.cxx
index 6a23526..c0bbd0d 100644
--- a/vncviewer/Viewport.cxx
+++ b/vncviewer/Viewport.cxx
@@ -28,6 +28,7 @@
 #include <rfb/CMsgWriter.h>
 #include <rfb/LogWriter.h>
 #include <rfb/Exception.h>
+#include <rfb/ledStates.h>
 
 // FLTK can pull in the X11 headers on some systems
 #ifndef XK_VoidSymbol
@@ -41,6 +42,10 @@
 #include <rfb/XF86keysym.h>
 #endif
 
+#if ! (defined(WIN32) || defined(__APPLE__))
+#include <X11/XKBlib.h>
+#endif
+
 #ifndef NoSymbol
 #define NoSymbol 0
 #endif
@@ -92,6 +97,11 @@
 // Fake key presses use this value and above
 static const int fakeKeyBase = 0x200;
 
+// Used to detect fake input (0xaa is not a real key)
+#ifdef WIN32
+static const WORD SCAN_FAKE = 0xaa;
+#endif
+
 Viewport::Viewport(int w, int h, const rfb::PixelFormat& serverPF, CConn* cc_)
   : Fl_Widget(0, 0, w, h), cc(cc_), frameBuffer(NULL),
     lastPointerPos(0, 0), lastButtonMask(0),
@@ -218,6 +228,108 @@
 }
 
 
+void Viewport::setLEDState(unsigned int state)
+{
+  Fl_Widget *focus;
+
+  vlog.debug("Got server LED state: 0x%08x", state);
+
+  focus = Fl::grab();
+  if (!focus)
+    focus = Fl::focus();
+  if (!focus)
+    return;
+
+  if (focus != this)
+    return;
+
+#if defined(WIN32)
+  INPUT input[6];
+  UINT count;
+  UINT ret;
+
+  memset(input, 0, sizeof(input));
+  count = 0;
+
+  if (!!(state & ledCapsLock) != !!(GetKeyState(VK_CAPITAL) & 0x1)) {
+    input[count].type = input[count+1].type = INPUT_KEYBOARD;
+    input[count].ki.wVk = input[count+1].ki.wVk = VK_CAPITAL;
+    input[count].ki.wScan = input[count+1].ki.wScan = SCAN_FAKE;
+    input[count].ki.dwFlags = 0;
+    input[count+1].ki.dwFlags = KEYEVENTF_KEYUP;
+    count += 2;
+  }
+
+  if (!!(state & ledNumLock) != !!(GetKeyState(VK_NUMLOCK) & 0x1)) {
+    input[count].type = input[count+1].type = INPUT_KEYBOARD;
+    input[count].ki.wVk = input[count+1].ki.wVk = VK_NUMLOCK;
+    input[count].ki.wScan = input[count+1].ki.wScan = SCAN_FAKE;
+    input[count].ki.dwFlags = KEYEVENTF_EXTENDEDKEY;
+    input[count+1].ki.dwFlags = KEYEVENTF_KEYUP | KEYEVENTF_EXTENDEDKEY;
+    count += 2;
+  }
+
+  if (!!(state & ledScrollLock) != !!(GetKeyState(VK_SCROLL) & 0x1)) {
+    input[count].type = input[count+1].type = INPUT_KEYBOARD;
+    input[count].ki.wVk = input[count+1].ki.wVk = VK_SCROLL;
+    input[count].ki.wScan = input[count+1].ki.wScan = SCAN_FAKE;
+    input[count].ki.dwFlags = 0;
+    input[count+1].ki.dwFlags = KEYEVENTF_KEYUP;
+    count += 2;
+  }
+
+  if (count == 0)
+    return;
+
+  ret = SendInput(count, input, sizeof(*input));
+  if (ret < count)
+    vlog.error(_("Failed to update keyboard LED state: %lu"), GetLastError());
+#elif defined(__APPLE__)
+  int ret;
+
+  ret = cocoa_set_caps_lock_state(state & ledCapsLock);
+  if (ret != 0) {
+    vlog.error(_("Failed to update keyboard LED state: %d"), ret);
+    return;
+  }
+
+  ret = cocoa_set_num_lock_state(state & ledNumLock);
+  if (ret != 0) {
+    vlog.error(_("Failed to update keyboard LED state: %d"), ret);
+    return;
+  }
+
+  // No support for Scroll Lock //
+
+#else
+  unsigned int affect, values;
+  unsigned int mask;
+
+  Bool ret;
+
+  affect = values = 0;
+
+  affect |= LockMask;
+  if (state & ledCapsLock)
+    values |= LockMask;
+
+  mask = getModifierMask(XK_Num_Lock);
+  affect |= mask;
+  if (state & ledNumLock)
+    values |= mask;
+
+  mask = getModifierMask(XK_Scroll_Lock);
+  affect |= mask;
+  if (state & ledScrollLock)
+    values |= mask;
+
+  ret = XkbLockModifiers(fl_display, XkbUseCoreKbd, affect, values);
+  if (!ret)
+    vlog.error(_("Failed to update keyboard LED state"));
+#endif
+}
+
+
 void Viewport::draw(Surface* dst)
 {
   int X, Y, W, H;
@@ -352,6 +464,55 @@
   return Fl_Widget::handle(event);
 }
 
+
+#if ! (defined(WIN32) || defined(__APPLE__))
+unsigned int Viewport::getModifierMask(unsigned int keysym)
+{
+  XkbDescPtr xkb;
+  unsigned int mask, keycode;
+  XkbAction *act;
+
+  mask = 0;
+
+  xkb = XkbGetMap(fl_display, XkbAllComponentsMask, XkbUseCoreKbd);
+  if (xkb == NULL)
+    return 0;
+
+  for (keycode = xkb->min_key_code; keycode <= xkb->max_key_code; keycode++) {
+    unsigned int state_out;
+    KeySym ks;
+
+    XkbTranslateKeyCode(xkb, keycode, 0, &state_out, &ks);
+    if (ks == NoSymbol)
+      continue;
+
+    if (ks == keysym)
+      break;
+  }
+
+  // KeySym not mapped?
+  if (keycode > xkb->max_key_code)
+    goto out;
+
+  act = XkbKeyAction(xkb, keycode, 0);
+  if (act == NULL)
+    goto out;
+  if (act->type != XkbSA_LockMods)
+    goto out;
+
+  if (act->mods.flags & XkbSA_UseModMapMods)
+    mask = xkb->map->modmap[keycode];
+  else
+    mask = act->mods.mask;
+
+out:
+  XkbFreeKeyboard(xkb, XkbAllComponentsMask, True);
+
+  return mask;
+}
+#endif
+
+
 void Viewport::handleClipboardChange(int source, void *data)
 {
   Viewport *self = (Viewport *)data;
@@ -577,6 +738,11 @@
 
     keyCode = ((msg->lParam >> 16) & 0xff);
 
+    if (keyCode == SCAN_FAKE) {
+      vlog.debug("Ignoring fake key press (virtual key 0x%02x)", vKey);
+      return 1;
+    }
+
     // Windows sets the scan code to 0x00 for multimedia keys, so we
     // have to do a reverse lookup based on the vKey.
     if (keyCode == 0x00) {
@@ -620,6 +786,12 @@
     isExtended = (msg->lParam & (1 << 24)) != 0;
 
     keyCode = ((msg->lParam >> 16) & 0xff);
+
+    if (keyCode == SCAN_FAKE) {
+      vlog.debug("Ignoring fake key release (virtual key 0x%02x)", vKey);
+      return 1;
+    }
+
     if (keyCode == 0x00)
       keyCode = MapVirtualKey(vKey, MAPVK_VK_TO_VSC);
     if (isExtended)
diff --git a/vncviewer/Viewport.h b/vncviewer/Viewport.h
index 6f0710d..652feb4 100644
--- a/vncviewer/Viewport.h
+++ b/vncviewer/Viewport.h
@@ -47,6 +47,9 @@
   void setCursor(int width, int height, const rfb::Point& hotspot,
                  const rdr::U8* data);
 
+  // Change client LED state
+  void setLEDState(unsigned int state);
+
   void draw(Surface* dst);
 
   // Fl_Widget callback methods
@@ -59,6 +62,8 @@
 
 private:
 
+  unsigned int getModifierMask(unsigned int keysym);
+
   static void handleClipboardChange(int source, void *data);
 
   void handlePointerEvent(const rfb::Point& pos, int buttonMask);
diff --git a/vncviewer/cocoa.h b/vncviewer/cocoa.h
index 0c3ac82..b5d5f76 100644
--- a/vncviewer/cocoa.h
+++ b/vncviewer/cocoa.h
@@ -33,4 +33,7 @@
 int cocoa_event_keycode(const void *event);
 int cocoa_event_keysym(const void *event);
 
+int cocoa_set_caps_lock_state(bool on);
+int cocoa_set_num_lock_state(bool on);
+
 #endif
diff --git a/vncviewer/cocoa.mm b/vncviewer/cocoa.mm
index 6e464fa..6483291 100644
--- a/vncviewer/cocoa.mm
+++ b/vncviewer/cocoa.mm
@@ -27,6 +27,9 @@
 #import <Cocoa/Cocoa.h>
 #import <Carbon/Carbon.h>
 
+#include <IOKit/hidsystem/IOHIDLib.h>
+#include <IOKit/hidsystem/IOHIDParameter.h>
+
 #define XK_LATIN1
 #define XK_MISCELLANY
 #define XK_XKB_KEYS
@@ -406,3 +409,50 @@
 
   return ucs2keysym([chars characterAtIndex:0]);
 }
+
+static int cocoa_open_hid(io_connect_t *ioc)
+{
+  kern_return_t ret;
+  io_service_t ios;
+  CFMutableDictionaryRef mdict;
+
+  mdict = IOServiceMatching(kIOHIDSystemClass);
+  ios = IOServiceGetMatchingService(kIOMasterPortDefault,
+                                    (CFDictionaryRef) mdict);
+  if (!ios)
+    return KERN_FAILURE;
+
+  ret = IOServiceOpen(ios, mach_task_self(), kIOHIDParamConnectType, ioc);
+  IOObjectRelease(ios);
+  if (ret != KERN_SUCCESS)
+    return ret;
+
+  return KERN_SUCCESS;
+}
+
+static int cocoa_set_modifier_lock_state(int modifier, bool on)
+{
+  kern_return_t ret;
+  io_connect_t ioc;
+
+  ret = cocoa_open_hid(&ioc);
+  if (ret != KERN_SUCCESS)
+    return ret;
+
+  ret = IOHIDSetModifierLockState(ioc, modifier, on);
+  IOServiceClose(ioc);
+  if (ret != KERN_SUCCESS)
+    return ret;
+
+  return KERN_SUCCESS;
+}
+
+int cocoa_set_caps_lock_state(bool on)
+{
+  return cocoa_set_modifier_lock_state(kIOHIDCapsLockState, on);
+}
+
+int cocoa_set_num_lock_state(bool on)
+{
+  return cocoa_set_modifier_lock_state(kIOHIDNumLockState, on);
+}