Merge branch 'fps' of https://github.com/CendioOssman/tigervnc
diff --git a/BUILDING.txt b/BUILDING.txt
index 9987bd7..9449810 100644
--- a/BUILDING.txt
+++ b/BUILDING.txt
@@ -503,7 +503,7 @@
 and releases can be found in the "contrib/rpm/el{5,6}" directories
 of the TigerVNC subversion trunk.  All external source tarballs
 must be fetched manually and placed into the 'SOURCES' directory
-under the rpmbuild root.  Additonally, the following macros need
+under the rpmbuild root.  Additionally, the following macros need
 to be defined:
 
   EL6:
diff --git a/cmake/Modules/CMakeMacroLibtoolFile.cmake b/cmake/Modules/CMakeMacroLibtoolFile.cmake
index a54d994..c882c4a 100644
--- a/cmake/Modules/CMakeMacroLibtoolFile.cmake
+++ b/cmake/Modules/CMakeMacroLibtoolFile.cmake
@@ -89,7 +89,7 @@
       endif()
     else()
       # Detected a static library.  Check whether the library pathname is
-      # absolute and, if not, use find_library() to get the abolute path.
+      # absolute and, if not, use find_library() to get the absolute path.
       get_filename_component(_name ${library} NAME)
       string(REPLACE "${_name}" "" _path ${library})
       if(NOT "${_path}" STREQUAL "")
@@ -132,6 +132,10 @@
   file(APPEND ${_laname} "dlpreopen=''\n\n")
   file(APPEND ${_laname} "libdir='/usr/lib'\n\n")
 
+  # Make sure the timestamp is updated to trigger other make invocations
+  add_custom_command(TARGET ${_target} POST_BUILD COMMAND
+    "${CMAKE_COMMAND}" -E touch "${_laname}")
+
 
   # Add custom command to symlink the static library so that autotools finds
   # the library in .libs.  These are executed after the specified target build.
diff --git a/common/Xregion/Region.c b/common/Xregion/Region.c
index 5209443..1acf581 100644
--- a/common/Xregion/Region.c
+++ b/common/Xregion/Region.c
@@ -1310,7 +1310,7 @@
 	else if (r2->x1 <= x1)
 	{
 	    /*
-	     * Subtrahend preceeds minuend: nuke left edge of minuend.
+	     * Subtrahend preceds minuend: nuke left edge of minuend.
 	     */
 	    x1 = r2->x2;
 	    if (x1 >= r1->x2)
diff --git a/common/rfb/CConnection.cxx b/common/rfb/CConnection.cxx
index 35be946..2020418 100644
--- a/common/rfb/CConnection.cxx
+++ b/common/rfb/CConnection.cxx
@@ -1,4 +1,5 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
+ * Copyright 2011-2017 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
@@ -320,6 +321,13 @@
   CMsgHandler::setExtendedDesktopSize(reason, result, w, h, layout);
 }
 
+void CConnection::readAndDecodeRect(const Rect& r, int encoding,
+                                    ModifiablePixelBuffer* pb)
+{
+  decoder.decodeRect(r, encoding, pb);
+  decoder.flush();
+}
+
 void CConnection::framebufferUpdateStart()
 {
   CMsgHandler::framebufferUpdateStart();
diff --git a/common/rfb/CConnection.h b/common/rfb/CConnection.h
index 6bc7a38..799a9c2 100644
--- a/common/rfb/CConnection.h
+++ b/common/rfb/CConnection.h
@@ -1,4 +1,5 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
+ * Copyright 2011-2017 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
@@ -99,6 +100,9 @@
                                         int w, int h,
                                         const ScreenSet& layout);
 
+    virtual void readAndDecodeRect(const Rect& r, int encoding,
+                                   ModifiablePixelBuffer* pb);
+
     virtual void framebufferUpdateStart();
     virtual void framebufferUpdateEnd();
     virtual void dataRect(const Rect& r, int encoding);
diff --git a/common/rfb/CMsgHandler.h b/common/rfb/CMsgHandler.h
index 7d2cdc2..993276e 100644
--- a/common/rfb/CMsgHandler.h
+++ b/common/rfb/CMsgHandler.h
@@ -50,13 +50,16 @@
                                         int w, int h,
                                         const ScreenSet& layout);
     virtual void setCursor(int width, int height, const Point& hotspot,
-                           void* data, void* mask) = 0;
+                           const rdr::U8* data) = 0;
     virtual void setPixelFormat(const PixelFormat& pf);
     virtual void setName(const char* name);
     virtual void fence(rdr::U32 flags, unsigned len, const char data[]);
     virtual void endOfContinuousUpdates();
     virtual void serverInit() = 0;
 
+    virtual void readAndDecodeRect(const Rect& r, int encoding,
+                                   ModifiablePixelBuffer* pb) = 0;
+
     virtual void framebufferUpdateStart();
     virtual void framebufferUpdateEnd();
     virtual void dataRect(const Rect& r, int encoding) = 0;
diff --git a/common/rfb/CMsgReader.cxx b/common/rfb/CMsgReader.cxx
index 96ddf44..7233fbd 100644
--- a/common/rfb/CMsgReader.cxx
+++ b/common/rfb/CMsgReader.cxx
@@ -1,5 +1,5 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright 2009-2014 Pierre Ossman for Cendio AB
+ * Copyright 2009-2017 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
@@ -16,7 +16,10 @@
  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
  * USA.
  */
+
+#include <assert.h>
 #include <stdio.h>
+
 #include <rfb/msgTypes.h>
 #include <rdr/InStream.h>
 #include <rfb/Exception.h>
@@ -88,9 +91,15 @@
     case pseudoEncodingLastRect:
       nUpdateRectsLeft = 1;     // this rectangle is the last one
       break;
+    case pseudoEncodingXCursor:
+      readSetXCursor(w, h, Point(x,y));
+      break;
     case pseudoEncodingCursor:
       readSetCursor(w, h, Point(x,y));
       break;
+    case pseudoEncodingCursorWithAlpha:
+      readSetCursorWithAlpha(w, h, Point(x,y));
+      break;
     case pseudoEncodingDesktopName:
       readSetDesktopName(x, y, w, h);
       break;
@@ -191,6 +200,61 @@
   handler->dataRect(r, encoding);
 }
 
+void CMsgReader::readSetXCursor(int width, int height, const Point& hotspot)
+{
+  rdr::U8 pr, pg, pb;
+  rdr::U8 sr, sg, sb;
+  int data_len = ((width+7)/8) * height;
+  int mask_len = ((width+7)/8) * height;
+  rdr::U8Array data(data_len);
+  rdr::U8Array mask(mask_len);
+
+  int x, y;
+  rdr::U8 buf[width*height*4];
+  rdr::U8* out;
+
+  if (width * height) {
+    pr = is->readU8();
+    pg = is->readU8();
+    pb = is->readU8();
+
+    sr = is->readU8();
+    sg = is->readU8();
+    sb = is->readU8();
+
+    is->readBytes(data.buf, data_len);
+    is->readBytes(mask.buf, mask_len);
+  }
+
+  int maskBytesPerRow = (width+7)/8;
+  out = buf;
+  for (y = 0;y < height;y++) {
+    for (x = 0;x < width;x++) {
+      int byte = y * maskBytesPerRow + x / 8;
+      int bit = 7 - x % 8;
+
+      if (data.buf[byte] & (1 << bit)) {
+        out[0] = pr;
+        out[1] = pg;
+        out[2] = pb;
+      } else {
+        out[0] = sr;
+        out[1] = sg;
+        out[2] = sb;
+      }
+
+      if (mask.buf[byte] & (1 << bit))
+        out[3] = 255;
+      else
+        out[3] = 0;
+
+      out += 4;
+    }
+  }
+
+  handler->setCursor(width, height, hotspot, buf);
+}
+
 void CMsgReader::readSetCursor(int width, int height, const Point& hotspot)
 {
   int data_len = width * height * (handler->cp.pf().bpp/8);
@@ -198,10 +262,79 @@
   rdr::U8Array data(data_len);
   rdr::U8Array mask(mask_len);
 
+  int x, y;
+  rdr::U8 buf[width*height*4];
+  rdr::U8* in;
+  rdr::U8* out;
+
   is->readBytes(data.buf, data_len);
   is->readBytes(mask.buf, mask_len);
 
-  handler->setCursor(width, height, hotspot, data.buf, mask.buf);
+  int maskBytesPerRow = (width+7)/8;
+  in = data.buf;
+  out = buf;
+  for (y = 0;y < height;y++) {
+    for (x = 0;x < width;x++) {
+      int byte = y * maskBytesPerRow + x / 8;
+      int bit = 7 - x % 8;
+
+      handler->cp.pf().rgbFromBuffer(out, in, 1);
+
+      if (mask.buf[byte] & (1 << bit))
+        out[3] = 255;
+      else
+        out[3] = 0;
+
+      in += handler->cp.pf().bpp/8;
+      out += 4;
+    }
+  }
+
+  handler->setCursor(width, height, hotspot, buf);
+}
+
+void CMsgReader::readSetCursorWithAlpha(int width, int height, const Point& hotspot)
+{
+  int encoding;
+
+  const PixelFormat rgbaPF(32, 32, false, true, 255, 255, 255, 16, 8, 0);
+  ManagedPixelBuffer pb(rgbaPF, width, height);
+  PixelFormat origPF;
+
+  rdr::U8* buf;
+  int stride;
+
+  encoding = is->readS32();
+
+  origPF = handler->cp.pf();
+  handler->cp.setPF(rgbaPF);
+  handler->readAndDecodeRect(pb.getRect(), encoding, &pb);
+  handler->cp.setPF(origPF);
+
+  // On-wire data has pre-multiplied alpha, but we store it
+  // non-pre-multiplied
+  buf = pb.getBufferRW(pb.getRect(), &stride);
+  assert(stride == width);
+
+  for (int i = 0;i < pb.area();i++) {
+    rdr::U8 alpha;
+
+    alpha = buf[3];
+    if (alpha == 0)
+      alpha = 1; // Avoid division by zero
+
+    buf[0] = (unsigned)buf[0] * 255/alpha;
+    buf[1] = (unsigned)buf[1] * 255/alpha;
+    buf[2] = (unsigned)buf[2] * 255/alpha;
+    buf[3] = alpha;
+
+    buf += 4;
+  }
+
+  pb.commitBufferRW(pb.getRect());
+
+  handler->setCursor(width, height, hotspot,
+                     pb.getBuffer(pb.getRect(), &stride));
 }
 
 void CMsgReader::readSetDesktopName(int x, int y, int w, int h)
diff --git a/common/rfb/CMsgReader.h b/common/rfb/CMsgReader.h
index 42c6496..ff73414 100644
--- a/common/rfb/CMsgReader.h
+++ b/common/rfb/CMsgReader.h
@@ -60,7 +60,9 @@
 
     void readRect(const Rect& r, int encoding);
 
+    void readSetXCursor(int width, int height, const Point& hotspot);
     void readSetCursor(int width, int height, const Point& hotspot);
+    void readSetCursorWithAlpha(int width, int height, const Point& hotspot);
     void readSetDesktopName(int x, int y, int w, int h);
     void readExtendedDesktopSize(int x, int y, int w, int h);
 
diff --git a/common/rfb/CMsgWriter.cxx b/common/rfb/CMsgWriter.cxx
index 6536eaf..8576d8f 100644
--- a/common/rfb/CMsgWriter.cxx
+++ b/common/rfb/CMsgWriter.cxx
@@ -71,8 +71,11 @@
   int nEncodings = 0;
   rdr::U32 encodings[encodingMax+3];
 
-  if (cp->supportsLocalCursor)
+  if (cp->supportsLocalCursor) {
+    encodings[nEncodings++] = pseudoEncodingXCursor;
     encodings[nEncodings++] = pseudoEncodingCursor;
+    encodings[nEncodings++] = pseudoEncodingCursorWithAlpha;
+  }
   if (cp->supportsDesktopResize)
     encodings[nEncodings++] = pseudoEncodingDesktopSize;
   if (cp->supportsExtendedDesktopSize)
diff --git a/common/rfb/ConnParams.cxx b/common/rfb/ConnParams.cxx
index ab3b884..9ee1d9c 100644
--- a/common/rfb/ConnParams.cxx
+++ b/common/rfb/ConnParams.cxx
@@ -31,6 +31,7 @@
   : majorVersion(0), minorVersion(0),
     width(0), height(0), useCopyRect(false),
     supportsLocalCursor(false), supportsLocalXCursor(false),
+    supportsLocalCursorWithAlpha(false),
     supportsDesktopResize(false), supportsExtendedDesktopSize(false),
     supportsDesktopRename(false), supportsLastRect(false),
     supportsSetDesktopSize(false), supportsFence(false),
@@ -39,11 +40,13 @@
     subsampling(subsampleUndefined), name_(0), verStrPos(0)
 {
   setName("");
+  cursor_ = new Cursor(0, 0, Point(), NULL);
 }
 
 ConnParams::~ConnParams()
 {
   delete [] name_;
+  delete cursor_;
 }
 
 bool ConnParams::readVersion(rdr::InStream* is, bool* done)
@@ -86,17 +89,8 @@
 
 void ConnParams::setCursor(const Cursor& other)
 {
-  const rdr::U8* data;
-  int stride;
-
-  cursor_.hotspot = other.hotspot;
-  cursor_.setPF(other.getPF());
-  cursor_.setSize(other.width(), other.height());
-
-  data = other.getBuffer(other.getRect(), &stride);
-  cursor_.imageRect(cursor_.getRect(), data, stride);
-
-  memcpy(cursor_.mask.buf, other.mask.buf, cursor_.maskLen());
+  delete cursor_;
+  cursor_ = new Cursor(other);
 }
 
 bool ConnParams::supportsEncoding(rdr::S32 encoding) const
@@ -108,6 +102,7 @@
 {
   useCopyRect = false;
   supportsLocalCursor = false;
+  supportsLocalCursorWithAlpha = false;
   supportsDesktopResize = false;
   supportsExtendedDesktopSize = false;
   supportsLocalXCursor = false;
@@ -131,6 +126,9 @@
     case pseudoEncodingXCursor:
       supportsLocalXCursor = true;
       break;
+    case pseudoEncodingCursorWithAlpha:
+      supportsLocalCursorWithAlpha = true;
+      break;
     case pseudoEncodingDesktopSize:
       supportsDesktopResize = true;
       break;
diff --git a/common/rfb/ConnParams.h b/common/rfb/ConnParams.h
index 9e647ba..5e53893 100644
--- a/common/rfb/ConnParams.h
+++ b/common/rfb/ConnParams.h
@@ -77,7 +77,7 @@
     const char* name() const { return name_; }
     void setName(const char* name);
 
-    const Cursor& cursor() const { return cursor_; }
+    const Cursor& cursor() const { return *cursor_; }
     void setCursor(const Cursor& cursor);
 
     bool supportsEncoding(rdr::S32 encoding) const;
@@ -88,6 +88,7 @@
 
     bool supportsLocalCursor;
     bool supportsLocalXCursor;
+    bool supportsLocalCursorWithAlpha;
     bool supportsDesktopResize;
     bool supportsExtendedDesktopSize;
     bool supportsDesktopRename;
@@ -106,7 +107,7 @@
 
     PixelFormat pf_;
     char* name_;
-    Cursor cursor_;
+    Cursor* cursor_;
     std::set<rdr::S32> encodings_;
     char verStr[13];
     int verStrPos;
diff --git a/common/rfb/Cursor.cxx b/common/rfb/Cursor.cxx
index 3bb8e0f..99df82d 100644
--- a/common/rfb/Cursor.cxx
+++ b/common/rfb/Cursor.cxx
@@ -1,5 +1,5 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright 2014 Pierre Ossman for Cendio AB
+ * Copyright 2014-2017 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
@@ -26,98 +26,179 @@
 
 static LogWriter vlog("Cursor");
 
-void Cursor::setSize(int w, int h) {
-  int oldMaskLen = maskLen();
-  ManagedPixelBuffer::setSize(w, h);
-  if (maskLen() > oldMaskLen) {
-    delete [] mask.buf;
-    mask.buf = new rdr::U8[maskLen()];
+Cursor::Cursor(int width, int height, const Point& hotspot,
+               const rdr::U8* data) :
+  width_(width), height_(height), hotspot_(hotspot)
+{
+  this->data = new rdr::U8[width_*height_*4];
+  memcpy(this->data, data, width_*height_*4);
+}
+
+Cursor::Cursor(const Cursor& other) :
+  width_(other.width_), height_(other.height_),
+  hotspot_(other.hotspot_)
+{
+  data = new rdr::U8[width_*height_*4];
+  memcpy(data, other.data, width_*height_*4);
+}
+
+Cursor::~Cursor()
+{
+  delete [] data;
+}
+
+static unsigned short pow223[] = { 0, 30, 143, 355, 676, 1113, 1673,
+                                   2361, 3181, 4139, 5237, 6479, 7869,
+                                   9409, 11103, 12952, 14961, 17130,
+                                   19462, 21960, 24626, 27461, 30467,
+                                   33647, 37003, 40535, 44245, 48136,
+                                   52209, 56466, 60907, 65535 };
+
+static unsigned short ipow(unsigned short val, unsigned short lut[])
+{
+  int idx = val >> (16-5);
+  int a, b;
+
+  if (val < 0x8000) {
+    a = lut[idx];
+    b = lut[idx+1];
+  } else {
+    a = lut[idx-1];
+    b = lut[idx];
+  }
+
+  return (val & 0x7ff) * (b-a) / 0x7ff + a;
+}
+
+static unsigned short srgb_to_lin(unsigned char srgb)
+{
+  return ipow((unsigned)srgb * 65535 / 255, pow223);
+}
+
+// Floyd-Steinberg dithering
+static void dither(int width, int height, int* data)
+{
+  for (int y = 0; y < height; y++) {
+    for (int x_ = 0; x_ < width; x_++) {
+      int x = (y & 1) ? (width - x_ - 1) : x_;
+      int error;
+
+      if (data[x] > 32767) {
+        error = data[x] - 65535;
+        data[x] = 65535;
+      } else {
+        error = data[x] - 0;
+        data[x] = 0;
+      }
+
+      if (y & 1) {
+        if (x > 0) {
+          data[x - 1] += error * 7 / 16;
+        }
+        if ((y + 1) < height) {
+          if (x > 0)
+            data[x - 1 + width] += error * 3 / 16;
+          data[x + width] += error * 5 / 16;
+          if ((x + 1) < width)
+            data[x + 1] += error * 1 / 16;
+        }
+      } else {
+        if ((x + 1) < width) {
+          data[x + 1] += error * 7 / 16;
+        }
+        if ((y + 1) < height) {
+          if ((x + 1) < width)
+            data[x + 1 + width] += error * 3 / 16;
+          data[x + width] += error * 5 / 16;
+          if (x > 0)
+            data[x - 1] += error * 1 / 16;
+        }
+      }
+    }
+    data += width;
   }
 }
 
-void Cursor::drawOutline(const Pixel& c)
+rdr::U8* Cursor::getBitmap() const
 {
-  Cursor outlined;
-  rdr::U8 cbuf[4];
-
-  // Create a mirror of the existing cursor
-  outlined.setPF(getPF());
-  outlined.setSize(width(), height());
-  outlined.hotspot = hotspot;
-
-  // Clear the mirror's background to the outline colour
-  outlined.getPF().bufferFromPixel(cbuf, c);
-  outlined.fillRect(getRect(), cbuf);
-
-  // Blit the existing cursor, using its mask
-  outlined.maskRect(getRect(), data, mask.buf);
-
-  // Now just adjust the mask to add the outline.  The outline pixels
-  // will already be the right colour. :)
-  int maskBytesPerRow = (width() + 7) / 8;
+  // First step is converting to luminance
+  int luminance[width()*height()];
+  int *lum_ptr = luminance;
+  const rdr::U8 *data_ptr = data;
   for (int y = 0; y < height(); y++) {
-    for (int byte=0; byte<maskBytesPerRow; byte++) {
-      rdr::U8 m8 = mask.buf[y*maskBytesPerRow + byte];
+    for (int x = 0; x < width(); x++) {
+      // Use BT.709 coefficients for grayscale
+      *lum_ptr = 0;
+      *lum_ptr += (int)srgb_to_lin(data_ptr[0]) * 6947;  // 0.2126
+      *lum_ptr += (int)srgb_to_lin(data_ptr[1]) * 23436; // 0.7152
+      *lum_ptr += (int)srgb_to_lin(data_ptr[2]) * 2366;  // 0.0722
+      *lum_ptr /= 32768;
 
-      // Handle above & below outline
-      if (y > 0) m8 |= mask.buf[(y-1)*maskBytesPerRow + byte];
-      if (y < height()-1) m8 |= mask.buf[(y+1)*maskBytesPerRow + byte];
-
-      // Left outline
-      m8 |= mask.buf[y*maskBytesPerRow + byte] << 1;
-      if (byte < maskBytesPerRow-1)
-        m8 |= (mask.buf[y*maskBytesPerRow + byte + 1] >> 7) & 1;
-
-      // Right outline
-      m8 |= mask.buf[y*maskBytesPerRow + byte] >> 1;
-      if (byte > 0)
-        m8 |= (mask.buf[y*maskBytesPerRow + byte - 1] << 7) & 128;
-
-      outlined.mask.buf[y*maskBytesPerRow + byte] = m8;
+      lum_ptr++;
+      data_ptr += 4;
     }
   }
 
-  // Replace the existing cursor & mask with the new one
-  delete [] data;
-  delete [] mask.buf;
-  data = outlined.data; outlined.data = 0;
-  mask.buf = outlined.mask.buf; outlined.mask.buf = 0;
-}
+  // Then diterhing
+  dither(width(), height(), luminance);
 
-rdr::U8* Cursor::getBitmap(Pixel* pix0, Pixel* pix1) const
-{
-  bool gotPix0 = false;
-  bool gotPix1 = false;
-  *pix0 = *pix1 = 0;
-  rdr::U8Array source(maskLen());
-  memset(source.buf, 0, maskLen());
-
+  // Then conversion to a bit mask
+  rdr::U8Array source((width()+7)/8*height());
+  memset(source.buf, 0, (width()+7)/8*height());
   int maskBytesPerRow = (width() + 7) / 8;
-  const rdr::U8 *data_ptr = data;
+  lum_ptr = luminance;
+  data_ptr = data;
   for (int y = 0; y < height(); y++) {
     for (int x = 0; x < width(); x++) {
       int byte = y * maskBytesPerRow + x / 8;
       int bit = 7 - x % 8;
-      if (mask.buf[byte] & (1 << bit)) {
-        Pixel pix = getPF().pixelFromBuffer(data_ptr);
-        if (!gotPix0 || pix == *pix0) {
-          gotPix0 = true;
-          *pix0 = pix;
-        } else if (!gotPix1 || pix == *pix1) {
-          gotPix1 = true;
-          *pix1 = pix;
-          source.buf[byte] |= (1 << bit);
-        } else {
-          // not a bitmap
-          return 0;
-        }
-      }
-      data_ptr += getPF().bpp/8;
+      if (*lum_ptr > 32767)
+        source.buf[byte] |= (1 << bit);
+      lum_ptr++;
+      data_ptr += 4;
     }
   }
+
   return source.takeBuf();
 }
 
+rdr::U8* Cursor::getMask() const
+{
+  // First step is converting to integer array
+  int alpha[width()*height()];
+  int *alpha_ptr = alpha;
+  const rdr::U8 *data_ptr = data;
+  for (int y = 0; y < height(); y++) {
+    for (int x = 0; x < width(); x++) {
+      *alpha_ptr = (int)data_ptr[3] * 65535 / 255;
+      alpha_ptr++;
+      data_ptr += 4;
+    }
+  }
+
+  // Then diterhing
+  dither(width(), height(), alpha);
+
+  // Then conversion to a bit mask
+  rdr::U8Array mask((width()+7)/8*height());
+  memset(mask.buf, 0, (width()+7)/8*height());
+  int maskBytesPerRow = (width() + 7) / 8;
+  alpha_ptr = alpha;
+  data_ptr = data;
+  for (int y = 0; y < height(); y++) {
+    for (int x = 0; x < width(); x++) {
+      int byte = y * maskBytesPerRow + x / 8;
+      int bit = 7 - x % 8;
+      if (*alpha_ptr > 32767)
+        mask.buf[byte] |= (1 << bit);
+      alpha_ptr++;
+      data_ptr += 4;
+    }
+  }
+
+  return mask.takeBuf();
+}
+
 // crop() determines the "busy" rectangle for the cursor - the minimum bounding
 // rectangle containing actual pixels.  This isn't the most efficient algorithm
 // but it's short.  For sanity, we make sure that the busy rectangle always
@@ -126,58 +207,40 @@
 
 void Cursor::crop()
 {
-  Rect busy = getRect().intersect(Rect(hotspot.x, hotspot.y,
-                                       hotspot.x+1, hotspot.y+1));
-  int maskBytesPerRow = (width() + 7) / 8;
+  Rect busy = Rect(0, 0, width_, height_);
+  busy = busy.intersect(Rect(hotspot_.x, hotspot_.y,
+                             hotspot_.x+1, hotspot_.y+1));
   int x, y;
+  rdr::U8 *data_ptr = data;
   for (y = 0; y < height(); y++) {
     for (x = 0; x < width(); x++) {
-      int byte = y * maskBytesPerRow + x / 8;
-      int bit = 7 - x % 8;
-      if (mask.buf[byte] & (1 << bit)) {
+      if (data_ptr[3] > 0) {
         if (x < busy.tl.x) busy.tl.x = x;
         if (x+1 > busy.br.x) busy.br.x = x+1;
         if (y < busy.tl.y) busy.tl.y = y;
         if (y+1 > busy.br.y) busy.br.y = y+1;
       }
+      data_ptr += 4;
     }
   }
 
   if (width() == busy.width() && height() == busy.height()) return;
 
-  vlog.debug("cropping %dx%d to %dx%d", width(), height(),
-             busy.width(), busy.height());
-
   // Copy the pixel data
-  int newDataLen = busy.area() * (getPF().bpp/8);
+  int newDataLen = busy.area() * 4;
   rdr::U8* newData = new rdr::U8[newDataLen];
-  getImage(newData, busy);
-
-  // Copy the mask
-  int newMaskBytesPerRow = (busy.width()+7)/8;
-  int newMaskLen = newMaskBytesPerRow * busy.height();
-  rdr::U8* newMask = new rdr::U8[newMaskLen];
-  memset(newMask, 0, newMaskLen);
-  for (y = 0; y < busy.height(); y++) {
-    int newByte, newBit;
-    for (x = 0; x < busy.width(); x++) {
-      int oldByte = (y+busy.tl.y) * maskBytesPerRow + (x+busy.tl.x) / 8;
-      int oldBit = 7 - (x+busy.tl.x) % 8;
-      newByte = y * newMaskBytesPerRow + x / 8;
-      newBit = 7 - x % 8;
-      if (mask.buf[oldByte] & (1 << oldBit))
-        newMask[newByte] |= (1 << newBit);
-    }
+  data_ptr = newData;
+  for (y = busy.tl.y; y < busy.br.y; y++) {
+    memcpy(data_ptr, data + y*width()*4 + busy.tl.x*4, busy.width()*4);
+    data_ptr += busy.width()*4;
   }
 
   // Set the size and data to the new, cropped cursor.
-  setSize(busy.width(), busy.height());
-  hotspot = hotspot.subtract(busy.tl);
+  width_ = busy.width();
+  height_ = busy.height();
+  hotspot_ = hotspot_.subtract(busy.tl);
   delete [] data;
-  delete [] mask.buf;
-  datasize = newDataLen;
   data = newData;
-  mask.buf = newMask;
 }
 
 RenderedCursor::RenderedCursor()
@@ -198,7 +261,7 @@
 void RenderedCursor::update(PixelBuffer* framebuffer,
                             Cursor* cursor, const Point& pos)
 {
-  Point rawOffset;
+  Point rawOffset, diff;
   Rect clippedRect;
 
   const rdr::U8* data;
@@ -207,24 +270,53 @@
   assert(framebuffer);
   assert(cursor);
 
-  if (!framebuffer->getPF().equal(cursor->getPF()))
-    throw Exception("RenderedCursor: Trying to render cursor on incompatible frame buffer");
-
   format = framebuffer->getPF();
   width_ = framebuffer->width();
   height_ = framebuffer->height();
 
-  rawOffset = pos.subtract(cursor->hotspot);
-  clippedRect = cursor->getRect(rawOffset).intersect(framebuffer->getRect());
+  rawOffset = pos.subtract(cursor->hotspot());
+  clippedRect = Rect(0, 0, cursor->width(), cursor->height())
+                .translate(rawOffset)
+                .intersect(framebuffer->getRect());
   offset = clippedRect.tl;
 
-  buffer.setPF(cursor->getPF());
+  buffer.setPF(format);
   buffer.setSize(clippedRect.width(), clippedRect.height());
 
+  // Bail out early to avoid pestering the framebuffer with
+  // bogus coordinates
+  if (clippedRect.area() == 0)
+    return;
+
   data = framebuffer->getBuffer(buffer.getRect(offset), &stride);
   buffer.imageRect(buffer.getRect(), data, stride);
 
-  data = cursor->getBuffer(cursor->getRect(), &stride);
-  buffer.maskRect(cursor->getRect(rawOffset.subtract(offset)),
-                  data, cursor->mask.buf);
+  diff = offset.subtract(rawOffset);
+  for (int y = 0;y < buffer.height();y++) {
+    for (int x = 0;x < buffer.width();x++) {
+      size_t idx;
+      rdr::U8 bg[4], fg[4];
+      rdr::U8 rgb[3];
+
+      idx = (y+diff.y)*cursor->width() + (x+diff.x);
+      memcpy(fg, cursor->getBuffer() + idx*4, 4);
+
+      if (fg[3] == 0x00)
+        continue;
+      else if (fg[3] == 0xff) {
+        memcpy(rgb, fg, 3);
+      } else {
+        buffer.getImage(bg, Rect(x, y, x+1, y+1));
+        format.rgbFromBuffer(rgb, bg, 1);
+        // FIXME: Gamma aware blending
+        for (int i = 0;i < 3;i++) {
+          rgb[i] = (unsigned)rgb[i]*(255-fg[3])/255 +
+                   (unsigned)fg[i]*fg[3]/255;
+        }
+      }
+
+      format.bufferFromRGB(bg, rgb, 1);
+      buffer.imageRect(Rect(x, y, x+1, y+1), bg);
+    }
+  }
 }
diff --git a/common/rfb/Cursor.h b/common/rfb/Cursor.h
index 560e4d8..6c6db7e 100644
--- a/common/rfb/Cursor.h
+++ b/common/rfb/Cursor.h
@@ -1,5 +1,5 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright 2014 Pierre Ossman for Cendio AB
+ * Copyright 2014-2017 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
@@ -28,29 +28,30 @@
 
 namespace rfb {
 
-  class Cursor : public ManagedPixelBuffer {
+  class Cursor {
   public:
-    Cursor() {}
-    rdr::U8Array mask;
-    Point hotspot;
+    Cursor(int width, int height, const Point& hotspot, const rdr::U8* data);
+    Cursor(const Cursor& other);
+    ~Cursor();
 
-    int maskLen() const { return (width() + 7) / 8 * height(); }
+    int width() const { return width_; };
+    int height() const { return height_; };
+    const Point& hotspot() const { return hotspot_; };
+    const rdr::U8* getBuffer() const { return data; };
 
-    // setSize() resizes the cursor.  The contents of the data and mask are
-    // undefined after this call.
-    virtual void setSize(int w, int h);
-
-    // drawOutline() adds an outline to the cursor in the given colour.
-    void drawOutline(const Pixel& c);
-
-    // getBitmap() tests whether the cursor is monochrome, and if so returns a
-    // bitmap together with background and foreground colours.  The size and
-    // layout of the bitmap are the same as the mask.
-    rdr::U8* getBitmap(Pixel* pix0, Pixel* pix1) const;
+    // getBitmap() returns a monochrome version of the cursor
+    rdr::U8* getBitmap() const;
+    // getMask() returns a simple mask version of the alpha channel
+    rdr::U8* getMask() const;
 
     // crop() crops the cursor down to the smallest possible size, based on the
     // mask.
     void crop();
+
+  protected:
+    int width_, height_;
+    Point hotspot_;
+    rdr::U8* data;
   };
 
   class RenderedCursor : public PixelBuffer {
diff --git a/common/rfb/DecodeManager.cxx b/common/rfb/DecodeManager.cxx
index a655c53..ccf084f 100644
--- a/common/rfb/DecodeManager.cxx
+++ b/common/rfb/DecodeManager.cxx
@@ -197,7 +197,7 @@
 {
   os::AutoMutex a(queueMutex);
 
-  if (threadException == NULL)
+  if (threadException != NULL)
     return;
 
   threadException = new rdr::Exception("Exception on worker thread: %s", e.str());
diff --git a/common/rfb/Encoder.h b/common/rfb/Encoder.h
index 62cb6eb..a8a447e 100644
--- a/common/rfb/Encoder.h
+++ b/common/rfb/Encoder.h
@@ -43,7 +43,7 @@
             enum EncoderFlags flags, unsigned int maxPaletteSize);
     virtual ~Encoder();
 
-    // isSupported() should return a boolean indiciating if this encoder
+    // isSupported() should return a boolean indicating if this encoder
     // is okay to use with the current connection. This usually involves
     // checking the list of encodings in the connection parameters.
     virtual bool isSupported()=0;
diff --git a/common/rfb/JpegCompressor.cxx b/common/rfb/JpegCompressor.cxx
index c8bf841..27cb9de 100644
--- a/common/rfb/JpegCompressor.cxx
+++ b/common/rfb/JpegCompressor.cxx
@@ -42,7 +42,7 @@
 static const PixelFormat pfXBGR(32, 24, false, true, 255, 255, 255, 24, 16, 8);
 
 //
-// Error manager implmentation for the JPEG library
+// Error manager implementation for the JPEG library
 //
 
 struct JPEG_ERROR_MGR {
diff --git a/common/rfb/JpegDecompressor.cxx b/common/rfb/JpegDecompressor.cxx
index 70a4276..4f94faa 100644
--- a/common/rfb/JpegDecompressor.cxx
+++ b/common/rfb/JpegDecompressor.cxx
@@ -43,7 +43,7 @@
 static const PixelFormat pfXBGR(32, 24, false, true, 255, 255, 255, 24, 16, 8);
 
 //
-// Error manager implmentation for the JPEG library
+// Error manager implementation for the JPEG library
 //
 
 struct JPEG_ERROR_MGR {
diff --git a/common/rfb/PixelBuffer.cxx b/common/rfb/PixelBuffer.cxx
index 89addab..007b6c8 100644
--- a/common/rfb/PixelBuffer.cxx
+++ b/common/rfb/PixelBuffer.cxx
@@ -42,17 +42,32 @@
 
 
 void
-PixelBuffer::getImage(void* imageBuf, const Rect& r, int outStride) const {
+PixelBuffer::getImage(void* imageBuf, const Rect& r, int outStride) const
+{
   int inStride;
-  const U8* data = getBuffer(r, &inStride);
-  // We assume that the specified rectangle is pre-clipped to the buffer
-  int bytesPerPixel = format.bpp/8;
-  int inBytesPerRow = inStride * bytesPerPixel;
-  if (!outStride) outStride = r.width();
-  int outBytesPerRow = outStride * bytesPerPixel;
-  int bytesPerMemCpy = r.width() * bytesPerPixel;
-  U8* imageBufPos = (U8*)imageBuf;
-  const U8* end = data + (inBytesPerRow * r.height());
+  const U8* data;
+  int bytesPerPixel, inBytesPerRow, outBytesPerRow, bytesPerMemCpy;
+  U8* imageBufPos;
+  const U8* end;
+
+  if (!r.enclosed_by(getRect()))
+    throw rfb::Exception("Source rect %dx%d at %d,%d exceeds framebuffer %dx%d",
+                         r.width(), r.height(),
+                         r.tl.x, r.tl.y, width_, height_);
+
+  data = getBuffer(r, &inStride);
+
+  bytesPerPixel = format.bpp/8;
+  inBytesPerRow = inStride * bytesPerPixel;
+
+  if (!outStride)
+    outStride = r.width();
+  outBytesPerRow = outStride * bytesPerPixel;
+  bytesPerMemCpy = r.width() * bytesPerPixel;
+
+  imageBufPos = (U8*)imageBuf;
+  end = data + (inBytesPerRow * r.height());
+
   while (data < end) {
     memcpy(imageBufPos, data, bytesPerMemCpy);
     imageBufPos += outBytesPerRow;
@@ -71,6 +86,11 @@
     return;
   }
 
+  if (!r.enclosed_by(getRect()))
+    throw rfb::Exception("Source rect %dx%d at %d,%d exceeds framebuffer %dx%d",
+                         r.width(), r.height(),
+                         r.tl.x, r.tl.y, width_, height_);
+
   if (stride == 0)
     stride = r.width();
 
@@ -102,6 +122,10 @@
   U8 *buf;
   int w, h, b;
 
+  if (!r.enclosed_by(getRect()))
+    throw rfb::Exception("Destination rect %dx%d at %d,%d exceeds framebuffer %dx%d",
+                         r.width(), r.height(), r.tl.x, r.tl.y, width_, height_);
+
   w = r.width();
   h = r.height();
   b = format.bpp/8;
@@ -142,105 +166,40 @@
 void ModifiablePixelBuffer::imageRect(const Rect& r,
                                       const void* pixels, int srcStride)
 {
-  int bytesPerPixel = getPF().bpp/8;
+  U8* dest;
   int destStride;
-  U8* dest = getBufferRW(r, &destStride);
-  int bytesPerDestRow = bytesPerPixel * destStride;
-  if (!srcStride) srcStride = r.width();
-  int bytesPerSrcRow = bytesPerPixel * srcStride;
-  int bytesPerFill = bytesPerPixel * r.width();
-  const U8* src = (const U8*)pixels;
-  U8* end = dest + (bytesPerDestRow * r.height());
+  int bytesPerPixel, bytesPerDestRow, bytesPerSrcRow, bytesPerFill;
+  const U8* src;
+  U8* end;
+
+  if (!r.enclosed_by(getRect()))
+    throw rfb::Exception("Destination rect %dx%d at %d,%d exceeds framebuffer %dx%d",
+                         r.width(), r.height(),
+                         r.tl.x, r.tl.y, width_, height_);
+
+  bytesPerPixel = getPF().bpp/8;
+
+  dest = getBufferRW(r, &destStride);
+
+  bytesPerDestRow = bytesPerPixel * destStride;
+
+  if (!srcStride)
+    srcStride = r.width();
+  bytesPerSrcRow = bytesPerPixel * srcStride;
+  bytesPerFill = bytesPerPixel * r.width();
+
+  src = (const U8*)pixels;
+  end = dest + (bytesPerDestRow * r.height());
+
   while (dest < end) {
     memcpy(dest, src, bytesPerFill);
     dest += bytesPerDestRow;
     src += bytesPerSrcRow;
   }
+
   commitBufferRW(r);
 }
 
-void ModifiablePixelBuffer::maskRect(const Rect& r,
-                                     const void* pixels, const void* mask_)
-{
-  Rect cr = getRect().intersect(r);
-  if (cr.is_empty()) return;
-  int stride;
-  U8* data = getBufferRW(cr, &stride);
-  U8* mask = (U8*) mask_;
-  int w = cr.width();
-  int h = cr.height();
-  int bpp = getPF().bpp;
-  int pixelStride = r.width();
-  int maskStride = (r.width() + 7) / 8;
-
-  Point offset = Point(cr.tl.x-r.tl.x, cr.tl.y-r.tl.y);
-  mask += offset.y * maskStride;
-  for (int y = 0; y < h; y++) {
-    int cy = offset.y + y;
-    for (int x = 0; x < w; x++) {
-      int cx = offset.x + x;
-      U8* byte = mask + (cx / 8);
-      int bit = 7 - cx % 8;
-      if ((*byte) & (1 << bit)) {
-        switch (bpp) {
-        case 8:
-          ((U8*)data)[y * stride + x] = ((U8*)pixels)[cy * pixelStride + cx];
-          break;
-        case 16:
-          ((U16*)data)[y * stride + x] = ((U16*)pixels)[cy * pixelStride + cx];
-          break;
-        case 32:
-          ((U32*)data)[y * stride + x] = ((U32*)pixels)[cy * pixelStride + cx];
-          break;
-        }
-      }
-    }
-    mask += maskStride;
-  }
-
-  commitBufferRW(cr);
-}
-
-void ModifiablePixelBuffer::maskRect(const Rect& r,
-                                     Pixel pixel, const void* mask_)
-{
-  Rect cr = getRect().intersect(r);
-  if (cr.is_empty()) return;
-  int stride;
-  U8* data = getBufferRW(cr, &stride);
-  U8* mask = (U8*) mask_;
-  int w = cr.width();
-  int h = cr.height();
-  int bpp = getPF().bpp;
-  int maskStride = (r.width() + 7) / 8;
-
-  Point offset = Point(cr.tl.x-r.tl.x, cr.tl.y-r.tl.y);
-  mask += offset.y * maskStride;
-  for (int y = 0; y < h; y++) {
-    for (int x = 0; x < w; x++) {
-      int cx = offset.x + x;
-      U8* byte = mask + (cx / 8);
-      int bit = 7 - cx % 8;
-      if ((*byte) & (1 << bit)) {
-        switch (bpp) {
-        case 8:
-          ((U8*)data)[y * stride + x] = pixel;
-          break;
-        case 16:
-          ((U16*)data)[y * stride + x] = pixel;
-          break;
-        case 32:
-          ((U32*)data)[y * stride + x] = pixel;
-          break;
-        }
-      }
-    }
-    mask += maskStride;
-  }
-
-  commitBufferRW(cr);
-}
-
 void ModifiablePixelBuffer::copyRect(const Rect &rect,
                                      const Point &move_by_delta)
 {
@@ -251,26 +210,16 @@
   Rect drect, srect;
 
   drect = rect;
-  if (!drect.enclosed_by(getRect())) {
-    vlog.error("Destination rect %dx%d at %d,%d exceeds framebuffer %dx%d",
-               drect.width(), drect.height(), drect.tl.x, drect.tl.y, width_, height_);
-    drect = drect.intersect(getRect());
-  }
-
-  if (drect.is_empty())
-    return;
+  if (!drect.enclosed_by(getRect()))
+    throw rfb::Exception("Destination rect %dx%d at %d,%d exceeds framebuffer %dx%d",
+                         drect.width(), drect.height(),
+                         drect.tl.x, drect.tl.y, width_, height_);
 
   srect = drect.translate(move_by_delta.negate());
-  if (!srect.enclosed_by(getRect())) {
-    vlog.error("Source rect %dx%d at %d,%d exceeds framebuffer %dx%d",
-               srect.width(), srect.height(), srect.tl.x, srect.tl.y, width_, height_);
-    srect = srect.intersect(getRect());
-    // Need to readjust the destination now that the area has changed
-    drect = srect.translate(move_by_delta);
-  }
-
-  if (srect.is_empty())
-    return;
+  if (!srect.enclosed_by(getRect()))
+    throw rfb::Exception("Source rect %dx%d at %d,%d exceeds framebuffer %dx%d",
+                         srect.width(), srect.height(),
+                         srect.tl.x, srect.tl.y, width_, height_);
 
   srcData = getBuffer(srect, &srcStride);
   dstData = getBufferRW(drect, &dstStride);
@@ -320,6 +269,11 @@
   rdr::U8* dstBuffer;
   int dstStride;
 
+  if (!dest.enclosed_by(getRect()))
+    throw rfb::Exception("Destination rect %dx%d at %d,%d exceeds framebuffer %dx%d",
+                         dest.width(), dest.height(),
+                         dest.tl.x, dest.tl.y, width_, height_);
+
   if (stride == 0)
     stride = dest.width();
 
@@ -344,6 +298,11 @@
 
 rdr::U8* FullFramePixelBuffer::getBufferRW(const Rect& r, int* stride_)
 {
+  if (!r.enclosed_by(getRect()))
+    throw rfb::Exception("Pixel buffer request %dx%d at %d,%d exceeds framebuffer %dx%d",
+                         r.width(), r.height(),
+                         r.tl.x, r.tl.y, width_, height_);
+
   *stride_ = stride;
   return &data[(r.tl.x + (r.tl.y * stride)) * format.bpp/8];
 }
@@ -354,6 +313,11 @@
 
 const rdr::U8* FullFramePixelBuffer::getBuffer(const Rect& r, int* stride_) const
 {
+  if (!r.enclosed_by(getRect()))
+    throw rfb::Exception("Pixel buffer request %dx%d at %d,%d exceeds framebuffer %dx%d",
+                         r.width(), r.height(),
+                         r.tl.x, r.tl.y, width_, height_);
+
   *stride_ = stride;
   return &data[(r.tl.x + (r.tl.y * stride)) * format.bpp/8];
 }
@@ -392,7 +356,6 @@
 ManagedPixelBuffer::checkDataSize() {
   unsigned long new_datasize = width_ * height_ * (format.bpp/8);
   if (datasize < new_datasize) {
-    vlog.debug("reallocating managed buffer (%dx%d)", width_, height_);
     if (data) {
       delete [] data;
       datasize = 0; data = 0;
diff --git a/common/rfb/PixelBuffer.h b/common/rfb/PixelBuffer.h
index ba586b0..75caa63 100644
--- a/common/rfb/PixelBuffer.h
+++ b/common/rfb/PixelBuffer.h
@@ -127,16 +127,6 @@
     // Copy pixel data from one PixelBuffer location to another
     void copyRect(const Rect &dest, const Point& move_by_delta);
 
-    // Copy pixel data to the buffer through a mask
-    //   pixels is a pointer to the pixel to be copied to r.tl.
-    //   maskPos specifies the pixel offset in the mask to start from.
-    //   mask_ is a pointer to the mask bits at (0,0).
-    //   pStride and mStride are the strides of the pixel and mask buffers.
-    void maskRect(const Rect& r, const void* pixels, const void* mask_);
-
-    //   pixel is the Pixel value to be used where mask_ is set
-    void maskRect(const Rect& r, Pixel pixel, const void* mask_);
-
     // Render in a specific format
     //   Does the exact same thing as the above methods, but the given
     //   pixel values are defined by the given PixelFormat.
diff --git a/common/rfb/SMsgWriter.cxx b/common/rfb/SMsgWriter.cxx
index 5040b65..cf3264e 100644
--- a/common/rfb/SMsgWriter.cxx
+++ b/common/rfb/SMsgWriter.cxx
@@ -1,6 +1,6 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
  * Copyright (C) 2011 D. R. Commander.  All Rights Reserved.
- * Copyright 2009-2014 Pierre Ossman for Cendio AB
+ * Copyright 2009-2017 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
@@ -36,7 +36,8 @@
   : cp(cp_), os(os_),
     nRectsInUpdate(0), nRectsInHeader(0),
     needSetDesktopSize(false), needExtendedDesktopSize(false),
-    needSetDesktopName(false), needSetCursor(false), needSetXCursor(false)
+    needSetDesktopName(false), needSetCursor(false),
+    needSetXCursor(false), needSetCursorWithAlpha(false)
 {
 }
 
@@ -180,11 +181,21 @@
   return true;
 }
 
+bool SMsgWriter::writeSetCursorWithAlpha()
+{
+  if (!cp->supportsLocalCursorWithAlpha)
+    return false;
+
+  needSetCursorWithAlpha = true;
+
+  return true;
+}
+
 bool SMsgWriter::needFakeUpdate()
 {
   if (needSetDesktopName)
     return true;
-  if (needSetCursor || needSetXCursor)
+  if (needSetCursor || needSetXCursor || needSetCursorWithAlpha)
     return true;
   if (needNoDataUpdate())
     return true;
@@ -232,6 +243,8 @@
       nRects++;
     if (needSetXCursor)
       nRects++;
+    if (needSetCursorWithAlpha)
+      nRects++;
   }
 
   os->writeU16(nRects);
@@ -301,41 +314,48 @@
 void SMsgWriter::writePseudoRects()
 {
   if (needSetCursor) {
-    rdr::U8* data;
-
     const Cursor& cursor = cp->cursor();
 
-    data = new rdr::U8[cursor.area() * cp->pf().bpp/8];
-    cursor.getImage(cp->pf(), data, cursor.getRect());
+    rdr::U8Array data(cursor.width()*cursor.height() * cp->pf().bpp/8);
+    rdr::U8Array mask(cursor.getMask());
+
+    const rdr::U8* in;
+    rdr::U8* out;
+
+    in = cursor.getBuffer();
+    out = data.buf;
+    for (int i = 0;i < cursor.width()*cursor.height();i++) {
+      cp->pf().bufferFromRGB(out, in, 1);
+      in += 4;
+      out += cp->pf().bpp/8;
+    }
 
     writeSetCursorRect(cursor.width(), cursor.height(),
-                       cursor.hotspot.x, cursor.hotspot.y,
-                       data, cursor.mask.buf);
+                       cursor.hotspot().x, cursor.hotspot().y,
+                       data.buf, mask.buf);
     needSetCursor = false;
-
-    delete [] data;
   }
 
   if (needSetXCursor) {
     const Cursor& cursor = cp->cursor();
-    Pixel pix0, pix1;
-    rdr::U8 rgb0[3], rgb1[3];
-    rdr::U8Array bitmap(cursor.getBitmap(&pix0, &pix1));
-
-    if (!bitmap.buf) {
-      // FIXME: We could reduce to two colors.
-      throw Exception("SMsgWriter::writePseudoRects: Unable to send multicolor cursor: RichCursor not supported by client");
-    }
-
-    cp->pf().rgbFromPixel(pix0, &rgb0[0], &rgb0[1], &rgb0[2]);
-    cp->pf().rgbFromPixel(pix1, &rgb1[0], &rgb1[1], &rgb1[2]);
+    rdr::U8Array bitmap(cursor.getBitmap());
+    rdr::U8Array mask(cursor.getMask());
 
     writeSetXCursorRect(cursor.width(), cursor.height(),
-                        cursor.hotspot.x, cursor.hotspot.y,
-                        rgb0, rgb1, bitmap.buf, cursor.mask.buf);
+                        cursor.hotspot().x, cursor.hotspot().y,
+                        bitmap.buf, mask.buf);
     needSetXCursor = false;
   }
 
+  if (needSetCursorWithAlpha) {
+    const Cursor& cursor = cp->cursor();
+
+    writeSetCursorWithAlphaRect(cursor.width(), cursor.height(),
+                                cursor.hotspot().x, cursor.hotspot().y,
+                                cursor.getBuffer());
+    needSetCursorWithAlpha = false;
+  }
+
   if (needSetDesktopName) {
     writeSetDesktopNameRect(cp->name());
     needSetDesktopName = false;
@@ -452,8 +472,6 @@
 
 void SMsgWriter::writeSetXCursorRect(int width, int height,
                                      int hotspotX, int hotspotY,
-                                     const rdr::U8 pix0[],
-                                     const rdr::U8 pix1[],
                                      const void* data, const void* mask)
 {
   if (!cp->supportsLocalXCursor)
@@ -467,13 +485,41 @@
   os->writeU16(height);
   os->writeU32(pseudoEncodingXCursor);
   if (width * height) {
-    os->writeU8(pix0[0]);
-    os->writeU8(pix0[1]);
-    os->writeU8(pix0[2]);
-    os->writeU8(pix1[0]);
-    os->writeU8(pix1[1]);
-    os->writeU8(pix1[2]);
+    os->writeU8(255);
+    os->writeU8(255);
+    os->writeU8(255);
+    os->writeU8(0);
+    os->writeU8(0);
+    os->writeU8(0);
     os->writeBytes(data, (width+7)/8 * height);
     os->writeBytes(mask, (width+7)/8 * height);
   }
 }
+
+void SMsgWriter::writeSetCursorWithAlphaRect(int width, int height,
+                                             int hotspotX, int hotspotY,
+                                             const rdr::U8* data)
+{
+  if (!cp->supportsLocalCursorWithAlpha)
+    throw Exception("Client does not support local cursors");
+  if (++nRectsInUpdate > nRectsInHeader && nRectsInHeader)
+    throw Exception("SMsgWriter::writeSetCursorWithAlphaRect: nRects out of sync");
+
+  os->writeS16(hotspotX);
+  os->writeS16(hotspotY);
+  os->writeU16(width);
+  os->writeU16(height);
+  os->writeU32(pseudoEncodingCursorWithAlpha);
+
+  // FIXME: Use an encoder with compression?
+  os->writeU32(encodingRaw);
+
+  // Alpha needs to be pre-multiplied
+  for (int i = 0;i < width*height;i++) {
+    os->writeU8((unsigned)data[0] * data[3] / 255);
+    os->writeU8((unsigned)data[1] * data[3] / 255);
+    os->writeU8((unsigned)data[2] * data[3] / 255);
+    os->writeU8(data[3]);
+    data += 4;
+  }
+}
diff --git a/common/rfb/SMsgWriter.h b/common/rfb/SMsgWriter.h
index 917b933..548b8e8 100644
--- a/common/rfb/SMsgWriter.h
+++ b/common/rfb/SMsgWriter.h
@@ -80,6 +80,7 @@
     // immediately. 
     bool writeSetCursor();
     bool writeSetXCursor();
+    bool writeSetCursorWithAlpha();
 
     // needFakeUpdate() returns true when an immediate update is needed in
     // order to flush out pseudo-rectangles to the client.
@@ -126,8 +127,10 @@
                             const void* data, const void* mask);
     void writeSetXCursorRect(int width, int height,
                              int hotspotX, int hotspotY,
-                             const rdr::U8 pix0[], const rdr::U8 pix1[],
                              const void* data, const void* mask);
+    void writeSetCursorWithAlphaRect(int width, int height,
+                                     int hotspotX, int hotspotY,
+                                     const rdr::U8* data);
 
     ConnParams* cp;
     rdr::OutStream* os;
@@ -141,6 +144,7 @@
     bool needLastRect;
     bool needSetCursor;
     bool needSetXCursor;
+    bool needSetCursorWithAlpha;
 
     typedef struct {
       rdr::U16 reason, result;
diff --git a/common/rfb/VNCSConnectionST.cxx b/common/rfb/VNCSConnectionST.cxx
index 9b5eaf8..0a2ca33 100644
--- a/common/rfb/VNCSConnectionST.cxx
+++ b/common/rfb/VNCSConnectionST.cxx
@@ -51,7 +51,7 @@
 static const unsigned INITIAL_WINDOW = 16384;
 
 // TCP's minimal window is 3*MSS. But since we don't know the MSS, we
-// make a guess at 4 KiB (it's probaly a bit higher).
+// make a guess at 4 KiB (it's probably a bit higher).
 static const unsigned MINIMUM_WINDOW = 4096;
 
 // The current default maximum window for Linux (4 MiB). Should be a good
@@ -827,7 +827,7 @@
   }
 
   // We only keep track of the minimum latency seen (for a given interval)
-  // on the basis that we want to avoid continous buffer issue, but don't
+  // on the basis that we want to avoid continuous buffer issue, but don't
   // mind (or even approve of) bursts.
   if (rtt < minRTT)
     minRTT = rtt;
@@ -1129,12 +1129,14 @@
   if (state() != RFBSTATE_NORMAL)
     return;
 
-  cp.setCursor(server->cursor);
+  cp.setCursor(*server->cursor);
 
-  if (!writer()->writeSetCursor()) {
-    if (!writer()->writeSetXCursor()) {
-      // No client support
-      return;
+  if (!writer()->writeSetCursorWithAlpha()) {
+    if (!writer()->writeSetCursor()) {
+      if (!writer()->writeSetXCursor()) {
+        // No client support
+        return;
+      }
     }
   }
 
diff --git a/common/rfb/VNCServer.h b/common/rfb/VNCServer.h
index c76e5c9..982a4ff 100644
--- a/common/rfb/VNCServer.h
+++ b/common/rfb/VNCServer.h
@@ -64,16 +64,10 @@
     virtual void closeClients(const char* reason) = 0;
 
     // setCursor() tells the server that the cursor has changed.  The
-    // cursorData argument contains width*height pixel values in the pixel
-    // buffer's format.  The mask argument is a bitmask with a 1-bit meaning
-    // the corresponding pixel in cursorData is valid.  The mask consists of
-    // left-to-right, top-to-bottom scanlines, where each scanline is padded to
-    // a whole number of bytes [(width+7)/8].  Within each byte the most
-    // significant bit represents the leftmost pixel, and the bytes are simply
-    // in left-to-right order.  The server takes its own copy of the data in
-    // cursorData and mask.
+    // cursorData argument contains width*height rgba quadruplets with
+    // non-premultiplied alpha.
     virtual void setCursor(int width, int height, const Point& hotspot,
-                           const void* cursorData, const void* mask) = 0;
+                           const rdr::U8* cursorData) = 0;
 
     // setCursorPos() tells the server the current position of the cursor.
     virtual void setCursorPos(const Point& p) = 0;
diff --git a/common/rfb/VNCServerST.cxx b/common/rfb/VNCServerST.cxx
index 80c992f..ec5e962 100644
--- a/common/rfb/VNCServerST.cxx
+++ b/common/rfb/VNCServerST.cxx
@@ -1,5 +1,5 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright 2009-2016 Pierre Ossman for Cendio AB
+ * Copyright 2009-2017 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
@@ -76,6 +76,7 @@
   : blHosts(&blacklist), desktop(desktop_), desktopStarted(false),
     blockCounter(0), pb(0),
     name(strDup(name_)), pointerClient(0), comparer(0),
+    cursor(new Cursor(0, 0, Point(), NULL)),
     renderedCursorInvalid(false),
     queryConnectionHandler(0), keyRemapper(&KeyRemapper::defInstance),
     lastConnectionTime(0), disableclients(false),
@@ -110,6 +111,8 @@
   if (comparer)
     comparer->logStats();
   delete comparer;
+
+  delete cursor;
 }
 
 
@@ -315,7 +318,6 @@
   }
 
   comparer = new ComparingUpdateTracker(pb);
-  cursor.setPF(pb->getPF());
   renderedCursorInvalid = true;
 
   // Make sure that we have at least one screen
@@ -429,14 +431,11 @@
 }
 
 void VNCServerST::setCursor(int width, int height, const Point& newHotspot,
-                            const void* data, const void* mask)
+                            const rdr::U8* data)
 {
-  cursor.hotspot = newHotspot;
-  cursor.setSize(width, height);
-  cursor.imageRect(cursor.getRect(), data);
-  memcpy(cursor.mask.buf, mask, cursor.maskLen());
-
-  cursor.crop();
+  delete cursor;
+  cursor = new Cursor(width, height, newHotspot, data);
+  cursor->crop();
 
   renderedCursorInvalid = true;
 
@@ -585,8 +584,9 @@
   toCheck = ui.changed.union_(ui.copied);
 
   if (needRenderedCursor()) {
-    Rect clippedCursorRect
-      = cursor.getRect(cursorPos.subtract(cursor.hotspot)).intersect(pb->getRect());
+    Rect clippedCursorRect = Rect(0, 0, cursor->width(), cursor->height())
+                             .translate(cursorPos.subtract(cursor->hotspot()))
+                             .intersect(pb->getRect());
 
     if (!toCheck.intersect(clippedCursorRect).is_empty())
       renderedCursorInvalid = true;
@@ -631,7 +631,7 @@
 const RenderedCursor* VNCServerST::getRenderedCursor()
 {
   if (renderedCursorInvalid) {
-    renderedCursor.update(pb, &cursor, cursorPos);
+    renderedCursor.update(pb, cursor, cursorPos);
     renderedCursorInvalid = false;
   }
 
diff --git a/common/rfb/VNCServerST.h b/common/rfb/VNCServerST.h
index b5eb415..00f77c7 100644
--- a/common/rfb/VNCServerST.h
+++ b/common/rfb/VNCServerST.h
@@ -99,7 +99,7 @@
     virtual void add_changed(const Region &region);
     virtual void add_copied(const Region &dest, const Point &delta);
     virtual void setCursor(int width, int height, const Point& hotspot,
-                           const void* cursorData, const void* mask);
+                           const rdr::U8* data);
     virtual void setCursorPos(const Point& p);
 
     virtual void bell();
@@ -219,7 +219,7 @@
     ComparingUpdateTracker* comparer;
 
     Point cursorPos;
-    Cursor cursor;
+    Cursor* cursor;
     RenderedCursor renderedCursor;
     bool renderedCursorInvalid;
 
diff --git a/common/rfb/XF86keysym.h b/common/rfb/XF86keysym.h
index fd3af4f..ff3dd4f 100644
--- a/common/rfb/XF86keysym.h
+++ b/common/rfb/XF86keysym.h
@@ -136,7 +136,7 @@
 #define XF86XK_LogOff		0x1008FF61   /* Log off system              */
 #define XF86XK_Market		0x1008FF62   /* ??                          */
 #define XF86XK_Meeting		0x1008FF63   /* enter meeting in calendar   */
-#define XF86XK_MenuKB		0x1008FF65   /* distingush keyboard from PB */
+#define XF86XK_MenuKB		0x1008FF65   /* distinguish keyboard from PB */
 #define XF86XK_MenuPB		0x1008FF66   /* distinuish PB from keyboard */
 #define XF86XK_MySites		0x1008FF67   /* Favourites                  */
 #define XF86XK_New		0x1008FF68   /* New (folder, document...    */
diff --git a/common/rfb/ZRLEDecoder.cxx b/common/rfb/ZRLEDecoder.cxx
index b891ba5..1dfb72a 100644
--- a/common/rfb/ZRLEDecoder.cxx
+++ b/common/rfb/ZRLEDecoder.cxx
@@ -1,4 +1,5 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
+ * Copyright 2009-2017 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
@@ -91,24 +92,27 @@
   case 16: zrleDecode16(r, &is, &zis, pf, pb); break;
   case 32:
     {
-      Pixel maxPixel = pf.pixelFromRGB((rdr::U16)-1, (rdr::U16)-1, (rdr::U16)-1);
-      bool fitsInLS3Bytes = maxPixel < (1<<24);
-      bool fitsInMS3Bytes = (maxPixel & 0xff) == 0;
+      if (pf.depth <= 24) {
+        Pixel maxPixel = pf.pixelFromRGB((rdr::U16)-1, (rdr::U16)-1, (rdr::U16)-1);
+        bool fitsInLS3Bytes = maxPixel < (1<<24);
+        bool fitsInMS3Bytes = (maxPixel & 0xff) == 0;
 
-      if ((fitsInLS3Bytes && pf.isLittleEndian()) ||
-          (fitsInMS3Bytes && pf.isBigEndian()))
-      {
-        zrleDecode24A(r, &is, &zis, pf, pb);
+        if ((fitsInLS3Bytes && pf.isLittleEndian()) ||
+            (fitsInMS3Bytes && pf.isBigEndian()))
+        {
+          zrleDecode24A(r, &is, &zis, pf, pb);
+          break;
+        }
+
+        if ((fitsInLS3Bytes && pf.isBigEndian()) ||
+            (fitsInMS3Bytes && pf.isLittleEndian()))
+        {
+          zrleDecode24B(r, &is, &zis, pf, pb);
+          break;
+        }
       }
-      else if ((fitsInLS3Bytes && pf.isBigEndian()) ||
-               (fitsInMS3Bytes && pf.isLittleEndian()))
-      {
-        zrleDecode24B(r, &is, &zis, pf, pb);
-      }
-      else
-      {
-        zrleDecode32(r, &is, &zis, pf, pb);
-      }
+
+      zrleDecode32(r, &is, &zis, pf, pb);
       break;
     }
   }
diff --git a/common/rfb/encodings.h b/common/rfb/encodings.h
index 5c6c5ea..a65d863 100644
--- a/common/rfb/encodings.h
+++ b/common/rfb/encodings.h
@@ -38,6 +38,7 @@
   const int pseudoEncodingDesktopName = -307;
   const int pseudoEncodingFence = -312;
   const int pseudoEncodingContinuousUpdates = -313;
+  const int pseudoEncodingCursorWithAlpha = -314;
 
   // TightVNC-specific
   const int pseudoEncodingLastRect = -224;
diff --git a/common/rfb/keysymdef.h b/common/rfb/keysymdef.h
index 979ebdd..a80f3f5 100644
--- a/common/rfb/keysymdef.h
+++ b/common/rfb/keysymdef.h
@@ -175,7 +175,7 @@
 
 
 /*
- * Auxilliary Functions; note the duplicate definitions for left and right
+ * Auxiliary Functions; note the duplicate definitions for left and right
  * function keys;  Sun keyboards and a few other manufactures have such
  * function key groups on the left and/or right sides of the keyboard.
  * We've not found a keyboard with more than 35 function keys total.
diff --git a/common/rfb/rreDecode.h b/common/rfb/rreDecode.h
index 56defbd..f9fdcfc 100644
--- a/common/rfb/rreDecode.h
+++ b/common/rfb/rreDecode.h
@@ -22,6 +22,7 @@
 // BPP                - 8, 16 or 32
 
 #include <rdr/InStream.h>
+#include <rfb/Exception.h>
 
 namespace rfb {
 
@@ -49,6 +50,10 @@
     int y = is->readU16();
     int w = is->readU16();
     int h = is->readU16();
+
+    if (((x+w) > r.width()) || ((y+h) > r.height()))
+      throw Exception ("RRE decode error");
+
     pb->fillRect(pf, Rect(r.tl.x+x, r.tl.y+y, r.tl.x+x+w, r.tl.y+y+h), &pix);
   }
 }
diff --git a/common/rfb/util.cxx b/common/rfb/util.cxx
index aec45f6..22e00ff 100644
--- a/common/rfb/util.cxx
+++ b/common/rfb/util.cxx
@@ -139,7 +139,7 @@
   static size_t doPrefix(long long value, const char *unit,
                          char *buffer, size_t maxlen,
                          unsigned divisor, const char **prefixes,
-                         size_t prefixCount) {
+                         size_t prefixCount, int precision) {
     double newValue;
     size_t prefix, len;
 
@@ -152,7 +152,7 @@
       prefix++;
     }
 
-    len = snprintf(buffer, maxlen, "%g %s%s", newValue,
+    len = snprintf(buffer, maxlen, "%.*g %s%s", precision, newValue,
                    (prefix == 0) ? "" : prefixes[prefix-1], unit);
     buffer[maxlen-1] = '\0';
 
@@ -165,14 +165,16 @@
     { "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi" };
 
   size_t siPrefix(long long value, const char *unit,
-                  char *buffer, size_t maxlen) {
+                  char *buffer, size_t maxlen, int precision) {
     return doPrefix(value, unit, buffer, maxlen, 1000, siPrefixes,
-                    sizeof(siPrefixes)/sizeof(*siPrefixes));
+                    sizeof(siPrefixes)/sizeof(*siPrefixes),
+                    precision);
   }
 
   size_t iecPrefix(long long value, const char *unit,
-                   char *buffer, size_t maxlen) {
+                   char *buffer, size_t maxlen, int precision) {
     return doPrefix(value, unit, buffer, maxlen, 1024, iecPrefixes,
-                    sizeof(iecPrefixes)/sizeof(*iecPrefixes));
+                    sizeof(iecPrefixes)/sizeof(*iecPrefixes),
+                    precision);
   }
 };
diff --git a/common/rfb/util.h b/common/rfb/util.h
index 9ad1772..e9114c3 100644
--- a/common/rfb/util.h
+++ b/common/rfb/util.h
@@ -99,9 +99,9 @@
   unsigned msSince(const struct timeval *then);
 
   size_t siPrefix(long long value, const char *unit,
-                  char *buffer, size_t maxlen);
+                  char *buffer, size_t maxlen, int precision=6);
   size_t iecPrefix(long long value, const char *unit,
-                   char *buffer, size_t maxlen);
+                   char *buffer, size_t maxlen, int precision=6);
 }
 
 // Some platforms (e.g. Windows) include max() and min() macros in their
diff --git a/contrib/packages/deb/ubuntu-xenial/debian/control b/contrib/packages/deb/ubuntu-xenial/debian/control
index 626efee..c063496 100644
--- a/contrib/packages/deb/ubuntu-xenial/debian/control
+++ b/contrib/packages/deb/ubuntu-xenial/debian/control
@@ -30,7 +30,7 @@
 Package: xtigervncviewer
 Architecture: any
 Provides: vncviewer, vnc-viewer
-Depends: libc6, libexpat1, libfontconfig1, libfreetype6, libgcc1, libgnutls30, libjpeg-turbo8, libp11-kit0, libpng12-0, libstdc++6, libtasn1-3-bin, libx11-6, libxau6, libxcb1, libxdmcp6, libxext6, libxft2, libxrender1, zlib1g, libglu1-mesa, libxcursor1, libxinerama1, libxfixes3, libfltk1.3
+Depends: libc6, libexpat1, libfontconfig1, libfreetype6, libgcc1, libgnutls30, libjpeg-turbo8, libp11-kit0, libpng12-0, libstdc++6, libtasn1-3-bin, libx11-6, libxau6, libxcb1, libxdmcp6, libxext6, libxft2, libxrender1, zlib1g, libglu1-mesa, libxcursor1, libxinerama1, libxfixes3, libfltk1.3, libfltk-images1.3
 Recommends: xfonts-base
 Suggests: tigervncserver, ssh
 Description: virtual network computing client software for X
diff --git a/java/com/tigervnc/network/SSLEngineManager.java b/java/com/tigervnc/network/SSLEngineManager.java
index c011099..e6abe3e 100644
--- a/java/com/tigervnc/network/SSLEngineManager.java
+++ b/java/com/tigervnc/network/SSLEngineManager.java
@@ -74,7 +74,7 @@
            hs != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
 
       switch (hs) {
-    
+
       case NEED_UNWRAP:
         // Receive handshaking data from peer
         peerNetData.flip();
@@ -92,26 +92,26 @@
             peerNetData.flip();
             peerNetData.compact();
             break;
-    
+
           case OK:
             // Process incoming handshaking data
             break;
-    
+
           case CLOSED:
             engine.closeInbound();
             break;
-    
+
         }
         break;
-    
+
       case NEED_WRAP:
         // Empty the local network packet buffer.
         myNetData.clear();
-    
+
         // Generate handshaking data
         res = engine.wrap(myAppData, myNetData);
         hs = res.getHandshakeStatus();
-    
+
         // Check status
         switch (res.getStatus()) {
           case OK:
@@ -121,15 +121,15 @@
             os.flush();
             myNetData.clear();
             break;
-    
+
           case BUFFER_OVERFLOW:
             // FIXME: How much larger should the buffer be?
             break;
-    
+
           case CLOSED:
             engine.closeOutbound();
             break;
-    
+
         }
         break;
 
diff --git a/java/com/tigervnc/network/TcpSocket.java b/java/com/tigervnc/network/TcpSocket.java
index 43787e4..95f16d3 100644
--- a/java/com/tigervnc/network/TcpSocket.java
+++ b/java/com/tigervnc/network/TcpSocket.java
@@ -32,6 +32,9 @@
 import java.nio.*;
 import java.nio.channels.*;
 
+import java.util.Set;
+import java.util.Iterator;
+
 public class TcpSocket extends Socket {
 
   // -=- Socket initialisation
@@ -77,23 +80,37 @@
     /* Attempt to connect to the remote host */
     try {
       result = sock.connect(new InetSocketAddress(addr, port));
+      Selector selector = Selector.open();
+      SelectionKey connect_key =
+        sock.socket().getChannel().register(selector, SelectionKey.OP_CONNECT);
+      // Try for the connection for 250ms
+      while (selector.select(250) > 0) {
+        while (!result) {
+
+          Set keys = selector.selectedKeys();
+          Iterator i = keys.iterator();
+
+          while (i.hasNext()) {
+            SelectionKey key = (SelectionKey)i.next();
+
+            // Remove the current key
+            i.remove();
+
+            // Attempt a connection
+            if (key.isConnectable()) {
+              if (sock.isConnectionPending())
+                sock.finishConnect();
+              result = true;
+            }
+          }
+        }
+      }
+      if (!result)
+        throw new SocketException("unable to connect to socket: Host is down");
     } catch(java.io.IOException e) {
       throw new SocketException("unable to connect:"+e.getMessage());
     }
 
-    if (!result && sock.isConnectionPending()) {
-      while (!result) {
-        try {
-          result = sock.finishConnect();
-        } catch(java.io.IOException e) {
-          throw new Exception(e.getMessage());
-        }
-      }
-    }
-
-    if (!result)
-      throw new SocketException("unable connect to socket");
-
     // Disable Nagle's algorithm, to reduce latency
     enableNagles(sock, false);
 
diff --git a/java/com/tigervnc/rdr/InStream.java b/java/com/tigervnc/rdr/InStream.java
index 3938a32..0906d32 100644
--- a/java/com/tigervnc/rdr/InStream.java
+++ b/java/com/tigervnc/rdr/InStream.java
@@ -23,6 +23,8 @@
 
 package com.tigervnc.rdr;
 
+import java.nio.*;
+
 import com.tigervnc.network.*;
 
 abstract public class InStream {
@@ -98,6 +100,16 @@
 
   // readBytes() reads an exact number of bytes into an array at an offset.
 
+  public void readBytes(ByteBuffer data, int length) {
+    int dataEnd = data.mark().position() + length;
+    while (data.position() < dataEnd) {
+      int n = check(1, dataEnd - data.position());
+      data.put(b, ptr, n);
+      ptr += n;
+    }
+    data.reset();
+  }
+
   public void readBytes(byte[] data, int dataPtr, int length) {
     int dataEnd = dataPtr + length;
     while (dataPtr < dataEnd) {
@@ -147,20 +159,6 @@
     }
   }
 
-  public final int readCompactLength() {
-    int b = readU8();
-    int result = b & 0x7F;
-    if ((b & 0x80) != 0) {
-      b = readU8();
-      result |= (b & 0x7F) << 7;
-      if ((b & 0x80) != 0) {
-        b = readU8();
-        result |= (b & 0xFF) << 14;
-      }
-    }
-    return result;
-  }
-
   // pos() returns the position in the stream.
 
   abstract public int pos();
diff --git a/java/com/tigervnc/rdr/OutStream.java b/java/com/tigervnc/rdr/OutStream.java
index 46fe734..a3b1a6c 100644
--- a/java/com/tigervnc/rdr/OutStream.java
+++ b/java/com/tigervnc/rdr/OutStream.java
@@ -116,6 +116,17 @@
     }
   }
 
+  // copyBytes() efficiently transfers data between streams
+
+  public void copyBytes(InStream is, int length) {
+    while (length > 0) {
+      int n = check(1, length);
+      is.readBytes(b, ptr, n);
+      ptr += n;
+      length -= n;
+    }
+  }
+
   // writeOpaqueN() writes a quantity without byte-swapping.  Because java has
   // no byte-ordering, we just use big-endian.
 
diff --git a/java/com/tigervnc/rdr/ZlibInStream.java b/java/com/tigervnc/rdr/ZlibInStream.java
index 103bb64..151633e 100644
--- a/java/com/tigervnc/rdr/ZlibInStream.java
+++ b/java/com/tigervnc/rdr/ZlibInStream.java
@@ -62,6 +62,18 @@
     ptr = end = start;
   }
 
+  public void removeUnderlying()
+  {
+    ptr = end = start;
+    if (underlying == null) return;
+
+    while (bytesIn > 0) {
+      decompress(true);
+      end = start; // throw away any data
+    }
+    underlying = null;
+  }
+
   public int pos()
   {
     return offset + ptr - start;
diff --git a/java/com/tigervnc/rfb/AliasParameter.java b/java/com/tigervnc/rfb/AliasParameter.java
index a1ae838..3f20ae4 100644
--- a/java/com/tigervnc/rfb/AliasParameter.java
+++ b/java/com/tigervnc/rfb/AliasParameter.java
@@ -44,5 +44,13 @@
     param.setImmutable();
   }
 
+  public void setHasBeenSet() {
+    param.setHasBeenSet();
+  }
+
+  public boolean hasBeenSet() {
+    return param.hasBeenSet();
+  }
+
   protected VoidParameter param;
 }
diff --git a/java/com/tigervnc/rfb/CConnection.java b/java/com/tigervnc/rfb/CConnection.java
index c354868..0b38aea 100644
--- a/java/com/tigervnc/rfb/CConnection.java
+++ b/java/com/tigervnc/rfb/CConnection.java
@@ -19,7 +19,11 @@
 
 package com.tigervnc.rfb;
 
+import java.awt.color.*;
+import java.awt.image.*;
+import java.nio.*;
 import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import com.tigervnc.network.*;
 import com.tigervnc.rdr.*;
@@ -28,21 +32,86 @@
 
   public CConnection()
   {
-    csecurity = null; is = null; os = null; reader_ = null;
-    writer_ = null; shared = false;
+    super();
+    csecurity = null; is = null; os = null; reader_ = null; writer_ = null;
+    shared = false;
     state_ = RFBSTATE_UNINITIALISED; useProtocol3_3 = false;
+    framebuffer = null; decoder = new DecodeManager(this);
     security = new SecurityClient();
   }
 
-  // deleteReaderAndWriter() deletes the reader and writer associated with
-  // this connection.  This may be useful if you want to delete the streams
-  // before deleting the SConnection to make sure that no attempt by the
-  // SConnection is made to read or write.
-  // XXX Do we really need this at all???
-  public void deleteReaderAndWriter()
+  // Methods to initialise the connection
+
+  // setServerName() is used to provide a unique(ish) name for the server to
+  // which we are connected.  This might be the result of getPeerEndpoint on
+  // a TcpSocket, for example, or a host specified by DNS name & port.
+  // The serverName is used when verifying the Identity of a host (see RA2).
+  public final void setServerName(String name) {
+    serverName = name;
+  }
+
+  // setShared sets the value of the shared flag which will be sent to the
+  // server upon initialisation.
+  public final void setShared(boolean s) { shared = s; }
+
+  // setProtocol3_3 configures whether or not the CConnection should
+  // only ever support protocol version 3.3
+  public final void setProtocol3_3(boolean s) { useProtocol3_3 = s; }
+
+  // setStreams() sets the streams to be used for the connection.  These must
+  // be set before initialiseProtocol() and processMsg() are called.  The
+  // CSecurity object may call setStreams() again to provide alternative
+  // streams over which the RFB protocol is sent (i.e. encrypting/decrypting
+  // streams).  Ownership of the streams remains with the caller
+  // (i.e. SConnection will not delete them).
+  public final void setStreams(InStream is_, OutStream os_)
   {
-    reader_ = null;
-    writer_ = null;
+    is = is_;
+    os = os_;
+  }
+
+  // setFramebuffer configures the PixelBuffer that the CConnection
+  // should render all pixel data in to. Note that the CConnection
+  // takes ownership of the PixelBuffer and it must not be deleted by
+  // anyone else. Call setFramebuffer again with NULL or a different
+  // PixelBuffer to delete the previous one.
+  public void setFramebuffer(ModifiablePixelBuffer fb)
+  {
+    decoder.flush();
+
+    if ((framebuffer != null) && (fb != null)) {
+      Rect rect = new Rect();
+
+      Raster data;
+
+      byte[] black = new byte[4];
+
+      // Copy still valid area
+
+      rect.setXYWH(0, 0,
+                   Math.min(fb.width(), framebuffer.width()),
+                   Math.min(fb.height(), framebuffer.height()));
+      data = framebuffer.getBuffer(rect);
+      fb.imageRect(framebuffer.getPF(), rect, data);
+
+      // Black out any new areas
+
+      if (fb.width() > framebuffer.width()) {
+        rect.setXYWH(framebuffer.width(), 0,
+                     fb.width() - framebuffer.width(),
+                     fb.height());
+        fb.fillRect(rect, black);
+      }
+
+      if (fb.height() > framebuffer.height()) {
+        rect.setXYWH(0, framebuffer.height(),
+                     fb.width(),
+                     fb.height() - framebuffer.height());
+        fb.fillRect(rect, black);
+      }
+    }
+
+    framebuffer = fb;
   }
 
   // initialiseProtocol() should be called once the streams and security
@@ -75,11 +144,12 @@
   private void processVersionMsg()
   {
     vlog.debug("reading protocol version");
-    if (!cp.readVersion(is)) {
+    AtomicBoolean done = new AtomicBoolean();
+    if (!cp.readVersion(is, done)) {
       state_ = RFBSTATE_INVALID;
       throw new Exception("reading version failed: not an RFB server?");
     }
-    if (!cp.done) return;
+    if (!done.get()) return;
 
     vlog.info("Server supports RFB protocol version "
               +cp.majorVersion+"."+ cp.minorVersion);
@@ -111,7 +181,7 @@
     int secType = Security.secTypeInvalid;
 
     List<Integer> secTypes = new ArrayList<Integer>();
-    secTypes = Security.GetEnabledSecTypes();
+    secTypes = security.GetEnabledSecTypes();
 
     if (cp.isVersion(3,3)) {
 
@@ -241,59 +311,56 @@
 
   private void securityCompleted() {
     state_ = RFBSTATE_INITIALISATION;
-    reader_ = new CMsgReaderV3(this, is);
-    writer_ = new CMsgWriterV3(cp, os);
+    reader_ = new CMsgReader(this, is);
+    writer_ = new CMsgWriter(cp, os);
     vlog.debug("Authentication success!");
     authSuccess();
     writer_.writeClientInit(shared);
   }
 
-  // Methods to initialise the connection
-
-  // setServerName() is used to provide a unique(ish) name for the server to
-  // which we are connected.  This might be the result of getPeerEndpoint on
-  // a TcpSocket, for example, or a host specified by DNS name & port.
-  // The serverName is used when verifying the Identity of a host (see RA2).
-  public final void setServerName(String name) {
-    serverName = name;
-  }
-
-  // setStreams() sets the streams to be used for the connection.  These must
-  // be set before initialiseProtocol() and processMsg() are called.  The
-  // CSecurity object may call setStreams() again to provide alternative
-  // streams over which the RFB protocol is sent (i.e. encrypting/decrypting
-  // streams).  Ownership of the streams remains with the caller
-  // (i.e. SConnection will not delete them).
-  public final void setStreams(InStream is_, OutStream os_)
-  {
-    is = is_;
-    os = os_;
-  }
-
-  // setShared sets the value of the shared flag which will be sent to the
-  // server upon initialisation.
-  public final void setShared(boolean s) { shared = s; }
-
-  // setProtocol3_3 configures whether or not the CConnection should
-  // only ever support protocol version 3.3
-  public final void setProtocol3_3(boolean s) { useProtocol3_3 = s; }
-
-  public void setServerPort(int port) {
-    serverPort = port;
-  }
-
-  public void initSecTypes() {
-    nSecTypes = 0;
-  }
-
   // Methods to be overridden in a derived class
 
+  // Note: These must be called by any deriving classes
+
+  public void setDesktopSize(int w, int h) {
+    decoder.flush();
+
+    super.setDesktopSize(w,h);
+  }
+
+  public void setExtendedDesktopSize(int reason,
+                                     int result, int w, int h,
+                                     ScreenSet layout) {
+    decoder.flush();
+
+    super.setExtendedDesktopSize(reason, result, w, h, layout);
+  }
+
   // getIdVerifier() returns the identity verifier associated with the connection.
   // Ownership of the IdentityVerifier is retained by the CConnection instance.
   //public IdentityVerifier getIdentityVerifier() { return 0; }
 
+  public void framebufferUpdateStart()
+  {
+    super.framebufferUpdateStart();
+  }
+
+  public void framebufferUpdateEnd()
+  {
+    decoder.flush();
+
+    super.framebufferUpdateEnd();
+  }
+
+  public void dataRect(Rect r, int encoding)
+  {
+    decoder.decodeRect(r, encoding, framebuffer);
+  }
+
   // authSuccess() is called when authentication has succeeded.
-  public void authSuccess() {}
+  public void authSuccess()
+  {
+  }
 
   // serverInit() is called when the ServerInit message is received.  The
   // derived class must call on to CConnection::serverInit().
@@ -303,34 +370,17 @@
     vlog.debug("initialisation done");
   }
 
-  // getCSecurity() gets the CSecurity object for the given type.  The type
-  // is guaranteed to be one of the secTypes passed in to addSecType().  The
-  // CSecurity object's destroy() method will be called by the CConnection
-  // from its destructor.
-  //abstract public CSecurity getCSecurity(int secType);
-
-  // getCurrentCSecurity() gets the CSecurity instance used for this
-  // connection.
-  //public CSecurity getCurrentCSecurity() { return security; }
-
-  // setClientSecTypeOrder() determines whether the client should obey the
-  // server's security type preference, by picking the first server security
-  // type that the client supports, or whether it should pick the first type
-  // that the server supports, from the client-supported list of types.
-  public void setClientSecTypeOrder( boolean csto ) {
-    clientSecTypeOrder = csto;
-  }
-
   // Other methods
 
-  public CMsgReaderV3 reader() { return reader_; }
-  public CMsgWriterV3 writer() { return writer_; }
+  public CMsgReader reader() { return reader_; }
+  public CMsgWriter writer() { return writer_; }
 
   public InStream getInStream() { return is; }
   public OutStream getOutStream() { return os; }
 
+  // Access method used by SSecurity implementations that can verify servers'
+  // Identities, to determine the unique(ish) name of the server.
   public String getServerName() { return serverName; }
-  public int getServerPort() { return serverPort; }
 
   public static final int RFBSTATE_UNINITIALISED = 0;
   public static final int RFBSTATE_PROTOCOL_VERSION = 1;
@@ -343,7 +393,17 @@
 
   public int state() { return state_; }
 
-  protected final void setState(int s) { state_ = s; }
+  public int getServerPort() { return serverPort; }
+  public void setServerPort(int port) {
+    serverPort = port;
+  }
+
+  protected void setState(int s) { state_ = s; }
+
+  protected void setReader(CMsgReader r) { reader_ = r; }
+  protected void setWriter(CMsgWriter w) { writer_ = w; }
+
+  protected ModifiablePixelBuffer getFramebuffer() { return framebuffer; }
 
   public void fence(int flags, int len, byte[] data)
   {
@@ -373,20 +433,27 @@
     throw new AuthFailureException(reason);
   }
 
-  InStream is;
-  OutStream os;
-  CMsgReaderV3 reader_;
-  CMsgWriterV3 writer_;
-  boolean shared;
+  private InStream is;
+  private OutStream os;
+  private CMsgReader reader_;
+  private CMsgWriter writer_;
+  private boolean deleteStreamsWhenDone;
+  private boolean shared;
+  private int state_ = RFBSTATE_UNINITIALISED;
+
+  private String serverName;
+
+  private boolean useProtocol3_3;
+
+  protected ModifiablePixelBuffer framebuffer;
+  private DecodeManager decoder;
+
   public CSecurity csecurity;
   public SecurityClient security;
   public static final int maxSecTypes = 8;
   int nSecTypes;
   int[] secTypes;
-  int state_ = RFBSTATE_UNINITIALISED;
-  String serverName;
   int serverPort;
-  boolean useProtocol3_3;
   boolean clientSecTypeOrder;
 
   static LogWriter vlog = new LogWriter("CConnection");
diff --git a/java/com/tigervnc/rfb/CMsgHandler.java b/java/com/tigervnc/rfb/CMsgHandler.java
index dd9767e..9940598 100644
--- a/java/com/tigervnc/rfb/CMsgHandler.java
+++ b/java/com/tigervnc/rfb/CMsgHandler.java
@@ -78,25 +78,18 @@
                              String x509subject) {}
 
   public void setCursor(int width, int height, Point hotspot,
-                        int[] data, byte[] mask) {}
+                        byte[] data, byte[] mask) {}
   public void serverInit() {}
 
   public void framebufferUpdateStart() {}
   public void framebufferUpdateEnd() {}
-  public void beginRect(Rect r, int encoding) {}
-  public void endRect(Rect r, int encoding) {}
+  public void dataRect(Rect r, int encoding) {}
 
   public void setColourMapEntries(int firstColour, int nColours,
     int[] rgbs) { }
   public void bell() {}
   public void serverCutText(String str, int len) {}
 
-  public void fillRect(Rect r, int pix) {}
-  public void imageRect(Rect r, Object pixels) {}
-  public void copyRect(Rect r, int srcX, int srcY) {}
-
-  abstract public PixelFormat getPreferredPF();
-
   public ConnParams cp;
 
   static LogWriter vlog = new LogWriter("CMsgHandler");
diff --git a/java/com/tigervnc/rfb/CMsgReader.java b/java/com/tigervnc/rfb/CMsgReader.java
index a93324c..e9cad89 100644
--- a/java/com/tigervnc/rfb/CMsgReader.java
+++ b/java/com/tigervnc/rfb/CMsgReader.java
@@ -28,7 +28,7 @@
 import java.nio.charset.Charset;
 import com.tigervnc.rdr.*;
 
-abstract public class CMsgReader {
+public class CMsgReader {
 
   protected CMsgReader(CMsgHandler handler_, InStream is_)
   {
@@ -37,7 +37,86 @@
     is = is_;
     imageBuf = null;
     imageBufSize = 0;
-    decoders = new Decoder[Encodings.encodingMax+1];
+    nUpdateRectsLeft = 0;
+  }
+
+  public void readServerInit()
+  {
+    int width = is.readU16();
+    int height = is.readU16();
+    handler.setDesktopSize(width, height);
+    PixelFormat pf = new PixelFormat();
+    pf.read(is);
+    handler.setPixelFormat(pf);
+    String name = is.readString();
+    handler.setName(name);
+    handler.serverInit();
+  }
+
+  public void readMsg()
+  {
+    if (nUpdateRectsLeft == 0) {
+      int type = is.readU8();
+
+      switch (type) {
+      case MsgTypes.msgTypeSetColourMapEntries:
+        readSetColourMapEntries();
+        break;
+      case MsgTypes.msgTypeBell:
+        readBell();
+        break;
+      case MsgTypes.msgTypeServerCutText:
+        readServerCutText();
+        break;
+      case MsgTypes.msgTypeFramebufferUpdate:
+        readFramebufferUpdate();
+        break;
+      case MsgTypes.msgTypeServerFence:
+        readFence();
+        break;
+      case MsgTypes.msgTypeEndOfContinuousUpdates:
+        readEndOfContinuousUpdates();
+        break;
+      default:
+        //fprintf(stderr, "unknown message type %d\n", type);
+        throw new Exception("unknown message type");
+      }
+    } else {
+      int x = is.readU16();
+      int y = is.readU16();
+      int w = is.readU16();
+      int h = is.readU16();
+      int encoding = is.readS32();
+
+      switch (encoding) {
+      case Encodings.pseudoEncodingLastRect:
+        nUpdateRectsLeft = 1;     // this rectangle is the last one
+        break;
+      case Encodings.pseudoEncodingCursor:
+        readSetCursor(w, h, new Point(x,y));
+        break;
+      case Encodings.pseudoEncodingDesktopName:
+        readSetDesktopName(x, y, w, h);
+        break;
+      case Encodings.pseudoEncodingDesktopSize:
+        handler.setDesktopSize(w, h);
+        break;
+      case Encodings.pseudoEncodingExtendedDesktopSize:
+        readExtendedDesktopSize(x, y, w, h);
+        break;
+      case Encodings.pseudoEncodingClientRedirect:
+        nUpdateRectsLeft = 0;
+        readClientRedirect(x, y, w, h);
+        return;
+      default:
+        readRect(new Rect(x, y, x+w, y+h), encoding);
+        break;
+      };
+
+      nUpdateRectsLeft--;
+      if (nUpdateRectsLeft == 0)
+        handler.framebufferUpdateEnd();
+    }
   }
 
   protected void readSetColourMapEntries()
@@ -72,6 +151,43 @@
     handler.serverCutText(chars.toString(), len);
   }
 
+  protected void readFence()
+  {
+    int flags;
+    int len;
+    byte[] data = new byte[64];
+
+    is.skip(3);
+
+    flags = is.readU32();
+
+    len = is.readU8();
+    if (len > data.length) {
+      System.out.println("Ignoring fence with too large payload\n");
+      is.skip(len);
+      return;
+    }
+
+    is.readBytes(data, 0, len);
+
+    handler.fence(flags, len, data);
+  }
+
+  protected void readEndOfContinuousUpdates()
+  {
+    handler.endOfContinuousUpdates();
+  }
+
+  protected void readFramebufferUpdate()
+  {
+    is.skip(1);
+    nUpdateRectsLeft = is.readU16();
+    handler.framebufferUpdateStart();
+  }
+
+
+
+  /*
   protected void readFramebufferUpdateStart()
   {
     handler.framebufferUpdateStart();
@@ -81,6 +197,7 @@
   {
     handler.framebufferUpdateEnd();
   }
+  */
 
   protected void readRect(Rect r, int encoding)
   {
@@ -94,45 +211,70 @@
     if (r.is_empty())
       vlog.error("Ignoring zero size rect");
 
-    handler.beginRect(r, encoding);
-
-    if (encoding == Encodings.encodingCopyRect) {
-      readCopyRect(r);
-    } else {
-
-      if (decoders[encoding] == null) {
-        decoders[encoding] = Decoder.createDecoder(encoding, this);
-        if (decoders[encoding] == null) {
-          vlog.error("Unknown rect encoding "+encoding);
-          throw new Exception("Unknown rect encoding");
-        }
-      }
-      decoders[encoding].readRect(r, handler);
-    }
-
-    handler.endRect(r, encoding);
-  }
-
-  protected void readCopyRect(Rect r)
-  {
-    int srcX = is.readU16();
-    int srcY = is.readU16();
-    handler.copyRect(r, srcX, srcY);
+    handler.dataRect(r, encoding);
   }
 
   protected void readSetCursor(int width, int height, Point hotspot)
   {
-    int data_len = width * height;
+    int data_len = width * height * (handler.cp.pf().bpp/8);
     int mask_len = ((width+7)/8) * height;
-    int[] data = new int[data_len];
+    byte[] data = new byte[data_len];
     byte[] mask = new byte[mask_len];
 
-    is.readPixels(data, data_len, (handler.cp.pf().bpp/8), handler.cp.pf().bigEndian);
+    is.readBytes(data, 0, data_len);
     is.readBytes(mask, 0, mask_len);
 
     handler.setCursor(width, height, hotspot, data, mask);
   }
 
+  protected void readSetDesktopName(int x, int y, int w, int h)
+  {
+    String name = is.readString();
+
+    if (x != 0 || y != 0 || w != 0 || h != 0) {
+      vlog.error("Ignoring DesktopName rect with non-zero position/size");
+    } else {
+      handler.setName(name);
+    }
+
+  }
+
+  protected void readExtendedDesktopSize(int x, int y, int w, int h)
+  {
+    int screens, i;
+    int id, flags;
+    int sx, sy, sw, sh;
+    ScreenSet layout = new ScreenSet();
+
+    screens = is.readU8();
+    is.skip(3);
+
+    for (i = 0;i < screens;i++) {
+      id = is.readU32();
+      sx = is.readU16();
+      sy = is.readU16();
+      sw = is.readU16();
+      sh = is.readU16();
+      flags = is.readU32();
+
+      layout.add_screen(new Screen(id, sx, sy, sw, sh, flags));
+    }
+
+    handler.setExtendedDesktopSize(x, y, w, h, layout);
+  }
+
+  protected void readClientRedirect(int x, int y, int w, int h)
+  {
+    int port = is.readU16();
+    String host = is.readString();
+    String x509subject = is.readString();
+
+    if (x != 0 || y != 0 || w != 0 || h != 0)
+      vlog.error("Ignoring ClientRedirect rect with non-zero position/size");
+    else
+      handler.clientRedirect(port, host, x509subject);
+  }
+
   public int[] getImageBuf(int required) { return getImageBuf(required, 0, 0); }
 
   public int[] getImageBuf(int required, int requested, int nPixels)
@@ -154,23 +296,13 @@
     return imageBuf;
   }
 
-  public final int bpp()
-  {
-    return handler.cp.pf().bpp;
-  }
-
-  abstract public void readServerInit();
-
-  // readMsg() reads a message, calling the handler as appropriate.
-  abstract public void readMsg();
-
   public InStream getInStream() { return is; }
 
   public int imageBufIdealSize;
 
   protected CMsgHandler handler;
   protected InStream is;
-  protected Decoder[] decoders;
+  protected int nUpdateRectsLeft;
   protected int[] imageBuf;
   protected int imageBufSize;
 
diff --git a/java/com/tigervnc/rfb/CMsgReaderV3.java b/java/com/tigervnc/rfb/CMsgReaderV3.java
deleted file mode 100644
index e09d3bb..0000000
--- a/java/com/tigervnc/rfb/CMsgReaderV3.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright 2009-2011 Pierre Ossman for Cendio AB
- * Copyright (C) 2011 Brian P. Hinz
- *
- * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
- * USA.
- */
-
-package com.tigervnc.rfb;
-
-import com.tigervnc.rdr.*;
-
-public class CMsgReaderV3 extends CMsgReader {
-
-  public CMsgReaderV3(CMsgHandler handler_, InStream is_)
-  {
-    super(handler_, is_);
-    nUpdateRectsLeft = 0;
-  }
-
-  public void readServerInit()
-  {
-    int width = is.readU16();
-    int height = is.readU16();
-    handler.setDesktopSize(width, height);
-    PixelFormat pf = new PixelFormat();
-    pf.read(is);
-    handler.setPixelFormat(pf);
-    String name = is.readString();
-    handler.setName(name);
-    handler.serverInit();
-  }
-
-  public void readMsg()
-  {
-    if (nUpdateRectsLeft == 0) {
-
-      int type = is.readU8();
-      switch (type) {
-      case MsgTypes.msgTypeFramebufferUpdate:   readFramebufferUpdate(); break;
-      case MsgTypes.msgTypeSetColourMapEntries: readSetColourMapEntries(); break;
-      case MsgTypes.msgTypeBell:                readBell(); break;
-      case MsgTypes.msgTypeServerCutText:       readServerCutText(); break;
-      case MsgTypes.msgTypeServerFence:         readFence(); break;
-      case MsgTypes.msgTypeEndOfContinuousUpdates:  readEndOfContinuousUpdates(); break;
-      default:
-        vlog.error("unknown message type "+type);
-        throw new Exception("unknown message type");
-      }
-
-    } else {
-
-      int x = is.readU16();
-      int y = is.readU16();
-      int w = is.readU16();
-      int h = is.readU16();
-      int encoding = is.readS32();
-
-      switch (encoding) {
-      case Encodings.pseudoEncodingDesktopSize:
-        handler.setDesktopSize(w, h);
-        break;
-      case Encodings.pseudoEncodingExtendedDesktopSize:
-        readExtendedDesktopSize(x, y, w, h);
-        break;
-      case Encodings.pseudoEncodingDesktopName:
-        readSetDesktopName(x, y, w, h);
-        break;
-      case Encodings.pseudoEncodingCursor:
-        readSetCursor(w, h, new Point(x,y));
-        break;
-      case Encodings.pseudoEncodingLastRect:
-        nUpdateRectsLeft = 1;     // this rectangle is the last one
-        break;
-      case Encodings.pseudoEncodingClientRedirect:
-        readClientRedirect(x, y, w, h);
-        break;
-      default:
-        readRect(new Rect(x, y, x+w, y+h), encoding);
-        break;
-      }
-
-      nUpdateRectsLeft--;
-      if (nUpdateRectsLeft == 0) handler.framebufferUpdateEnd();
-    }
-  }
-
-  void readFramebufferUpdate()
-  {
-    is.skip(1);
-    nUpdateRectsLeft = is.readU16();
-    handler.framebufferUpdateStart();
-  }
-
-  void readSetDesktopName(int x, int y, int w, int h)
-  {
-    String name = is.readString();
-
-    if (x != 0 || y != 0 || w != 0 || h != 0) {
-      vlog.error("Ignoring DesktopName rect with non-zero position/size");
-    } else {
-      handler.setName(name);
-    }
-
-  }
-
-  void readExtendedDesktopSize(int x, int y, int w, int h)
-  {
-    int screens, i;
-    int id, flags;
-    int sx, sy, sw, sh;
-    ScreenSet layout = new ScreenSet();
-
-    screens = is.readU8();
-    is.skip(3);
-
-    for (i = 0;i < screens;i++) {
-      id = is.readU32();
-      sx = is.readU16();
-      sy = is.readU16();
-      sw = is.readU16();
-      sh = is.readU16();
-      flags = is.readU32();
-
-      layout.add_screen(new Screen(id, sx, sy, sw, sh, flags));
-    }
-
-    handler.setExtendedDesktopSize(x, y, w, h, layout);
-  }
-
-  void readFence()
-  {
-    int flags;
-    int len;
-    byte[] data = new byte[64];
-
-    is.skip(3);
-
-    flags = is.readU32();
-
-    len = is.readU8();
-    if (len > data.length) {
-      System.out.println("Ignoring fence with too large payload\n");
-      is.skip(len);
-      return;
-    }
-
-    is.readBytes(data, 0, len);
-
-    handler.fence(flags, len, data);
-  }
-
-  void readEndOfContinuousUpdates()
-  {
-    handler.endOfContinuousUpdates();
-  }
-
-  void readClientRedirect(int x, int y, int w, int h)
-  {
-    int port = is.readU16();
-    String host = is.readString();
-    String x509subject = is.readString();
-
-    if (x != 0 || y != 0 || w != 0 || h != 0) {
-      vlog.error("Ignoring ClientRedirect rect with non-zero position/size");
-    } else {
-      handler.clientRedirect(port, host, x509subject);
-    }
-  }
-
-  int nUpdateRectsLeft;
-
-  static LogWriter vlog = new LogWriter("CMsgReaderV3");
-}
diff --git a/java/com/tigervnc/rfb/CMsgWriter.java b/java/com/tigervnc/rfb/CMsgWriter.java
index 0d3dccf..bdf50c2 100644
--- a/java/com/tigervnc/rfb/CMsgWriter.java
+++ b/java/com/tigervnc/rfb/CMsgWriter.java
@@ -20,13 +20,23 @@
 
 package com.tigervnc.rfb;
 
+import com.tigervnc.rdr.*;
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
-import com.tigervnc.rdr.*;
+import java.util.Iterator;
 
-abstract public class CMsgWriter {
+public class CMsgWriter {
 
-  abstract public void writeClientInit(boolean shared);
+  protected CMsgWriter(ConnParams cp_, OutStream os_)
+  {
+    cp = cp_;
+    os = os_;
+  }
+
+  synchronized public void writeClientInit(boolean shared) {
+    os.writeU8(shared?1:0);
+    endMsg();
+  }
 
   synchronized public void writeSetPixelFormat(PixelFormat pf)
   {
@@ -53,6 +63,7 @@
   {
     int nEncodings = 0;
     int[] encodings = new int[Encodings.encodingMax+3];
+
     if (cp.supportsLocalCursor)
       encodings[nEncodings++] = Encodings.pseudoEncodingCursor;
     if (cp.supportsDesktopResize)
@@ -68,7 +79,6 @@
     encodings[nEncodings++] = Encodings.pseudoEncodingContinuousUpdates;
     encodings[nEncodings++] = Encodings.pseudoEncodingFence;
 
-
     if (Decoder.supported(preferredEncoding)) {
       encodings[nEncodings++] = preferredEncoding;
     }
@@ -98,9 +108,11 @@
     // Remaining encodings
     for (int i = Encodings.encodingMax; i >= 0; i--) {
       switch (i) {
+      case Encodings.encodingCopyRect:
       case Encodings.encodingTight:
       case Encodings.encodingZRLE:
       case Encodings.encodingHextile:
+        /* These have already been sent earlier */
         break;
       default:
         if ((i != preferredEncoding) && Decoder.supported(i))
@@ -108,15 +120,44 @@
       }
     }
 
-    encodings[nEncodings++] = Encodings.pseudoEncodingLastRect;
-    if (cp.customCompressLevel && cp.compressLevel >= 0 && cp.compressLevel <= 9)
-      encodings[nEncodings++] = Encodings.pseudoEncodingCompressLevel0 + cp.compressLevel;
-    if (!cp.noJpeg && cp.qualityLevel >= 0 && cp.qualityLevel <= 9)
-      encodings[nEncodings++] = Encodings.pseudoEncodingQualityLevel0 + cp.qualityLevel;
+    if (cp.compressLevel >= 0 && cp.compressLevel <= 9)
+      encodings[nEncodings++] =
+        Encodings.pseudoEncodingCompressLevel0 + cp.compressLevel;
+    if (cp.qualityLevel >= 0 && cp.qualityLevel <= 9)
+      encodings[nEncodings++] =
+        Encodings.pseudoEncodingQualityLevel0 + cp.qualityLevel;
 
     writeSetEncodings(nEncodings, encodings);
   }
 
+  synchronized public void writeSetDesktopSize(int width, int height,
+                                               ScreenSet layout)
+	{
+	  if (!cp.supportsSetDesktopSize)
+	    throw new Exception("Server does not support SetDesktopSize");
+
+	  startMsg(MsgTypes.msgTypeSetDesktopSize);
+	  os.pad(1);
+
+	  os.writeU16(width);
+	  os.writeU16(height);
+
+	  os.writeU8(layout.num_screens());
+	  os.pad(1);
+
+    for (Iterator<Screen> iter = layout.screens.iterator(); iter.hasNext(); ) {
+      Screen refScreen = (Screen)iter.next();
+	    os.writeU32(refScreen.id);
+	    os.writeU16(refScreen.dimensions.tl.x);
+	    os.writeU16(refScreen.dimensions.tl.y);
+	    os.writeU16(refScreen.dimensions.width());
+	    os.writeU16(refScreen.dimensions.height());
+	    os.writeU32(refScreen.flags);
+	  }
+
+	  endMsg();
+	}
+
   synchronized public void writeFramebufferUpdateRequest(Rect r, boolean incremental)
   {
     startMsg(MsgTypes.msgTypeFramebufferUpdateRequest);
@@ -128,6 +169,44 @@
     endMsg();
   }
 
+  synchronized public void writeEnableContinuousUpdates(boolean enable,
+                                           int x, int y, int w, int h)
+  {
+    if (!cp.supportsContinuousUpdates)
+      throw new Exception("Server does not support continuous updates");
+
+    startMsg(MsgTypes.msgTypeEnableContinuousUpdates);
+
+    os.writeU8((enable?1:0));
+
+    os.writeU16(x);
+    os.writeU16(y);
+    os.writeU16(w);
+    os.writeU16(h);
+
+    endMsg();
+  }
+
+  synchronized public void writeFence(int flags, int len, byte[] data)
+  {
+    if (!cp.supportsFence)
+      throw new Exception("Server does not support fences");
+    if (len > 64)
+      throw new Exception("Too large fence payload");
+    if ((flags & ~fenceTypes.fenceFlagsSupported) != 0)
+      throw new Exception("Unknown fence flags");
+
+    startMsg(MsgTypes.msgTypeClientFence);
+    os.pad(3);
+
+    os.writeU32(flags);
+
+    os.writeU8(len);
+    os.writeBytes(data, 0, len);
+
+    endMsg();
+  }
+
   synchronized public void writeKeyEvent(int key, boolean down)
   {
     startMsg(MsgTypes.msgTypeKeyEvent);
@@ -163,17 +242,14 @@
     endMsg();
   }
 
-  abstract public void startMsg(int type);
-  abstract public void endMsg();
+  synchronized public void startMsg(int type) {
+    os.writeU8(type);
+  }
 
-  synchronized public void setOutStream(OutStream os_) { os = os_; }
-
-  ConnParams getConnParams() { return cp; }
-  OutStream getOutStream() { return os; }
-
-  protected CMsgWriter(ConnParams cp_, OutStream os_) {cp = cp_; os = os_;}
+  synchronized public void endMsg() {
+    os.flush();
+  }
 
   ConnParams cp;
   OutStream os;
-  static LogWriter vlog = new LogWriter("CMsgWriter");
 }
diff --git a/java/com/tigervnc/rfb/CMsgWriterV3.java b/java/com/tigervnc/rfb/CMsgWriterV3.java
deleted file mode 100644
index 10c377a..0000000
--- a/java/com/tigervnc/rfb/CMsgWriterV3.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright 2009-2011 Pierre Ossman for Cendio AB
- * Copyright (C) 2011 Brian P. Hinz
- *
- * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
- * USA.
- */
-
-package com.tigervnc.rfb;
-
-import com.tigervnc.rdr.*;
-import java.util.*;
-
-public class CMsgWriterV3 extends CMsgWriter {
-
-  public CMsgWriterV3(ConnParams cp_, OutStream os_) { super(cp_, os_); }
-
-  synchronized public void writeClientInit(boolean shared) {
-    os.writeU8(shared?1:0);
-    endMsg();
-  }
-
-  synchronized public void startMsg(int type) {
-    os.writeU8(type);
-  }
-
-  synchronized public void endMsg() {
-    os.flush();
-  }
-
-  synchronized public void writeSetDesktopSize(int width, int height,
-                                  ScreenSet layout)
-	{
-	  if (!cp.supportsSetDesktopSize)
-	    throw new Exception("Server does not support SetDesktopSize");
-
-	  startMsg(MsgTypes.msgTypeSetDesktopSize);
-	  os.pad(1);
-
-	  os.writeU16(width);
-	  os.writeU16(height);
-
-	  os.writeU8(layout.num_screens());
-	  os.pad(1);
-
-    for (Iterator<Screen> iter = layout.screens.iterator(); iter.hasNext(); ) {
-      Screen refScreen = (Screen)iter.next();
-	    os.writeU32(refScreen.id);
-	    os.writeU16(refScreen.dimensions.tl.x);
-	    os.writeU16(refScreen.dimensions.tl.y);
-	    os.writeU16(refScreen.dimensions.width());
-	    os.writeU16(refScreen.dimensions.height());
-	    os.writeU32(refScreen.flags);
-	  }
-
-	  endMsg();
-	}
-
-  synchronized public void writeFence(int flags, int len, byte[] data)
-  {
-    if (!cp.supportsFence)
-      throw new Exception("Server does not support fences");
-    if (len > 64)
-      throw new Exception("Too large fence payload");
-    if ((flags & ~fenceTypes.fenceFlagsSupported) != 0)
-      throw new Exception("Unknown fence flags");
-
-    startMsg(MsgTypes.msgTypeClientFence);
-    os.pad(3);
-
-    os.writeU32(flags);
-
-    os.writeU8(len);
-    os.writeBytes(data, 0, len);
-
-    endMsg();
-  }
-
-  synchronized public void writeEnableContinuousUpdates(boolean enable,
-                                           int x, int y, int w, int h)
-  {
-    if (!cp.supportsContinuousUpdates)
-      throw new Exception("Server does not support continuous updates");
-
-    startMsg(MsgTypes.msgTypeEnableContinuousUpdates);
-
-    os.writeU8((enable?1:0));
-
-    os.writeU16(x);
-    os.writeU16(y);
-    os.writeU16(w);
-    os.writeU16(h);
-
-    endMsg();
-  }
-}
diff --git a/java/com/tigervnc/rfb/CSecurityTLS.java b/java/com/tigervnc/rfb/CSecurityTLS.java
index a8f6df3..4b20e0b 100644
--- a/java/com/tigervnc/rfb/CSecurityTLS.java
+++ b/java/com/tigervnc/rfb/CSecurityTLS.java
@@ -58,11 +58,11 @@
 
 public class CSecurityTLS extends CSecurity {
 
-  public static StringParameter x509ca
-  = new StringParameter("x509ca",
+  public static StringParameter X509CA
+  = new StringParameter("X509CA",
                         "X509 CA certificate", "", Configuration.ConfigurationObject.ConfViewer);
-  public static StringParameter x509crl
-  = new StringParameter("x509crl",
+  public static StringParameter X509CRL
+  = new StringParameter("X509CRL",
                         "X509 CRL file", "", Configuration.ConfigurationObject.ConfViewer);
 
   private void initGlobal()
@@ -80,8 +80,8 @@
     manager = null;
 
     setDefaults();
-    cafile = x509ca.getData();
-    crlfile = x509crl.getData();
+    cafile = X509CA.getData();
+    crlfile = X509CRL.getData();
   }
 
   public static String getDefaultCA() {
@@ -99,9 +99,9 @@
   public static void setDefaults()
   {
     if (new File(getDefaultCA()).exists())
-      x509ca.setDefaultStr(getDefaultCA());
+      X509CA.setDefaultStr(getDefaultCA());
     if (new File(getDefaultCRL()).exists())
-      x509crl.setDefaultStr(getDefaultCRL());
+      X509CRL.setDefaultStr(getDefaultCRL());
   }
 
 // FIXME:
diff --git a/java/com/tigervnc/rfb/CSecurityVeNCrypt.java b/java/com/tigervnc/rfb/CSecurityVeNCrypt.java
index f353874..179900a 100644
--- a/java/com/tigervnc/rfb/CSecurityVeNCrypt.java
+++ b/java/com/tigervnc/rfb/CSecurityVeNCrypt.java
@@ -134,7 +134,7 @@
           Iterator<Integer> j;
           List<Integer> secTypes = new ArrayList<Integer>();
 
-          secTypes = Security.GetEnabledExtSecTypes();
+          secTypes = security.GetEnabledExtSecTypes();
 
           /* Honor server's security type order */
           for (i = 0; i < nAvailableTypes; i++) {
diff --git a/java/com/tigervnc/rfb/Configuration.java b/java/com/tigervnc/rfb/Configuration.java
index 5d140d9..11fc89a 100644
--- a/java/com/tigervnc/rfb/Configuration.java
+++ b/java/com/tigervnc/rfb/Configuration.java
@@ -26,14 +26,19 @@
 
 import java.io.FileInputStream;
 import java.io.PrintWriter;
+import java.lang.reflect.Field;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.*;
 
+import com.tigervnc.vncviewer.VncViewer;
+
 public class Configuration {
 
   static LogWriter vlog = new LogWriter("Configuration");
 
+  private static final String IDENTIFIER_STRING = "TigerVNC Configuration file Version 1.0";
+
   public enum ConfigurationObject { ConfGlobal, ConfServer, ConfViewer };
 
   // -=- The Global/server/viewer Configuration objects
@@ -259,77 +264,6 @@
     list(79, 10);
   }
 
-  public void readAppletParams(java.applet.Applet applet) {
-    VoidParameter current = head;
-    while (current != null) {
-      String str = applet.getParameter(current.getName());
-      if (str != null)
-        current.setParam(str);
-      current = current._next;
-    }
-  }
-
-  public static void load(String filename) {
-    if (filename == null)
-      return;
-
-    /* Read parameters from file */
-    Properties props = new Properties();
-    try {
-      props.load(new FileInputStream(filename));
-    } catch(java.security.AccessControlException e)	{
-      vlog.error("Cannot access system properties:"+e.getMessage());
-      return;
-    } catch (java.lang.Exception e)	{
-      vlog.error("Error opening config file:"+e.getMessage());
-      return;
-    }
-
-    for (Iterator<String> i = props.stringPropertyNames().iterator(); i.hasNext();) {
-      String name = (String)i.next();
-      if (name.startsWith("[")) {
-        // skip the section delimiters
-        continue;
-      } else if (name.equals("host")) {
-        setParam("Server", props.getProperty(name));
-      } else if (name.equals("disableclipboard")) {
-        setParam("RecvClipboard", props.getProperty(name));
-        setParam("SendClipboard", props.getProperty(name));
-      } else if (name.equals("localcursor")) {
-        setParam("UseLocalCursor", props.getProperty(name));
-      } else {
-        if (!setParam(name, props.getProperty(name)))
-          vlog.debug("Cannot set parameter: "+name);
-      }
-    }
-  }
-
-  public static void save(String filename) {
-    PrintWriter pw = null;
-    try {
-      pw = new PrintWriter(filename, "UTF-8");
-    } catch (java.lang.Exception e)	{
-      vlog.error("Error opening config file:"+e.getMessage());
-      return;
-    }
-
-    pw.println("# TigerVNC viewer configuration");
-    DateFormat dateFormat = new SimpleDateFormat("E MMM d k:m:s z yyyy");
-    Date date = new Date();
-    pw.println("# "+dateFormat.format(date));
-    VoidParameter current = Configuration.global().head;
-    while (current != null) {
-      String name = current.getName();
-      String value = current.getValueStr();
-      if (!name.equals("Server") && !name.equals("Port") &&
-          value != null && value != current.getDefaultStr())
-        pw.println(name+"="+current.getValueStr());
-      current = current._next;
-    }
-    pw.flush();
-    pw.close();
-  }
-
   // Name for this Configuration
   private String name;
 
diff --git a/java/com/tigervnc/rfb/ConnParams.java b/java/com/tigervnc/rfb/ConnParams.java
index f1f5395..fe52770 100644
--- a/java/com/tigervnc/rfb/ConnParams.java
+++ b/java/com/tigervnc/rfb/ConnParams.java
@@ -1,6 +1,6 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
  * Copyright (C) 2011 D. R. Commander.  All Rights Reserved.
- * Copyright (C) 2012 Brian P. Hinz
+ * Copyright (C) 2012-2016 Brian P. Hinz
  *
  * This is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,11 +21,21 @@
 package com.tigervnc.rfb;
 
 import com.tigervnc.rdr.*;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public class ConnParams {
-  static LogWriter vlog = new LogWriter("ConnParams");
 
-  public ConnParams() {
+  private static final int subsampleUndefined = -1;
+  private static final int subsampleNone = 0;
+  private static final int subsampleGray = 1;
+  private static final int subsample2X = 2;
+  private static final int subsample4X = 3;
+  private static final int subsample8X = 4;
+  private static final int subsample16X = 5;
+
+  public ConnParams()
+  {
     majorVersion = 0; minorVersion = 0;
     width = 0; height = 0; useCopyRect = false;
     supportsLocalCursor = false; supportsLocalXCursor = false;
@@ -34,19 +44,17 @@
     supportsSetDesktopSize = false; supportsFence = false;
     supportsContinuousUpdates = false;
     supportsClientRedirect = false;
-    customCompressLevel = false; compressLevel = 6;
-    noJpeg = false; qualityLevel = -1; fineQualityLevel = -1;
-    subsampling = "SUBSAMP_UNDEFINED";
-    name_ = null; nEncodings_ = 0; encodings_ = null;
-    currentEncoding_ = Encodings.encodingRaw; verStrPos = 0;
+    compressLevel = 6; qualityLevel = -1; fineQualityLevel = -1;
+    subsampling = subsampleUndefined; name_ = null; verStrPos = 0;
+
+    encodings_ = new ArrayList();
     screenLayout = new ScreenSet();
 
     setName("");
   }
 
-  public boolean readVersion(InStream is)
+  public boolean readVersion(InStream is, AtomicBoolean done)
   {
-    done = false;
     if (verStrPos >= 12) return false;
     verStr = new StringBuilder(13);
     while (is.checkNoWait(1) && verStrPos < 12) {
@@ -54,10 +62,10 @@
     }
 
     if (verStrPos < 12) {
-      done = false;
+      done.set(false);
       return true;
     }
-    done = true;
+    done.set(true);
     verStr.insert(12,'0');
     verStrPos = 0;
     if (verStr.toString().matches("RFB \\d{3}\\.\\d{3}\\n0")) {
@@ -68,7 +76,8 @@
     return false;
   }
 
-  public void writeVersion(OutStream os) {
+  public void writeVersion(OutStream os)
+  {
     String str = String.format("RFB %03d.%03d\n", majorVersion, minorVersion);
     os.writeBytes(str.getBytes(), 0, 12);
     os.flush();
@@ -97,10 +106,11 @@
 
   public PixelFormat pf() { return pf_; }
   public void setPF(PixelFormat pf) {
+
     pf_ = pf;
-    if (pf.bpp != 8 && pf.bpp != 16 && pf.bpp != 32) {
+
+    if (pf.bpp != 8 && pf.bpp != 16 && pf.bpp != 32)
       throw new Exception("setPF: not 8, 16 or 32 bpp?");
-    }
   }
 
   public String name() { return name_; }
@@ -109,80 +119,96 @@
     name_ = name;
   }
 
-  public int currentEncoding() { return currentEncoding_; }
-  public int nEncodings() { return nEncodings_; }
-  public int[] encodings() { return encodings_; }
+  public boolean supportsEncoding(int encoding)
+  {
+    return encodings_.indexOf(encoding) != -1;
+  }
+
   public void setEncodings(int nEncodings, int[] encodings)
   {
-    if (nEncodings > nEncodings_) {
-      encodings_ = new int[nEncodings];
-    }
-    nEncodings_ = nEncodings;
     useCopyRect = false;
     supportsLocalCursor = false;
     supportsDesktopResize = false;
     supportsExtendedDesktopSize = false;
     supportsLocalXCursor = false;
     supportsLastRect = false;
-    customCompressLevel = false;
     compressLevel = -1;
-    noJpeg = true;
     qualityLevel = -1;
     fineQualityLevel = -1;
-    subsampling = "SUBSAMP_UNDEFINED";
-    currentEncoding_ = Encodings.encodingRaw;
+    subsampling = subsampleUndefined;
+
+    encodings_.clear();
+    encodings_.add(Encodings.encodingRaw);
 
     for (int i = nEncodings-1; i >= 0; i--) {
-      encodings_[i] = encodings[i];
-
-      if (encodings[i] == Encodings.encodingCopyRect)
+      switch (encodings[i]) {
+      case Encodings.encodingCopyRect:
         useCopyRect = true;
-      else if (encodings[i] == Encodings.pseudoEncodingCursor)
+        break;
+      case Encodings.pseudoEncodingCursor:
         supportsLocalCursor = true;
-      else if (encodings[i] == Encodings.pseudoEncodingXCursor)
+        break;
+      case Encodings.pseudoEncodingXCursor:
         supportsLocalXCursor = true;
-      else if (encodings[i] == Encodings.pseudoEncodingDesktopSize)
+        break;
+      case Encodings.pseudoEncodingDesktopSize:
         supportsDesktopResize = true;
-      else if (encodings[i] == Encodings.pseudoEncodingExtendedDesktopSize)
+        break;
+      case Encodings.pseudoEncodingExtendedDesktopSize:
         supportsExtendedDesktopSize = true;
-      else if (encodings[i] == Encodings.pseudoEncodingDesktopName)
+        break;
+      case Encodings.pseudoEncodingDesktopName:
         supportsDesktopRename = true;
-      else if (encodings[i] == Encodings.pseudoEncodingLastRect)
+        break;
+      case Encodings.pseudoEncodingLastRect:
         supportsLastRect = true;
-      else if (encodings[i] == Encodings.pseudoEncodingFence)
+        break;
+      case Encodings.pseudoEncodingFence:
         supportsFence = true;
-      else if (encodings[i] == Encodings.pseudoEncodingContinuousUpdates)
+        break;
+      case Encodings.pseudoEncodingContinuousUpdates:
         supportsContinuousUpdates = true;
-      else if (encodings[i] == Encodings.pseudoEncodingClientRedirect)
+        break;
+      case Encodings.pseudoEncodingClientRedirect:
         supportsClientRedirect = true;
-      else if (encodings[i] >= Encodings.pseudoEncodingCompressLevel0 &&
-          encodings[i] <= Encodings.pseudoEncodingCompressLevel9) {
-        customCompressLevel = true;
-        compressLevel = encodings[i] - Encodings.pseudoEncodingCompressLevel0;
-      } else if (encodings[i] >= Encodings.pseudoEncodingQualityLevel0 &&
-          encodings[i] <= Encodings.pseudoEncodingQualityLevel9) {
-        noJpeg = false;
-        qualityLevel = encodings[i] - Encodings.pseudoEncodingQualityLevel0;
-      } else if (encodings[i] <= Encodings.encodingMax &&
-               Encoder.supported(encodings[i]))
-        currentEncoding_ = encodings[i];
-    }
-
-    // If the TurboVNC fine quality/subsampling encodings exist, let them
-    // override the coarse TightVNC quality level
-    for (int i = nEncodings-1; i >= 0; i--) {
-      if (encodings[i] >= Encodings.pseudoEncodingFineQualityLevel0 + 1 &&
-          encodings[i] <= Encodings.pseudoEncodingFineQualityLevel100) {
-        noJpeg = false;
-        fineQualityLevel = encodings[i] - Encodings.pseudoEncodingFineQualityLevel0;
-      } else if (encodings[i] >= Encodings.pseudoEncodingSubsamp1X &&
-                 encodings[i] <= Encodings.pseudoEncodingSubsampGray) {
-        noJpeg = false;
-        subsampling = JpegCompressor.subsamplingName(encodings[i] - Encodings.pseudoEncodingSubsamp1X);
+        break;
+      case Encodings.pseudoEncodingSubsamp1X:
+        subsampling = subsampleNone;
+        break;
+      case Encodings.pseudoEncodingSubsampGray:
+        subsampling = subsampleGray;
+        break;
+      case Encodings.pseudoEncodingSubsamp2X:
+        subsampling = subsample2X;
+        break;
+      case Encodings.pseudoEncodingSubsamp4X:
+        subsampling = subsample4X;
+        break;
+      case Encodings.pseudoEncodingSubsamp8X:
+        subsampling = subsample8X;
+        break;
+      case Encodings.pseudoEncodingSubsamp16X:
+        subsampling = subsample16X;
+        break;
       }
+
+      if (encodings[i] >= Encodings.pseudoEncodingCompressLevel0 &&
+          encodings[i] <= Encodings.pseudoEncodingCompressLevel9)
+        compressLevel = encodings[i] - Encodings.pseudoEncodingCompressLevel0;
+
+      if (encodings[i] >= Encodings.pseudoEncodingQualityLevel0 &&
+          encodings[i] <= Encodings.pseudoEncodingQualityLevel9)
+        qualityLevel = encodings[i] - Encodings.pseudoEncodingQualityLevel0;
+
+      if (encodings[i] >= Encodings.pseudoEncodingFineQualityLevel0 &&
+          encodings[i] <= Encodings.pseudoEncodingFineQualityLevel100)
+        fineQualityLevel = encodings[i] - Encodings.pseudoEncodingFineQualityLevel0;
+
+      if (encodings[i] > 0)
+        encodings_.add(encodings[i]);
     }
   }
-  public boolean done;
+
   public boolean useCopyRect;
 
   public boolean supportsLocalCursor;
@@ -190,25 +216,22 @@
   public boolean supportsDesktopResize;
   public boolean supportsExtendedDesktopSize;
   public boolean supportsDesktopRename;
-  public boolean supportsClientRedirect;
-  public boolean supportsFence;
-  public boolean supportsContinuousUpdates;
   public boolean supportsLastRect;
+  public boolean supportsClientRedirect;
 
   public boolean supportsSetDesktopSize;
+  public boolean supportsFence;
+  public boolean supportsContinuousUpdates;
 
-  public boolean customCompressLevel;
   public int compressLevel;
-  public boolean noJpeg;
   public int qualityLevel;
   public int fineQualityLevel;
-  public String subsampling;
+  public int subsampling;
 
   private PixelFormat pf_;
   private String name_;
-  private int nEncodings_;
-  private int[] encodings_;
-  private int currentEncoding_;
+  private Cursor cursor_;
+  private ArrayList encodings_;
   private StringBuilder verStr;
   private int verStrPos;
 }
diff --git a/java/com/tigervnc/rfb/CopyRectDecoder.java b/java/com/tigervnc/rfb/CopyRectDecoder.java
new file mode 100644
index 0000000..a4298fd
--- /dev/null
+++ b/java/com/tigervnc/rfb/CopyRectDecoder.java
@@ -0,0 +1,44 @@
+/* Copyright 2014 Pierre Ossman <ossman@cendio.se> for Cendio AB
+ * Copyright 2016 Brian P. Hinz
+ * 
+ * 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.
+ */
+
+package com.tigervnc.rfb;
+
+import com.tigervnc.rdr.*;
+
+public class CopyRectDecoder extends Decoder {
+
+  public CopyRectDecoder() { super(DecoderFlags.DecoderPlain); }
+
+  public void readRect(Rect r, InStream is,
+                       ConnParams cp, OutStream os)
+  {
+    os.copyBytes(is, 4);
+  }
+
+  public void decodeRect(Rect r, Object buffer,
+                         int buflen, ConnParams cp,
+                         ModifiablePixelBuffer pb)
+  {
+    MemInStream is = new MemInStream((byte[])buffer, 0, buflen);
+    int srcX = is.readU16();
+    int srcY = is.readU16();
+    pb.copyRect(r, new Point(r.tl.x-srcX, r.tl.y-srcY));
+  }
+
+}
diff --git a/java/com/tigervnc/rfb/Cursor.java b/java/com/tigervnc/rfb/Cursor.java
index 78aa0fb..05122ae 100644
--- a/java/com/tigervnc/rfb/Cursor.java
+++ b/java/com/tigervnc/rfb/Cursor.java
@@ -20,11 +20,18 @@
 
 public class Cursor extends ManagedPixelBuffer {
 
+  public Cursor(PixelFormat pf, int w, int h) {
+    super(pf, w, h);
+    hotspot = new Point(0, 0);
+  }
+
   public void setSize(int w, int h) {
+    int oldMaskLen = maskLen();
     super.setSize(w, h);
-    if (mask == null || mask.length < maskLen())
+    if (mask == null || maskLen() > oldMaskLen)
       mask = new byte[maskLen()];
   }
+
   public int maskLen() { return (width() + 7) / 8 * height(); }
 
   public Point hotspot;
diff --git a/java/com/tigervnc/rfb/DecodeManager.java b/java/com/tigervnc/rfb/DecodeManager.java
new file mode 100644
index 0000000..9e254ad
--- /dev/null
+++ b/java/com/tigervnc/rfb/DecodeManager.java
@@ -0,0 +1,386 @@
+/* Copyright 2015 Pierre Ossman for Cendio AB
+ * Copyright 2016 Brian P. Hinz
+ * 
+ * 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.
+ */
+
+package com.tigervnc.rfb;
+
+import java.lang.Runtime;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.locks.*;
+
+import com.tigervnc.rdr.*;
+import com.tigervnc.rdr.Exception;
+
+import static com.tigervnc.rfb.Decoder.DecoderFlags.*;
+
+public class DecodeManager {
+
+  static LogWriter vlog = new LogWriter("DecodeManager");
+
+  public DecodeManager(CConnection conn) {
+    int cpuCount;
+
+    this.conn = conn; threadException = null;
+    decoders = new Decoder[Encodings.encodingMax+1];
+
+    queueMutex = new ReentrantLock();
+    producerCond = queueMutex.newCondition();
+    consumerCond = queueMutex.newCondition();
+
+    //cpuCount = 1;
+    cpuCount = Runtime.getRuntime().availableProcessors();
+    if (cpuCount == 0) {
+      vlog.error("Unable to determine the number of CPU cores on this system");
+      cpuCount = 1;
+    } else {
+      vlog.info("Detected "+cpuCount+" CPU core(s)");
+      // No point creating more threads than this, they'll just end up
+      // wasting CPU fighting for locks
+      if (cpuCount > 4)
+        cpuCount = 4;
+      // The overhead of threading is small, but not small enough to
+      // ignore on single CPU systems
+      if (cpuCount == 1)
+        vlog.info("Decoding data on main thread");
+      else
+        vlog.info("Creating "+cpuCount+" decoder thread(s)");
+    }
+
+    freeBuffers = new ArrayDeque<MemOutStream>(cpuCount*2);
+    workQueue = new ArrayDeque<QueueEntry>(cpuCount);
+    threads = new ArrayList<DecodeThread>(cpuCount);
+    while (cpuCount-- > 0) {
+      // Twice as many possible entries in the queue as there
+      // are worker threads to make sure they don't stall
+      try {
+      freeBuffers.addLast(new MemOutStream());
+      freeBuffers.addLast(new MemOutStream());
+
+      threads.add(new DecodeThread(this));
+      } catch (IllegalStateException e) { }
+    }
+
+  }
+
+  public void decodeRect(Rect r, int encoding,
+                         ModifiablePixelBuffer pb)
+  {
+    Decoder decoder;
+    MemOutStream bufferStream;
+
+    QueueEntry entry;
+
+    assert(pb != null);
+
+    if (!Decoder.supported(encoding)) {
+      vlog.error("Unknown encoding " + encoding);
+      throw new Exception("Unknown encoding");
+    }
+
+    if (decoders[encoding] == null) {
+      decoders[encoding] = Decoder.createDecoder(encoding);
+      if (decoders[encoding] == null) {
+        vlog.error("Unknown encoding " + encoding);
+        throw new Exception("Unknown encoding");
+      }
+    }
+
+    decoder = decoders[encoding];
+
+    // Fast path for single CPU machines to avoid the context
+    // switching overhead
+    if (threads.size() == 1) {
+      bufferStream = freeBuffers.getFirst();
+      bufferStream.clear();
+      decoder.readRect(r, conn.getInStream(), conn.cp, bufferStream);
+      decoder.decodeRect(r, (Object)bufferStream.data(), bufferStream.length(),
+                         conn.cp, pb);
+      return;
+    }
+
+    // Wait for an available memory buffer
+    queueMutex.lock();
+
+    while (freeBuffers.isEmpty())
+      try {
+      producerCond.await();
+      } catch (InterruptedException e) { }
+
+    // Don't pop the buffer in case we throw an exception
+    // whilst reading
+    bufferStream = freeBuffers.getFirst();
+
+    queueMutex.unlock();
+
+    // First check if any thread has encountered a problem
+    throwThreadException();
+
+    // Read the rect
+    bufferStream.clear();
+    decoder.readRect(r, conn.getInStream(), conn.cp, bufferStream);
+
+    // Then try to put it on the queue
+    entry = new QueueEntry();
+
+    entry.active = false;
+    entry.rect = r;
+    entry.encoding = encoding;
+    entry.decoder = decoder;
+    entry.cp = conn.cp;
+    entry.pb = pb;
+    entry.bufferStream = bufferStream;
+    entry.affectedRegion = new Region(r);
+
+    decoder.getAffectedRegion(r, bufferStream.data(),
+                              bufferStream.length(), conn.cp,
+                              entry.affectedRegion);
+
+    // The workers add buffers to the end so it's safe to assume
+    // the front is still the same buffer
+    freeBuffers.removeFirst();
+
+    queueMutex.lock();
+
+    workQueue.addLast(entry);
+
+    // We only put a single entry on the queue so waking a single
+    // thread is sufficient
+    consumerCond.signal();
+
+    queueMutex.unlock();
+  }
+
+  public void flush()
+  {
+    queueMutex.lock();
+
+    while (!workQueue.isEmpty())
+      try {
+      producerCond.await();
+      } catch (InterruptedException e) { }
+
+    queueMutex.unlock();
+
+    throwThreadException();
+  }
+
+  private void setThreadException(Exception e)
+  {
+    //os::AutoMutex a(queueMutex);
+    queueMutex.lock();
+
+    if (threadException == null)
+      return;
+
+    threadException =
+      new Exception("Exception on worker thread: "+e.getMessage());
+  }
+
+  private void throwThreadException()
+  {
+    //os::AutoMutex a(queueMutex);
+    queueMutex.lock();
+
+    if (threadException == null)
+      return;
+
+    Exception e = new Exception(threadException.getMessage());
+
+    threadException = null;
+
+    throw e;
+  }
+
+  private class QueueEntry {
+
+    public QueueEntry() {
+    }
+    public boolean active;
+    public Rect rect;
+    public int encoding;
+    public Decoder decoder;
+    public ConnParams cp;
+    public ModifiablePixelBuffer pb;
+    public MemOutStream bufferStream;
+    public Region affectedRegion;
+  }
+
+  private class DecodeThread implements Runnable {
+
+    public DecodeThread(DecodeManager manager)
+    {
+      this.manager = manager;
+
+      stopRequested = false;
+
+      (thread = new Thread(this)).start();
+    }
+
+    public void stop()
+    {
+      //os::AutoMutex a(manager.queueMutex);
+      manager.queueMutex.lock();
+
+      if (!thread.isAlive())
+        return;
+
+      stopRequested = true;
+
+      // We can't wake just this thread, so wake everyone
+      manager.consumerCond.signalAll();
+    }
+
+    public void run()
+    {
+      manager.queueMutex.lock();
+      while (!stopRequested) {
+        QueueEntry entry;
+
+        // Look for an available entry in the work queue
+        entry = findEntry();
+        if (entry == null) {
+          // Wait and try again
+          try {
+          manager.consumerCond.await();
+          } catch (InterruptedException e) { }
+          continue;
+        }
+
+        // This is ours now
+        entry.active = true;
+
+        manager.queueMutex.unlock();
+
+        // Do the actual decoding
+        try {
+          entry.decoder.decodeRect(entry.rect, entry.bufferStream.data(),
+                                   entry.bufferStream.length(),
+                                   entry.cp, entry.pb);
+        } catch (com.tigervnc.rdr.Exception e) {
+          manager.setThreadException(e);
+        } catch(java.lang.Exception e) {
+          assert(false);
+        }
+
+        manager.queueMutex.lock();
+
+        // Remove the entry from the queue and give back the memory buffer
+        manager.freeBuffers.add(entry.bufferStream);
+        manager.workQueue.remove(entry);
+        entry = null;
+
+        // Wake the main thread in case it is waiting for a memory buffer
+        manager.producerCond.signal();
+        // This rect might have been blocking multiple other rects, so
+        // wake up every worker thread
+        if (manager.workQueue.size() > 1)
+          manager.consumerCond.signalAll();
+      }
+
+      manager.queueMutex.unlock();
+    }
+
+    protected QueueEntry findEntry()
+    {
+      Iterator<QueueEntry> iter;
+      Region lockedRegion = new Region();
+
+      if (manager.workQueue.isEmpty())
+        return null;
+
+      if (!manager.workQueue.peek().active)
+        return manager.workQueue.peek();
+
+      for (iter = manager.workQueue.iterator(); iter.hasNext();) {
+        QueueEntry entry;
+
+        Iterator<QueueEntry> iter2;
+
+        entry = iter.next();
+
+        // Another thread working on this?
+        if (entry.active) {
+          lockedRegion.assign_union(entry.affectedRegion);
+          continue;
+        }
+
+        // If this is an ordered decoder then make sure this is the first
+        // rectangle in the queue for that decoder
+        if ((entry.decoder.flags & DecoderOrdered) != 0) {
+          for (iter2 = manager.workQueue.iterator(); iter2.hasNext() && iter2 != iter;) {
+            if (entry.encoding == (iter2.next()).encoding) {
+              lockedRegion.assign_union(entry.affectedRegion);
+              continue;
+            }
+          }
+        }
+
+        // For a partially ordered decoder we must ask the decoder for each
+        // pair of rectangles.
+        if ((entry.decoder.flags & DecoderPartiallyOrdered) != 0) {
+          for (iter2 = manager.workQueue.iterator(); iter2.hasNext() && iter2 != iter;) {
+            QueueEntry entry2 = iter2.next();
+            if (entry.encoding != entry2.encoding)
+              continue;
+            if (entry.decoder.doRectsConflict(entry.rect,
+                                              entry.bufferStream.data(),
+                                              entry.bufferStream.length(),
+                                              entry2.rect,
+                                              entry2.bufferStream.data(),
+                                              entry2.bufferStream.length(),
+                                              entry.cp))
+              lockedRegion.assign_union(entry.affectedRegion);
+              continue;
+          }
+        }
+
+        // Check overlap with earlier rectangles
+        if (!lockedRegion.intersect(entry.affectedRegion).is_empty()) {
+          lockedRegion.assign_union(entry.affectedRegion);
+          continue;
+        }
+
+        return entry;
+
+      }
+
+      return null;
+    }
+
+    private DecodeManager manager;
+    private boolean stopRequested;
+
+    private Thread thread;
+
+  }
+
+  private CConnection conn;
+  private Decoder[] decoders;
+
+  private ArrayDeque<MemOutStream> freeBuffers;
+  private ArrayDeque<QueueEntry> workQueue;
+
+  private ReentrantLock queueMutex;
+  private Condition producerCond;
+  private Condition consumerCond;
+
+  private List<DecodeThread> threads;
+  private com.tigervnc.rdr.Exception threadException;
+
+}
diff --git a/java/com/tigervnc/rfb/Decoder.java b/java/com/tigervnc/rfb/Decoder.java
index f0ece0a..6bbed85 100644
--- a/java/com/tigervnc/rfb/Decoder.java
+++ b/java/com/tigervnc/rfb/Decoder.java
@@ -18,34 +18,80 @@
 
 package com.tigervnc.rfb;
 
+import com.tigervnc.rdr.*;
+
 abstract public class Decoder {
 
-  abstract public void readRect(Rect r, CMsgHandler handler);
+  public static class DecoderFlags {
+    // A constant for decoders that don't need anything special
+    public static int DecoderPlain = 0;
+    // All rects for this decoder must be handled in order
+    public static int DecoderOrdered = 1 << 0;
+    // Only some of the rects must be handled in order,
+    // see doesRectsConflict()
+    public static int DecoderPartiallyOrdered = 1 << 1;
+  };
+
+  public Decoder(int flags)
+  {
+    this.flags = flags;
+  }
+
+  abstract public void readRect(Rect r, InStream is,
+                                ConnParams cp, OutStream os);
+
+  abstract public void decodeRect(Rect r, Object buffer,
+                                  int buflen, ConnParams cp,
+                                  ModifiablePixelBuffer pb);
+
+  public void getAffectedRegion(Rect rect, Object buffer,
+                                int buflen, ConnParams cp,
+                                Region region)
+  {
+    region.reset(rect);
+  }
+
+  public boolean doRectsConflict(Rect rectA, Object bufferA,
+                                 int buflenA, Rect rectB,
+                                 Object bufferB, int buflenB,
+                                 ConnParams cp)
+  {
+    return false;
+  }
 
   static public boolean supported(int encoding)
   {
-/*
-    return encoding <= Encodings.encodingMax && createFns[encoding];
-*/
-    return (encoding == Encodings.encodingRaw ||
-            encoding == Encodings.encodingRRE ||
-            encoding == Encodings.encodingHextile ||
-            encoding == Encodings.encodingTight ||
-            encoding == Encodings.encodingZRLE);
-  }
-  static public Decoder createDecoder(int encoding, CMsgReader reader) {
-/*
-    if (encoding <= Encodings.encodingMax && createFns[encoding])
-      return (createFns[encoding])(reader);
-    return 0;
-*/
     switch(encoding) {
-    case Encodings.encodingRaw:     return new RawDecoder(reader);
-    case Encodings.encodingRRE:     return new RREDecoder(reader);
-    case Encodings.encodingHextile: return new HextileDecoder(reader);
-    case Encodings.encodingTight:   return new TightDecoder(reader);
-    case Encodings.encodingZRLE:    return new ZRLEDecoder(reader);
+    case Encodings.encodingRaw:
+    case Encodings.encodingCopyRect:
+    case Encodings.encodingRRE:
+    case Encodings.encodingHextile:
+    case Encodings.encodingZRLE:
+    case Encodings.encodingTight:
+      return true;
+    default:
+      return false;
     }
-    return null;
   }
+
+  static public Decoder createDecoder(int encoding) {
+    switch(encoding) {
+    case Encodings.encodingRaw:
+      return new RawDecoder();
+    case Encodings.encodingCopyRect:
+      return new CopyRectDecoder();
+    case Encodings.encodingRRE:
+      return new RREDecoder();
+    case Encodings.encodingHextile:
+      return new HextileDecoder();
+    case Encodings.encodingZRLE:
+      return new ZRLEDecoder();
+    case Encodings.encodingTight:
+      return new TightDecoder();
+    default:
+      return null;
+    }
+  }
+
+  public final int flags;
 }
diff --git a/java/com/tigervnc/rfb/FullFramePixelBuffer.java b/java/com/tigervnc/rfb/FullFramePixelBuffer.java
new file mode 100644
index 0000000..1c3b095
--- /dev/null
+++ b/java/com/tigervnc/rfb/FullFramePixelBuffer.java
@@ -0,0 +1,54 @@
+/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ * USA.
+ */
+
+package com.tigervnc.rfb;
+
+import java.awt.image.*;
+
+public class FullFramePixelBuffer extends ModifiablePixelBuffer {
+
+  public FullFramePixelBuffer(PixelFormat pf, int w, int h,
+                              WritableRaster data_) {
+    super(pf, w, h);
+    data = data_;
+  }
+
+  protected FullFramePixelBuffer() {}
+
+  public WritableRaster getBufferRW(Rect r)
+  {
+    return data.createWritableChild(r.tl.x, r.tl.y, r.width(), r.height(),
+                                    0, 0, null);
+  }
+
+  public void commitBufferRW(Rect r)
+  {
+  }
+
+  public Raster getBuffer(Rect r)
+  {
+    Raster src =
+      data.createChild(r.tl.x, r.tl.y, r.width(), r.height(), 0, 0, null);
+    WritableRaster dst =
+      data.createCompatibleWritableRaster(r.width(), r.height());
+    dst.setDataElements(0, 0, src);
+    return dst;
+  }
+
+  protected WritableRaster data;
+}
diff --git a/java/com/tigervnc/rfb/HextileDecoder.java b/java/com/tigervnc/rfb/HextileDecoder.java
index 94e91f7..b0744ca 100644
--- a/java/com/tigervnc/rfb/HextileDecoder.java
+++ b/java/com/tigervnc/rfb/HextileDecoder.java
@@ -18,22 +18,126 @@
 
 package com.tigervnc.rfb;
 
+import java.awt.image.*;
+import java.nio.*;
+import java.util.Arrays;
+
 import com.tigervnc.rdr.*;
 
 public class HextileDecoder extends Decoder {
 
-  public HextileDecoder(CMsgReader reader_) { reader = reader_; }
+  public static final int hextileRaw = (1 << 0);
+  public static final int hextileBgSpecified = (1 << 1);
+  public static final int hextileFgSpecified = (1 << 2);
+  public static final int hextileAnySubrects = (1 << 3);
+  public static final int hextileSubrectsColoured = (1 << 4);
 
-  public void readRect(Rect r, CMsgHandler handler) {
-    InStream is = reader.getInStream();
-    int bytesPerPixel = handler.cp.pf().bpp / 8;
-    boolean bigEndian = handler.cp.pf().bigEndian;
+  public HextileDecoder() { super(DecoderFlags.DecoderPlain); }
 
-    int[] buf = reader.getImageBuf(16 * 16 * 4);
-
+  public void readRect(Rect r, InStream is,
+                       ConnParams cp, OutStream os)
+  {
     Rect t = new Rect();
-    int bg = 0;
-    int fg = 0;
+    int bytesPerPixel;
+
+    bytesPerPixel = cp.pf().bpp/8;
+
+    for (t.tl.y = r.tl.y; t.tl.y < r.br.y; t.tl.y += 16) {
+
+      t.br.y = Math.min(r.br.y, t.tl.y + 16);
+
+      for (t.tl.x = r.tl.x; t.tl.x < r.br.x; t.tl.x += 16) {
+        int tileType;
+
+        t.br.x = Math.min(r.br.x, t.tl.x + 16);
+
+        tileType = is.readU8() & 0xff;
+        os.writeU32(tileType);
+
+        if ((tileType & hextileRaw) != 0) {
+          os.copyBytes(is, t.area() * bytesPerPixel);
+          continue;
+        }
+
+        if ((tileType & hextileBgSpecified) != 0)
+          os.copyBytes(is, bytesPerPixel);
+
+        if ((tileType & hextileFgSpecified) != 0)
+          os.copyBytes(is, bytesPerPixel);
+
+        if ((tileType & hextileAnySubrects) != 0) {
+          int nSubrects;
+
+          nSubrects = is.readU8() & 0xff;
+          os.writeU32(nSubrects);
+
+          if ((tileType & hextileSubrectsColoured) != 0)
+            os.copyBytes(is, nSubrects * (bytesPerPixel + 2));
+          else
+            os.copyBytes(is, nSubrects * 2);
+        }
+      }
+    }
+  }
+
+  public void decodeRect(Rect r, Object buffer,
+                         int buflen, ConnParams cp,
+                         ModifiablePixelBuffer pb)
+  {
+    MemInStream is = new MemInStream((byte[])buffer, 0, buflen);
+    PixelFormat pf = cp.pf();
+    switch (pf.bpp) {
+    case 8:  hextileDecode8(r, is, pf, pb); break;
+    case 16: hextileDecode16(r, is, pf, pb); break;
+    case 32: hextileDecode32(r, is, pf, pb); break;
+    }
+  }
+
+  private void hextileDecode8(Rect r, InStream is,
+                              PixelFormat pf,
+                              ModifiablePixelBuffer pb)
+  {
+    HEXTILE_DECODE(r, is, pf, pb);
+  }
+
+  private void hextileDecode16(Rect r, InStream is,
+                               PixelFormat pf,
+                               ModifiablePixelBuffer pb)
+  {
+    HEXTILE_DECODE(r, is, pf, pb);
+  }
+
+  private void hextileDecode32(Rect r, InStream is,
+                               PixelFormat pf,
+                               ModifiablePixelBuffer pb)
+  {
+    HEXTILE_DECODE(r, is, pf, pb);
+  }
+
+  private static ByteBuffer READ_PIXEL(InStream is, PixelFormat pf) {
+    ByteBuffer b = ByteBuffer.allocate(4);
+    switch (pf.bpp) {
+    case 8:
+      b.putInt(is.readOpaque8());
+      return ByteBuffer.allocate(1).put(b.get(3));
+    case 16:
+      b.putInt(is.readOpaque16());
+      return ByteBuffer.allocate(2).put(b.array(), 2, 2);
+    case 32:
+    default:
+      b.putInt(is.readOpaque32());
+      return b;
+    }
+  }
+
+  private void HEXTILE_DECODE(Rect r, InStream is,
+                                     PixelFormat pf,
+                                     ModifiablePixelBuffer pb)
+  {
+    Rect t = new Rect();
+    ByteBuffer bg = ByteBuffer.allocate(pf.bpp/8);
+    ByteBuffer fg = ByteBuffer.allocate(pf.bpp/8);
+    ByteBuffer buf = ByteBuffer.allocate(16 * 16 * 4);
 
     for (t.tl.y = r.tl.y; t.tl.y < r.br.y; t.tl.y += 16) {
 
@@ -43,59 +147,51 @@
 
         t.br.x = Math.min(r.br.x, t.tl.x + 16);
 
-        int tileType = is.readU8();
+        int tileType = is.readU32();
 
-        if ((tileType & Hextile.raw) != 0) {
-          is.readPixels(buf, t.area(), bytesPerPixel, bigEndian);
-          handler.imageRect(t, buf);
+        if ((tileType & hextileRaw) != 0) {
+          is.readBytes(buf, t.area() * (pf.bpp/8));
+          pb.imageRect(pf, t, buf.array());
           continue;
         }
 
-        if ((tileType & Hextile.bgSpecified) != 0)
-          bg = is.readPixel(bytesPerPixel, bigEndian);
+        if ((tileType & hextileBgSpecified) != 0)
+          bg = READ_PIXEL(is, pf);
 
         int len = t.area();
-        int ptr = 0;
-        while (len-- > 0) buf[ptr++] = bg;
+        ByteBuffer ptr = buf.duplicate();
+        while (len-- > 0) ptr.put(bg.array());
 
-        if ((tileType & Hextile.fgSpecified) != 0)
-          fg = is.readPixel(bytesPerPixel, bigEndian);
+        if ((tileType & hextileFgSpecified) != 0)
+          fg = READ_PIXEL(is, pf);
 
-        if ((tileType & Hextile.anySubrects) != 0) {
-          int nSubrects = is.readU8();
+        if ((tileType & hextileAnySubrects) != 0) {
+          int nSubrects = is.readU32();
 
           for (int i = 0; i < nSubrects; i++) {
 
-            if ((tileType & Hextile.subrectsColoured) != 0)
-              fg = is.readPixel(bytesPerPixel, bigEndian);
+            if ((tileType & hextileSubrectsColoured) != 0)
+              fg = READ_PIXEL(is, pf);
 
             int xy = is.readU8();
             int wh = is.readU8();
 
-/*
-            Rect s = new Rect();
-            s.tl.x = t.tl.x + ((xy >> 4) & 15);
-            s.tl.y = t.tl.y + (xy & 15);
-            s.br.x = s.tl.x + ((wh >> 4) & 15) + 1;
-            s.br.y = s.tl.y + (wh & 15) + 1;
-*/
             int x = ((xy >> 4) & 15);
             int y = (xy & 15);
             int w = ((wh >> 4) & 15) + 1;
             int h = (wh & 15) + 1;
-            ptr = y * t.width() + x;
-            int rowAdd = t.width() - w;
+            ptr = buf.duplicate();
+            ptr.position((y * t.width() + x)*pf.bpp/8);
+            int rowAdd = (t.width() - w)*pf.bpp/8;
             while (h-- > 0) {
               len = w;
-              while (len-- > 0) buf[ptr++] = fg;
-              ptr += rowAdd;
+              while (len-- > 0) ptr.put(fg.array());
+              ptr.position(ptr.position()+Math.min(rowAdd,ptr.remaining()));
             }
           }
         }
-        handler.imageRect(t, buf);
+        pb.imageRect(pf, t, buf.array());
       }
     }
   }
-
-  CMsgReader reader;
 }
diff --git a/java/com/tigervnc/rfb/JpegDecompressor.java b/java/com/tigervnc/rfb/JpegDecompressor.java
new file mode 100644
index 0000000..9137847
--- /dev/null
+++ b/java/com/tigervnc/rfb/JpegDecompressor.java
@@ -0,0 +1,53 @@
+/* Copyright (C) 2016 Brian P. Hinz
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ * USA.
+ */
+package com.tigervnc.rfb;
+
+import java.awt.image.*;
+import java.io.*;
+import java.nio.ByteBuffer;
+import javax.imageio.*;
+import javax.imageio.stream.*;
+
+public class JpegDecompressor {
+
+  public JpegDecompressor() {}
+
+  public void decompress(ByteBuffer jpegBuf, int jpegBufLen,
+    WritableRaster buf, Rect r, PixelFormat pf)
+  {
+
+    byte[] src = new byte[jpegBufLen];
+
+    jpegBuf.get(src);
+    try {
+      BufferedImage image =
+        ImageIO.read(new MemoryCacheImageInputStream(new ByteArrayInputStream(src)));
+      ColorModel cm = pf.getColorModel();
+      if (cm.isCompatibleRaster(image.getRaster()) &&
+          cm.isCompatibleSampleModel(image.getRaster().getSampleModel())) {
+        buf.setDataElements(0, 0, image.getRaster());
+      } else {
+        ColorConvertOp converter = pf.getColorConvertOp(cm.getColorSpace());
+        converter.filter(image.getRaster(), buf);
+      }
+      image.flush();
+    } catch (IOException e) {
+      throw new Exception(e.getMessage());
+    }
+  }
+}
diff --git a/java/com/tigervnc/rfb/ManagedPixelBuffer.java b/java/com/tigervnc/rfb/ManagedPixelBuffer.java
index f947af7..6e14b92 100644
--- a/java/com/tigervnc/rfb/ManagedPixelBuffer.java
+++ b/java/com/tigervnc/rfb/ManagedPixelBuffer.java
@@ -18,21 +18,37 @@
 
 package com.tigervnc.rfb;
 
-public class ManagedPixelBuffer extends PixelBuffer {
-  public void setSize(int w, int h) {
-    width_ = w;
-    height_ = h;
-    checkDataSize();
-  }
-  public void setPF(PixelFormat pf) {
-    super.setPF(pf);
+public class ManagedPixelBuffer extends FullFramePixelBuffer {
+
+  public ManagedPixelBuffer() {
+    datasize = 0;
     checkDataSize();
   }
 
-  public int dataLen() { return area(); }
+  public ManagedPixelBuffer(PixelFormat pf, int w, int h)
+  {
+    super(pf, w, h, null);
+    datasize = 0;
+    checkDataSize();
+  }
+
+  public void setPF(PixelFormat pf) {
+    format = pf; checkDataSize();
+  }
+
+  public void setSize(int w, int h) {
+    width_ = w; height_ = h; checkDataSize();
+  }
 
   final void checkDataSize() {
-    if (data == null || data.length < dataLen())
-      data = new int[dataLen()];
+    int new_datasize = width_ * height_;
+    if (datasize < new_datasize) {
+      vlog.debug("reallocating managed buffer ("+width_+"x"+height_+")");
+      if (format != null)
+        data = PixelFormat.getColorModel(format).createCompatibleWritableRaster(width_, height_);
+    }
   }
+
+  protected int datasize;
+  static LogWriter vlog = new LogWriter("ManagedPixelBuffer");
 }
diff --git a/java/com/tigervnc/rfb/ModifiablePixelBuffer.java b/java/com/tigervnc/rfb/ModifiablePixelBuffer.java
new file mode 100644
index 0000000..bcc559d
--- /dev/null
+++ b/java/com/tigervnc/rfb/ModifiablePixelBuffer.java
@@ -0,0 +1,267 @@
+/* Copyright 2016 Brian P. Hinz
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ * USA.
+ */
+
+// -=- Modifiable generic pixel buffer class
+
+package com.tigervnc.rfb;
+
+import java.awt.image.*;
+import java.awt.Color;
+import java.awt.color.ColorSpace;
+import java.lang.*;
+import java.nio.*;
+import java.util.*;
+
+import static java.awt.image.DataBuffer.*;
+
+public abstract class ModifiablePixelBuffer extends PixelBuffer
+{
+
+  public ModifiablePixelBuffer(PixelFormat pf, int w, int h)
+  {
+    super(pf, w, h);
+  }
+
+  protected ModifiablePixelBuffer()
+  {
+  }
+
+  ///////////////////////////////////////////////
+  // Access to pixel data
+  //
+
+  // Get a writeable pointer into the buffer
+  //   Like getBuffer(), the pointer is to the top-left pixel of the
+  //   specified Rect.
+  public abstract WritableRaster getBufferRW(Rect r);
+  // Commit the modified contents
+  //   Ensures that the changes to the specified Rect is properly
+  //   stored away and any temporary buffers are freed. The Rect given
+  //   here needs to match the Rect given to the earlier call to
+  //   getBufferRW().
+  public abstract void commitBufferRW(Rect r);
+
+  static LogWriter vlog = new LogWriter("ModifiablePixelBuffer");
+  ///////////////////////////////////////////////
+  // Basic rendering operations
+  // These operations DO NOT clip to the pixelbuffer area, or trap overruns.
+
+  // Fill a rectangle
+  public synchronized void fillRect(Rect r, byte[] pix)
+  {
+    WritableRaster buf;
+    int w, h;
+
+    w = r.width();
+    h = r.height();
+
+    if (h == 0 || w ==0)
+      return;
+
+    buf = getBufferRW(r);
+
+    ByteBuffer src =
+      ByteBuffer.allocate(r.area()*format.bpp/8).order(format.getByteOrder());
+    for (int i=0; i < r.area(); i++)
+      src.put(pix);
+    Raster raster = format.rasterFromBuffer(r, (ByteBuffer)src.rewind());
+    buf.setDataElements(0, 0, raster);
+
+    commitBufferRW(r);
+  }
+
+  // Copy pixel data to the buffer
+  public synchronized void imageRect(Rect r, byte[] pixels)
+  {
+    WritableRaster dest = getBufferRW(r);
+
+    ByteBuffer src = ByteBuffer.wrap(pixels).order(format.getByteOrder());
+    Raster raster = format.rasterFromBuffer(r, src);
+    dest.setDataElements(0, 0, raster);
+
+    commitBufferRW(r);
+  }
+
+  // Copy pixel data from one PixelBuffer location to another
+  public synchronized void copyRect(Rect rect,
+                                    Point move_by_delta)
+  {
+    Raster srcData;
+    WritableRaster dstData;
+
+    Rect drect, srect;
+
+    drect = new Rect(rect.tl, rect.br);
+    if (!drect.enclosed_by(getRect())) {
+      String msg = "Destination rect %dx%d at %d,%d exceeds framebuffer %dx%d";
+      vlog.error(String.format(msg, drect.width(), drect.height(),
+                               drect.tl.x, drect.tl.y, width_, height_));
+      drect = drect.intersect(getRect());
+    }
+
+    if (drect.is_empty())
+      return;
+
+    srect = drect.translate(move_by_delta.negate());
+    if (!srect.enclosed_by(getRect())) {
+      String msg = "Source rect %dx%d at %d,%d exceeds framebuffer %dx%d";
+      vlog.error(String.format(msg, srect.width(), srect.height(),
+                               srect.tl.x, srect.tl.y, width_, height_));
+      srect = srect.intersect(getRect());
+      // Need to readjust the destination now that the area has changed
+      drect = srect.translate(move_by_delta);
+    }
+
+    if (srect.is_empty())
+      return;
+
+    srcData = getBuffer(srect);
+    dstData = getBufferRW(drect);
+
+    dstData.setDataElements(0, 0, srcData);
+
+    commitBufferRW(rect);
+  }
+
+  // Copy pixel data to the buffer through a mask
+  //   pixels is a pointer to the pixel to be copied to r.tl.
+  //   maskPos specifies the pixel offset in the mask to start from.
+  //   mask_ is a pointer to the mask bits at (0,0).
+  //   pStride and mStride are the strides of the pixel and mask buffers.
+  public synchronized void maskRect(Rect r,
+                                    Object pixels, byte[] mask_)
+  {
+    Rect cr = getRect().intersect(r);
+    if (cr.is_empty()) return;
+    WritableRaster data = getBufferRW(cr);
+
+    // FIXME
+    ColorModel cm = format.getColorModel();
+    SampleModel sm =
+      cm.createCompatibleSampleModel(r.width(), r.height());
+    DataBuffer db = null;
+    ByteBuffer src =
+      ByteBuffer.wrap((byte[])pixels).order(format.getByteOrder());
+    Buffer dst;
+    switch (sm.getTransferType()) {
+    case TYPE_INT:
+      dst = IntBuffer.allocate(src.remaining()).put(src.asIntBuffer());
+      db = new DataBufferInt(((IntBuffer)dst).array(), r.area());
+      break;
+    case TYPE_BYTE:
+      db = new DataBufferByte(src.array(), r.area());
+      break;
+    case TYPE_SHORT:
+      dst = ShortBuffer.allocate(src.remaining()).put(src.asShortBuffer());
+      db = new DataBufferShort(((ShortBuffer)dst).array(), r.area());
+      break;
+    }
+    assert(db != null);
+    Raster raster =
+      Raster.createRaster(sm, db, new java.awt.Point(0, 0));
+    ColorConvertOp converter = format.getColorConvertOp(cm.getColorSpace());
+    WritableRaster t = data.createCompatibleWritableRaster();
+    converter.filter(raster, t);
+
+    int w = cr.width();
+    int h = cr.height();
+
+    Point offset = new Point(cr.tl.x-r.tl.x, cr.tl.y-r.tl.y);
+
+    int maskBytesPerRow = (w + 7) / 8;
+
+    for (int y = 0; y < h; y++) {
+      int cy = offset.y + y;
+      for (int x = 0; x < w; x++) {
+        int cx = offset.x + x;
+        int byte_ = cy * maskBytesPerRow + y / 8;
+        int bit = 7 - cx % 8;
+
+        if ((mask_[byte_] & (1 << bit)) != 0)
+          data.setDataElements(x+cx, y+cy, t.getDataElements(x+cx, y+cy, null));
+      }
+    }
+
+    commitBufferRW(r);
+  }
+
+  //   pixel is the Pixel value to be used where mask_ is set
+  public synchronized void maskRect(Rect r, int pixel, byte[] mask)
+  {
+    // FIXME
+  }
+
+  // Render in a specific format
+  //   Does the exact same thing as the above methods, but the given
+  //   pixel values are defined by the given PixelFormat. 
+  public synchronized void fillRect(PixelFormat pf, Rect dest, byte[] pix)
+  {
+    WritableRaster dstBuffer = getBufferRW(dest);
+
+    ColorModel cm = pf.getColorModel();
+    if (cm.isCompatibleRaster(dstBuffer) &&
+        cm.isCompatibleSampleModel(dstBuffer.getSampleModel())) {
+      fillRect(dest, pix);
+    } else {
+      ByteBuffer src =
+        ByteBuffer.allocate(dest.area()*pf.bpp/8).order(pf.getByteOrder());
+      for (int i=0; i < dest.area(); i++)
+        src.put(pix);
+      Raster raster = pf.rasterFromBuffer(dest, (ByteBuffer)src.rewind());
+      ColorConvertOp converter = format.getColorConvertOp(cm.getColorSpace());
+      converter.filter(raster, dstBuffer);
+    }
+
+    commitBufferRW(dest);
+  }
+
+  public synchronized void imageRect(PixelFormat pf, Rect dest, byte[] pixels)
+  {
+    WritableRaster dstBuffer = getBufferRW(dest);
+
+    ColorModel cm = pf.getColorModel();
+    if (cm.isCompatibleRaster(dstBuffer) &&
+        cm.isCompatibleSampleModel(dstBuffer.getSampleModel())) {
+      imageRect(dest, pixels);
+    } else {
+      ByteBuffer src = ByteBuffer.wrap(pixels).order(pf.getByteOrder());
+      Raster raster = pf.rasterFromBuffer(dest, src);
+      ColorConvertOp converter = format.getColorConvertOp(cm.getColorSpace());
+      converter.filter(raster, dstBuffer);
+    }
+
+    commitBufferRW(dest);
+  }
+
+  public synchronized void imageRect(PixelFormat pf, Rect dest, Raster pixels)
+  {
+    WritableRaster dstBuffer = getBufferRW(dest);
+
+    ColorModel cm = pf.getColorModel();
+    if (cm.isCompatibleRaster(dstBuffer) &&
+        cm.isCompatibleSampleModel(dstBuffer.getSampleModel())) {
+      dstBuffer.setDataElements(0, 0, pixels);
+    } else {
+      ColorConvertOp converter = format.getColorConvertOp(cm.getColorSpace());
+      converter.filter(pixels, dstBuffer);
+    }
+
+    commitBufferRW(dest);
+  }
+
+}
diff --git a/java/com/tigervnc/rfb/PixelBuffer.java b/java/com/tigervnc/rfb/PixelBuffer.java
index a46667d..1b7d2c1 100644
--- a/java/com/tigervnc/rfb/PixelBuffer.java
+++ b/java/com/tigervnc/rfb/PixelBuffer.java
@@ -1,4 +1,5 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
+ * Copyright 2016 Brian P. Hinz
  *
  * This is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,117 +17,67 @@
  * USA.
  */
 
-//
-// PixelBuffer - note that this code is only written for the 8, 16, and 32 bpp cases at the
-// moment.
-//
+// -=- Generic pixel buffer class
 
 package com.tigervnc.rfb;
 
 import java.awt.image.*;
+import java.awt.Color;
+import java.nio.*;
+import java.util.concurrent.atomic.*;
 
-public class PixelBuffer {
+public abstract class PixelBuffer {
 
-  public PixelBuffer() {
-    setPF(new PixelFormat());
-  }
-
-  public void setPF(PixelFormat pf) {
-    if (!(pf.bpp == 32) && !(pf.bpp == 16) && !(pf.bpp == 8))
-      throw new Exception("Internal error: bpp must be 8, 16, or 32 in PixelBuffer ("+pf.bpp+")");
+  public PixelBuffer(PixelFormat pf, int w, int h) {
     format = pf;
-    switch (pf.depth) {
-    case  3:
-      // Fall-through to depth 8
-    case  6:
-      // Fall-through to depth 8
-    case  8:
-      if (!pf.trueColour) {
-        if (cm == null)
-          cm = new IndexColorModel(8, 256, new byte[256], new byte[256], new byte[256]);
-        break;
-      }
-      int rmask = pf.redMax << pf.redShift;
-      int gmask = pf.greenMax << pf.greenShift;
-      int bmask = pf.blueMax << pf.blueShift;
-      cm = new DirectColorModel(8, rmask, gmask, bmask);
-      break;
-    case 16:
-      cm = new DirectColorModel(32, 0xF800, 0x07C0, 0x003E);
-      break;
-    case 24:
-      cm = new DirectColorModel(32, (0xff << 16), (0xff << 8), 0xff);
-      break;
-    case 32:
-      cm = new DirectColorModel(32, (0xff << pf.redShift),
-        (0xff << pf.greenShift), (0xff << pf.blueShift));
-      break;
-    default:
-      throw new Exception("Unsupported color depth ("+pf.depth+")");
-    }
+    width_ = w;
+    height_= h;
   }
-  public PixelFormat getPF() { return format; }
 
+  protected PixelBuffer() { width_ = 0; height_ = 0; }
+
+  // Get pixel format
+  public final PixelFormat getPF() { return format; }
+
+  // Get width, height and number of pixels
   public final int width() { return width_; }
   public final int height() { return height_; }
   public final int area() { return width_ * height_; }
 
-  public void fillRect(int x, int y, int w, int h, int pix) {
-    for (int ry = y; ry < y + h; ry++)
-      for (int rx = x; rx < x + w; rx++)
-        data[ry * width_ + rx] = pix;
+  // Get rectangle encompassing this buffer
+  //   Top-left of rectangle is either at (0,0), or the specified point.
+  public final Rect getRect() { return new Rect(0, 0, width_, height_); }
+  public final Rect getRect(Point pos) {
+    return new Rect(pos, pos.translate(new Point(width_, height_)));
   }
 
-  public void imageRect(int x, int y, int w, int h, int[] pix) {
-    for (int j = 0; j < h; j++)
-      System.arraycopy(pix, (w * j), data, width_ * (y + j) + x, w);
-  }
+  ///////////////////////////////////////////////
+  // Access to pixel data
+  //
 
-  public void copyRect(int x, int y, int w, int h, int srcX, int srcY) {
-    int dest = (width_ * y) + x;
-    int src = (width_ * srcY) + srcX;
-    int inc = width_;
+  // Get a pointer into the buffer
+  //   The pointer is to the top-left pixel of the specified Rect.
+  public abstract Raster getBuffer(Rect r);
 
-    if (y > srcY) {
-      src += (h-1) * inc;
-      dest += (h-1) * inc;
-      inc = -inc;
-    }
-    int destEnd = dest + h * inc;
+  // Get pixel data for a given part of the buffer
+  //   Data is copied into the supplied buffer, with the specified
+  //   stride. Try to avoid using this though as getBuffer() will in
+  //   most cases avoid the extra memory copy.
+  //void getImage(void* imageBuf, const Rect& r, int stride=0) const;
+  // Get pixel data in a given format
+  //   Works just the same as getImage(), but guaranteed to be in a
+  //   specific format.
+  //void getImage(const PixelFormat& pf, void* imageBuf,
+  //                const Rect& r, int stride=0) const;
 
-    while (dest != destEnd) {
-      System.arraycopy(data, src, data, dest, w);
-      src += inc;
-      dest += inc;
-    }
-  }
+  ///////////////////////////////////////////////
+  // Framebuffer update methods
+  //
 
-  public void maskRect(int x, int y, int w, int h, int[] pix, byte[] mask) {
-    int maskBytesPerRow = (w + 7) / 8;
-
-    for (int j = 0; j < h; j++) {
-      int cy = y + j;
-
-      if (cy < 0 || cy >= height_)
-        continue;
-
-      for (int i = 0; i < w; i++) {
-        int cx = x + i;
-
-        if (cx < 0 || cx >= width_)
-          continue;
-
-        int byte_ = j * maskBytesPerRow + i / 8;
-        int bit = 7 - i % 8;
-
-        if ((mask[byte_] & (1 << bit)) != 0)
-         data[cy * width_ + cx] = pix[j * w + i];
-      }
-    }
-  }
-
-  public int[] data;
-  public ColorModel cm;
+  // Ensure that the specified rectangle of buffer is up to date.
+  //   Overridden by derived classes implementing framebuffer access
+  //   to copy the required display data into place.
+  //public abstract void grabRegion(Region& region) {}
 
   protected PixelFormat format;
   protected int width_, height_;
diff --git a/java/com/tigervnc/rfb/PixelFormat.java b/java/com/tigervnc/rfb/PixelFormat.java
index c4d6870..9a26999 100644
--- a/java/com/tigervnc/rfb/PixelFormat.java
+++ b/java/com/tigervnc/rfb/PixelFormat.java
@@ -25,40 +25,80 @@
 
 package com.tigervnc.rfb;
 
+import java.awt.color.*;
+import java.awt.image.*;
+import java.nio.*;
+import java.util.*;
+
 import com.tigervnc.rdr.*;
-import java.awt.image.ColorModel;
 
 public class PixelFormat {
 
-  public PixelFormat(int b, int d, boolean e, boolean t) {
-    bpp = b;
-    depth = d;
-    bigEndian = e;
-    trueColour = t;
-  }
   public PixelFormat(int b, int d, boolean e, boolean t,
-                     int rm, int gm, int bm, int rs, int gs, int bs) {
-    this(b, d, e, t);
-    redMax = rm;
-    greenMax = gm;
-    blueMax = bm;
-    redShift = rs;
-    greenShift = gs;
-    blueShift = bs;
-  }
-  public PixelFormat() { this(8,8,false,true,7,7,3,0,3,6); }
+                     int rm, int gm, int bm, int rs, int gs, int bs)
+  {
+    bpp = b; depth = d; trueColour = t; bigEndian = e;
+    redMax = rm; greenMax = gm; blueMax = bm;
+    redShift = rs; greenShift = gs; blueShift = bs;
+    converters = new HashMap<Integer, ColorConvertOp>();
+    assert(isSane());
 
-  public boolean equal(PixelFormat x) {
-    return (bpp == x.bpp &&
-            depth == x.depth &&
-            (bigEndian == x.bigEndian || bpp == 8) &&
-            trueColour == x.trueColour &&
-            (!trueColour || (redMax == x.redMax &&
-                             greenMax == x.greenMax &&
-                             blueMax == x.blueMax &&
-                             redShift == x.redShift &&
-                             greenShift == x.greenShift &&
-                             blueShift == x.blueShift)));
+    updateState();
+  }
+
+  public PixelFormat()
+  {
+    this(8, 8, false, true, 7, 7, 3, 0, 3, 6);
+    updateState();
+  }
+
+  public boolean equal(PixelFormat other)
+  {
+    if (bpp != other.bpp || depth != other.depth)
+      return false;
+
+    if (redMax != other.redMax)
+      return false;
+    if (greenMax != other.greenMax)
+      return false;
+    if (blueMax != other.blueMax)
+      return false;
+
+    // Endianness requires more care to determine compatibility
+    if (bigEndian == other.bigEndian || bpp == 8) {
+      if (redShift != other.redShift)
+        return false;
+      if (greenShift != other.greenShift)
+        return false;
+      if (blueShift != other.blueShift)
+        return false;
+    } else {
+      // Has to be the same byte for each channel
+      if (redShift/8 != (3 - other.redShift/8))
+        return false;
+      if (greenShift/8 != (3 - other.greenShift/8))
+        return false;
+      if (blueShift/8 != (3 - other.blueShift/8))
+        return false;
+
+      // And the same bit offset within the byte
+      if (redShift%8 != other.redShift%8)
+        return false;
+      if (greenShift%8 != other.greenShift%8)
+        return false;
+      if (blueShift%8 != other.blueShift%8)
+        return false;
+
+      // And not cross a byte boundary
+      if (redShift/8 != (redShift + redBits - 1)/8)
+        return false;
+      if (greenShift/8 != (greenShift + greenBits - 1)/8)
+        return false;
+      if (blueShift/8 != (blueShift + blueBits - 1)/8)
+        return false;
+    }
+
+    return true;
   }
 
   public void read(InStream is) {
@@ -73,6 +113,23 @@
     greenShift = is.readU8();
     blueShift = is.readU8();
     is.skip(3);
+
+    // We have no real support for colour maps. If the client
+    // wants one, then we force a 8-bit true colour format and
+    // pretend it's a colour map.
+    if (!trueColour) {
+      redMax = 7;
+      greenMax = 7;
+      blueMax = 3;
+      redShift = 0;
+      greenShift = 3;
+      blueShift = 6;
+    }
+
+    if (!isSane())
+      throw new Exception("invalid pixel format: "+print());
+
+    updateState();
   }
 
   public void write(OutStream os) {
@@ -89,6 +146,14 @@
     os.pad(3);
   }
 
+  public final boolean isBigEndian() {
+    return bigEndian;
+  }
+
+  public final boolean isLittleEndian() {
+    return ! bigEndian;
+  }
+
   public final boolean is888() {
     if(!trueColour)
       return false;
@@ -139,53 +204,140 @@
     return 0;
   }
 
-  public void bufferFromRGB(int[] dst, int dstPtr, byte[] src,
-                            int srcPtr, int pixels) {
+  public void bufferFromRGB(ByteBuffer dst, ByteBuffer src, int pixels)
+  {
+    bufferFromRGB(dst, src, pixels, pixels, 1);
+  }
+
+  public void bufferFromRGB(ByteBuffer dst, ByteBuffer src,
+                            int w, int stride, int h)
+  {
     if (is888()) {
       // Optimised common case
-      int r, g, b;
+      int r, g, b, x;
 
-      for (int i=srcPtr; i < pixels; i++) {
-        if (bigEndian) {
-          r = (src[3*i+0] & 0xff) << (24 - redShift);
-          g = (src[3*i+1] & 0xff) << (24 - greenShift);
-          b = (src[3*i+2] & 0xff) << (24 - blueShift);
-          dst[dstPtr+i] = r | g | b | 0xff;
-        } else {
-          r = (src[3*i+0] & 0xff) << redShift;
-          g = (src[3*i+1] & 0xff) << greenShift;
-          b = (src[3*i+2] & 0xff) << blueShift;
-          dst[dstPtr+i] = (0xff << 24) | r | g | b;
+      if (bigEndian) {
+        r = dst.position() + (24 - redShift)/8;
+        g = dst.position() + (24 - greenShift)/8;
+        b = dst.position() + (24 - blueShift)/8;
+        x = dst.position() + (24 - (48 - redShift - greenShift - blueShift))/8;
+      } else {
+        r = dst.position() + redShift/8;
+        g = dst.position() + greenShift/8;
+        b = dst.position() + blueShift/8;
+        x = dst.position() + (48 - redShift - greenShift - blueShift)/8;
+      }
+
+      int dstPad = (stride - w) * 4;
+      while (h-- > 0) {
+        int w_ = w;
+        while (w_-- > 0) {
+          dst.put(r, src.get());
+          dst.put(g, src.get());
+          dst.put(b, src.get());
+          dst.put(x, (byte)0);
+          r += 4;
+          g += 4;
+          b += 4;
+          x += 4;
         }
+        r += dstPad;
+        g += dstPad;
+        b += dstPad;
+        x += dstPad;
       }
     } else {
       // Generic code
-      int p, r, g, b;
-      int[] rgb = new int[4];
+      int dstPad = (stride - w) * bpp/8;
+      while (h-- > 0) {
+        int w_ = w;
+        while (w_-- > 0) {
+          int p;
+          int r, g, b;
 
-      int i = srcPtr; int j = dstPtr;
-      while (i < pixels) {
-        r = src[i++] & 0xff;
-        g = src[i++] & 0xff;
-        b = src[i++] & 0xff;
+          r = src.get();
+          g = src.get();
+          b = src.get();
 
-        //p = pixelFromRGB(r, g, b, cm);
-        p = ColorModel.getRGBdefault().getDataElement(new int[] {0xff, r, g, b}, 0);
+          p = pixelFromRGB(r, g, b, model);
 
-        bufferFromPixel(dst, j, p);
-        j += bpp/8;
+          bufferFromPixel(dst, p);
+          dst.position(dst.position() + bpp/8);
+        }
+        dst.position(dst.position() + dstPad);
       }
     }
   }
 
-  public void rgbFromBuffer(byte[] dst, int dstPtr, byte[] src, int srcPtr, int pixels, ColorModel cm)
+  public void rgbFromBuffer(ByteBuffer dst, ByteBuffer src, int pixels)
+  {
+    rgbFromBuffer(dst, src, pixels, pixels, 1);
+  }
+
+  public void rgbFromBuffer(ByteBuffer dst, ByteBuffer src,
+                            int w, int stride, int h)
+  {
+    if (is888()) {
+      // Optimised common case
+      int r, g, b;
+
+      if (bigEndian) {
+        r = src.position() + (24 - redShift)/8;
+        g = src.position() + (24 - greenShift)/8;
+        b = src.position() + (24 - blueShift)/8;
+      } else {
+        r = src.position() + redShift/8;
+        g = src.position() + greenShift/8;
+        b = src.position() + blueShift/8;
+      }
+
+      int srcPad = (stride - w) * 4;
+      while (h-- > 0) {
+        int w_ = w;
+        while (w_-- > 0) {
+          dst.put(src.get(r));
+          dst.put(src.get(g));
+          dst.put(src.get(b));
+          r += 4;
+          g += 4;
+          b += 4;
+        }
+        r += srcPad;
+        g += srcPad;
+        b += srcPad;
+      }
+    } else {
+      // Generic code
+      int srcPad = (stride - w) * bpp/8;
+      while (h-- > 0) {
+        int w_ = w;
+        while (w_-- > 0) {
+          int p;
+          byte r, g, b;
+
+          p = pixelFromBuffer(src.duplicate());
+
+          r = (byte)getColorModel().getRed(p);
+          g = (byte)getColorModel().getGreen(p);
+          b = (byte)getColorModel().getBlue(p);
+
+          dst.put(r);
+          dst.put(g);
+          dst.put(b);
+          src.position(src.position() + bpp/8);
+        }
+        src.reset().position(src.position() + srcPad).mark();
+      }
+    }
+  }
+
+  public void rgbFromPixels(byte[] dst, int dstPtr, int[] src, int srcPtr, int pixels, ColorModel cm)
   {
     int p;
     byte r, g, b;
 
     for (int i=0; i < pixels; i++) {
-      p = pixelFromBuffer(src, srcPtr);
-      srcPtr += bpp/8;
+      p = src[i];
 
       dst[dstPtr++] = (byte)cm.getRed(p);
       dst[dstPtr++] = (byte)cm.getGreen(p);
@@ -193,31 +345,29 @@
     }
   }
 
-  public int pixelFromBuffer(byte[] buffer, int bufferPtr)
+  public int pixelFromBuffer(ByteBuffer buffer)
   {
     int p;
 
-    p = 0;
+    p = 0xff000000;
 
-    if (bigEndian) {
+    if (!bigEndian) {
       switch (bpp) {
       case 32:
-        p = (buffer[0] & 0xff) << 24 | (buffer[1] & 0xff) << 16 | (buffer[2] & 0xff) << 8 | 0xff;
-        break;
+        p |= buffer.get() << 24;
+        p |= buffer.get() << 16;
       case 16:
-        p = (buffer[0] & 0xff) << 8 | (buffer[1] & 0xff);
-        break;
+        p |= buffer.get() << 8;
       case 8:
-        p = (buffer[0] & 0xff);
-        break;
+        p |= buffer.get();
       }
     } else {
-      p = (buffer[0] & 0xff);
+      p |= buffer.get(0);
       if (bpp >= 16) {
-        p |= (buffer[1] & 0xff) << 8;
+        p |= buffer.get(1) << 8;
         if (bpp == 32) {
-          p |= (buffer[2] & 0xff) << 16;
-          p |= (buffer[3] & 0xff) << 24;
+          p |= buffer.get(2) << 16;
+          p |= buffer.get(3) << 24;
         }
       }
     }
@@ -263,33 +413,212 @@
     return s.toString();
   }
 
-  public void bufferFromPixel(int[] buffer, int bufPtr, int p)
+  private static int bits(int value)
+  {
+    int bits;
+
+    bits = 16;
+
+    if ((value & 0xff00) == 0) {
+      bits -= 8;
+      value <<= 8;
+    }
+    if ((value & 0xf000) == 0) {
+      bits -= 4;
+      value <<= 4;
+    }
+    if ((value & 0xc000) == 0) {
+      bits -= 2;
+      value <<= 2;
+    }
+    if ((value & 0x8000) == 0) {
+      bits -= 1;
+      value <<= 1;
+    }
+
+    return bits;
+  }
+
+  private void updateState()
+  {
+    int endianTest = 1;
+
+    redBits = bits(redMax);
+    greenBits = bits(greenMax);
+    blueBits = bits(blueMax);
+
+    maxBits = redBits;
+    if (greenBits > maxBits)
+      maxBits = greenBits;
+    if (blueBits > maxBits)
+      maxBits = blueBits;
+
+    minBits = redBits;
+    if (greenBits < minBits)
+      minBits = greenBits;
+    if (blueBits < minBits)
+      minBits = blueBits;
+
+    if ((((char)endianTest) == 0) != bigEndian)
+      endianMismatch = true;
+    else
+      endianMismatch = false;
+
+    model = getColorModel(this);
+  }
+
+  private boolean isSane()
+  {
+    int totalBits;
+
+    if ((bpp != 8) && (bpp != 16) && (bpp != 32))
+      return false;
+    if (depth > bpp)
+      return false;
+
+    if (!trueColour && (depth != 8))
+      return false;
+
+    if ((redMax & (redMax + 1)) != 0)
+      return false;
+    if ((greenMax & (greenMax + 1)) != 0)
+      return false;
+    if ((blueMax & (blueMax + 1)) != 0)
+      return false;
+
+    /*
+     * We don't allow individual channels > 8 bits in order to keep our
+     * conversions simple.
+     */
+    if (redMax >= (1 << 8))
+      return false;
+    if (greenMax >= (1 << 8))
+      return false;
+    if (blueMax >= (1 << 8))
+      return false;
+
+    totalBits = bits(redMax) + bits(greenMax) + bits(blueMax);
+    if (totalBits > bpp)
+      return false;
+
+    if (((redMax << redShift) & (greenMax << greenShift)) != 0)
+      return false;
+    if (((redMax << redShift) & (blueMax << blueShift)) != 0)
+      return false;
+    if (((greenMax << greenShift) & (blueMax << blueShift)) != 0)
+      return false;
+
+    return true;
+  }
+
+  public void bufferFromPixel(ByteBuffer buffer, int p)
   {
     if (bigEndian) {
       switch (bpp) {
         case 32:
-          buffer[bufPtr++] = (p >> 24) & 0xff;
-          buffer[bufPtr++] = (p >> 16) & 0xff;
+          buffer.put((byte)((p >> 24) & 0xff));
+          buffer.put((byte)((p >> 16) & 0xff));
           break;
         case 16:
-          buffer[bufPtr++] = (p >> 8) & 0xff;
+          buffer.put((byte)((p >> 8) & 0xff));
           break;
         case 8:
-          buffer[bufPtr++] = (p >> 0) & 0xff;
+          buffer.put((byte)((p >> 0) & 0xff));
           break;
       }
     } else {
-      buffer[0] = (p >> 0) & 0xff;
+      buffer.put(0, (byte)((p >> 0) & 0xff));
       if (bpp >= 16) {
-        buffer[1] = (p >> 8) & 0xff;
+        buffer.put(1, (byte)((p >> 8) & 0xff));
         if (bpp == 32) {
-          buffer[2] = (p >> 16) & 0xff;
-          buffer[3] = (p >> 24) & 0xff;
+          buffer.put(2, (byte)((p >> 16) & 0xff));
+          buffer.put(3, (byte)((p >> 24) & 0xff));
         }
       }
     }
   }
 
+  public ColorModel getColorModel()
+  {
+    return model;
+  }
+
+  public static ColorModel getColorModel(PixelFormat pf) {
+    if (!(pf.bpp == 32) && !(pf.bpp == 16) && !(pf.bpp == 8))
+      throw new Exception("Internal error: bpp must be 8, 16, or 32 in PixelBuffer ("+pf.bpp+")");
+    ColorModel cm;
+    switch (pf.depth) {
+    case  3:
+      // Fall-through to depth 8
+    case  6:
+      // Fall-through to depth 8
+    case  8:
+      int rmask = pf.redMax << pf.redShift;
+      int gmask = pf.greenMax << pf.greenShift;
+      int bmask = pf.blueMax << pf.blueShift;
+      cm = new DirectColorModel(8, rmask, gmask, bmask);
+      break;
+    case 16:
+      cm = new DirectColorModel(32, 0xF800, 0x07C0, 0x003E);
+      break;
+    case 24:
+      cm = new DirectColorModel(32, (0xff << 16), (0xff << 8), 0xff);
+      break;
+    case 32:
+      cm = new DirectColorModel(32, (0xff << pf.redShift),
+        (0xff << pf.greenShift), (0xff << pf.blueShift));
+      break;
+    default:
+      throw new Exception("Unsupported color depth ("+pf.depth+")");
+    }
+    assert(cm != null);
+    return cm;
+  }
+
+  public ColorConvertOp getColorConvertOp(ColorSpace src)
+  {
+    // The overhead associated with initializing ColorConvertOps is
+    // enough to justify maintaining a static lookup table.
+    if (converters.containsKey(src.getType()))
+      return converters.get(src.getType());
+    ColorSpace dst = model.getColorSpace();
+    converters.put(src.getType(), new ColorConvertOp(src, dst, null));
+    return converters.get(src.getType());
+  }
+
+  public ByteOrder getByteOrder()
+  {
+    if (isBigEndian())
+      return ByteOrder.BIG_ENDIAN;
+    else
+      return ByteOrder.LITTLE_ENDIAN;
+  }
+
+  public Raster rasterFromBuffer(Rect r, ByteBuffer buf)
+  {
+    Buffer dst;
+    DataBuffer db = null;
+
+    SampleModel sm =
+      model.createCompatibleSampleModel(r.width(), r.height());
+    switch (sm.getTransferType()) {
+    case DataBuffer.TYPE_INT:
+      dst = IntBuffer.allocate(r.area()).put(buf.asIntBuffer());
+      db = new DataBufferInt(((IntBuffer)dst).array(), r.area());
+      break;
+    case DataBuffer.TYPE_BYTE:
+      db = new DataBufferByte(buf.array(), r.area());
+      break;
+    case DataBuffer.TYPE_SHORT:
+      dst = ShortBuffer.allocate(r.area()).put(buf.asShortBuffer());
+      db = new DataBufferShort(((ShortBuffer)dst).array(), r.area());
+      break;
+    }
+    assert(db != null);
+    return Raster.createRaster(sm, db, new java.awt.Point(0, 0));
+  }
+
+  private static HashMap<Integer, ColorConvertOp> converters;
 
   public int bpp;
   public int depth;
@@ -301,4 +630,10 @@
   public int redShift;
   public int greenShift;
   public int blueShift;
+
+  protected int redBits, greenBits, blueBits;
+  protected int maxBits, minBits;
+  protected boolean endianMismatch;
+
+  private ColorModel model;
 }
diff --git a/java/com/tigervnc/rfb/RREDecoder.java b/java/com/tigervnc/rfb/RREDecoder.java
index 487aa3d..c73c7a9 100644
--- a/java/com/tigervnc/rfb/RREDecoder.java
+++ b/java/com/tigervnc/rfb/RREDecoder.java
@@ -1,4 +1,5 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
+ * Copyright 2016 Brian P. Hinz
  *
  * This is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -18,30 +19,90 @@
 
 package com.tigervnc.rfb;
 
+import java.nio.*;
+
 import com.tigervnc.rdr.*;
 
 public class RREDecoder extends Decoder {
 
-  public RREDecoder(CMsgReader reader_) { reader = reader_; }
+  public RREDecoder() { super(DecoderFlags.DecoderPlain); }
 
-  public void readRect(Rect r, CMsgHandler handler) {
-    InStream is = reader.getInStream();
-    int bytesPerPixel = handler.cp.pf().bpp / 8;
-    boolean bigEndian = handler.cp.pf().bigEndian;
+  public void readRect(Rect r, InStream is,
+                       ConnParams cp, OutStream os)
+  {
+    int numRects;
+
+    numRects = is.readU32();
+    os.writeU32(numRects);
+
+    os.copyBytes(is, cp.pf().bpp/8 + numRects * (cp.pf().bpp/8 + 8));
+  }
+
+  public void decodeRect(Rect r, Object buffer,
+                         int buflen, ConnParams cp,
+                         ModifiablePixelBuffer pb)
+  {
+    MemInStream is = new MemInStream((byte[])buffer, 0, buflen);
+    PixelFormat pf = cp.pf();
+    switch (pf.bpp) {
+    case 8:  rreDecode8 (r, is, pf, pb); break;
+    case 16: rreDecode16(r, is, pf, pb); break;
+    case 32: rreDecode32(r, is, pf, pb); break;
+    }
+  }
+
+  private static ByteBuffer READ_PIXEL(InStream is, PixelFormat pf) {
+    ByteBuffer b = ByteBuffer.allocate(4);
+    switch (pf.bpp) {
+    case 8:
+      b.putInt(is.readOpaque8());
+      return ByteBuffer.allocate(1).put(b.get(3));
+    case 16:
+      b.putInt(is.readOpaque16());
+      return ByteBuffer.allocate(2).put(b.array(), 2, 2);
+    case 32:
+    default:
+      b.putInt(is.readOpaque32());
+      return b;
+    }
+  }
+
+  private void RRE_DECODE(Rect r, InStream is,
+                          PixelFormat pf, ModifiablePixelBuffer pb)
+  {
     int nSubrects = is.readU32();
-    int bg = is.readPixel(bytesPerPixel, bigEndian);
-    handler.fillRect(r, bg);
+    byte[] bg = READ_PIXEL(is, pf).array();
+    pb.fillRect(pf, r, bg);
 
     for (int i = 0; i < nSubrects; i++) {
-      int pix = is.readPixel(bytesPerPixel, bigEndian);
+      byte[] pix = READ_PIXEL(is, pf).array();
       int x = is.readU16();
       int y = is.readU16();
       int w = is.readU16();
       int h = is.readU16();
-      handler.fillRect(new Rect(r.tl.x + x, r.tl.y + y,
-                                r.tl.x + x + w, r.tl.y + y + h), pix);
+      pb.fillRect(pf, new Rect(r.tl.x+x, r.tl.y+y, r.tl.x+x+w, r.tl.y+y+h), pix);
     }
   }
 
-  CMsgReader reader;
+  private void rreDecode8(Rect r, InStream is,
+                          PixelFormat pf,
+                          ModifiablePixelBuffer pb)
+  {
+    RRE_DECODE(r, is, pf, pb);
+  }
+
+  private void rreDecode16(Rect r, InStream is,
+                           PixelFormat pf,
+                           ModifiablePixelBuffer pb)
+  {
+    RRE_DECODE(r, is, pf, pb);
+  }
+
+  private void rreDecode32(Rect r, InStream is,
+                           PixelFormat pf,
+                           ModifiablePixelBuffer pb)
+  {
+    RRE_DECODE(r, is, pf, pb);
+  }
+
 }
diff --git a/java/com/tigervnc/rfb/RawDecoder.java b/java/com/tigervnc/rfb/RawDecoder.java
index b2219a2..71b7960 100644
--- a/java/com/tigervnc/rfb/RawDecoder.java
+++ b/java/com/tigervnc/rfb/RawDecoder.java
@@ -18,28 +18,25 @@
 
 package com.tigervnc.rfb;
 
+import com.tigervnc.rdr.*;
+
 public class RawDecoder extends Decoder {
 
-  public RawDecoder(CMsgReader reader_) { reader = reader_; }
+  public RawDecoder() { super(DecoderFlags.DecoderPlain); }
 
-  public void readRect(Rect r, CMsgHandler handler) {
-    int x = r.tl.x;
-    int y = r.tl.y;
-    int w = r.width();
-    int h = r.height();
-    int[] imageBuf = new int[w*h];
-    int nPixels = imageBuf.length;
-    int bytesPerRow = w * (reader.bpp() / 8);
-    while (h > 0) {
-      int nRows = nPixels / w;
-      if (nRows > h) nRows = h;
-      reader.getInStream().readPixels(imageBuf, nPixels, (reader.bpp() / 8), handler.cp.pf().bigEndian);
-      handler.imageRect(new Rect(x, y, x+w, y+nRows), imageBuf);
-      h -= nRows;
-      y += nRows;
-    }
+  public void readRect(Rect r, InStream is,
+                       ConnParams cp, OutStream os)
+  {
+    os.copyBytes(is, r.area() * cp.pf().bpp/8);
   }
 
-  CMsgReader reader;
   static LogWriter vlog = new LogWriter("RawDecoder");
+  public void decodeRect(Rect r, Object buffer,
+                         int buflen, ConnParams cp,
+                         ModifiablePixelBuffer pb)
+  {
+    assert(buflen >= r.area() * cp.pf().bpp/8);
+    pb.imageRect(cp.pf(), r, (byte[])buffer);
+  }
+
 }
diff --git a/java/com/tigervnc/rfb/Region.java b/java/com/tigervnc/rfb/Region.java
new file mode 100644
index 0000000..f7da91d
--- /dev/null
+++ b/java/com/tigervnc/rfb/Region.java
@@ -0,0 +1,102 @@
+/* Copyright (C) 2016 Brian P. Hinz.  All Rights Reserved.
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ * USA.
+ */
+
+package com.tigervnc.rfb;
+
+import java.awt.*;
+import java.awt.geom.*;
+
+public class Region extends Area {
+
+  // Create an empty region
+  public Region() {
+    super();
+  }
+
+  // Create a rectangular region
+  public Region(Rect r) {
+    super(new Rectangle(r.tl.x, r.tl.y, r.width(), r.height()));
+  }
+
+  public Region(Region r) {
+    super(r);
+    //intersect(r);
+  }
+
+  public void clear() { reset(); }
+
+  public void reset(Rect r) {
+    if (r.is_empty()) {
+      clear();
+    } else {
+      clear();
+      assign_union(new Region(r));
+      /*
+      xrgn.numRects = 1;
+      xrgn.rects[0].x1 = xrgn.extents.x1 = r.tl.x;
+      xrgn.rects[0].y1 = xrgn.extents.y1 = r.tl.y;
+      xrgn.rects[0].x2 = xrgn.extents.x2 = r.br.x;
+      xrgn.rects[0].y2 = xrgn.extents.y2 = r.br.y;
+      */
+    }
+  }
+
+  public void translate(Point delta) {
+    AffineTransform t = 
+      AffineTransform.getTranslateInstance((double)delta.x, (double)delta.y);
+    transform(t);
+  }
+
+  public void assign_intersect(Region r) {
+    intersect(r);
+  }
+
+  public void assign_union(Region r) {
+    add(r);
+  }
+
+  public void assign_subtract(Region r) {
+    subtract(r);
+  }
+
+  public Region intersect(Region r) {
+    Region ret = new Region(this);
+    ((Area)ret).intersect(this);
+    return ret;
+  }
+
+  public Region union(Region r) {
+    Region ret = new Region(r);
+    ((Area)ret).add(this);
+    return ret;
+  }
+
+  public Region subtract(Region r) {
+    Region ret = new Region(this);
+    ((Area)ret).subtract(r);
+    return ret;
+  }
+
+  public boolean is_empty() { return isEmpty(); }
+
+  public Rect get_bounding_rect() {
+    Rectangle b = getBounds();
+    return new Rect((int)b.getX(), (int)b.getY(),
+                    (int)b.getWidth(), (int)b.getHeight());
+  }
+}
diff --git a/java/com/tigervnc/rfb/ScreenSet.java b/java/com/tigervnc/rfb/ScreenSet.java
index a14f561..173dd10 100644
--- a/java/com/tigervnc/rfb/ScreenSet.java
+++ b/java/com/tigervnc/rfb/ScreenSet.java
@@ -31,13 +31,18 @@
     screens = new ArrayList<Screen>();
   }
 
+  public final ListIterator<Screen> begin() { return screens.listIterator(0); }
+  public final ListIterator<Screen> end() {
+    return screens.listIterator(screens.size());
+  }
   public final int num_screens() { return screens.size(); }
 
   public final void add_screen(Screen screen) { screens.add(screen); }
   public final void remove_screen(int id) {
-    for (Iterator<Screen> iter = screens.iterator(); iter.hasNext(); ) {
-      Screen refScreen = (Screen)iter.next();
-      if (refScreen.id == id)
+    ListIterator iter, nextiter;
+    for (iter = begin(); iter != end(); iter = nextiter) {
+      nextiter = iter; nextiter.next();
+      if (((Screen)iter.next()).id == id)
         iter.remove();
     }
   }
@@ -68,9 +73,10 @@
   }
 
   public final void debug_print() {
+    vlog.debug(num_screens()+" screen(s)");
     for (Iterator<Screen> iter = screens.iterator(); iter.hasNext(); ) {
       Screen refScreen = (Screen)iter.next();
-      vlog.error("    "+refScreen.id+" (0x"+refScreen.id+"): "+
+      vlog.debug("    "+refScreen.id+" (0x"+refScreen.id+"): "+
                 refScreen.dimensions.width()+"x"+refScreen.dimensions.height()+
                 "+"+refScreen.dimensions.tl.x+"+"+refScreen.dimensions.tl.y+
                 " (flags 0x"+refScreen.flags+")");
diff --git a/java/com/tigervnc/rfb/Security.java b/java/com/tigervnc/rfb/Security.java
index a68ae3e..e256e6e 100644
--- a/java/com/tigervnc/rfb/Security.java
+++ b/java/com/tigervnc/rfb/Security.java
@@ -60,6 +60,8 @@
   public static final int secResultFailed = 1;
   public static final int secResultTooMany = 2; // deprecated
 
+  public Security() { }
+
   public Security(StringParameter secTypes)
   {
     String secTypesStr;
@@ -70,9 +72,9 @@
     secTypesStr = null;
   }
 
-  public static List<Integer> enabledSecTypes = new ArrayList<Integer>();
+  private List<Integer> enabledSecTypes = new ArrayList<Integer>();
 
-  public static final List<Integer> GetEnabledSecTypes()
+  public final List<Integer> GetEnabledSecTypes()
   {
     List<Integer> result = new ArrayList<Integer>();
 
@@ -98,7 +100,7 @@
     return (result);
   }
 
-  public static final List<Integer> GetEnabledExtSecTypes()
+  public final List<Integer> GetEnabledExtSecTypes()
   {
     List<Integer> result = new ArrayList<Integer>();
 
@@ -111,7 +113,7 @@
     return (result);
   }
 
-  public static final void EnableSecType(int secType)
+  public final void EnableSecType(int secType)
   {
 
     for (Iterator<Integer> i = enabledSecTypes.iterator(); i.hasNext(); )
@@ -134,7 +136,29 @@
     return false;
   }
 
-  public static void DisableSecType(int secType) { enabledSecTypes.remove((Object)secType); }
+  public String ToString()
+  {
+    Iterator<Integer> i;
+    String out = new String("");
+    boolean firstpass = true;
+    String name;
+
+    for (i = enabledSecTypes.iterator(); i.hasNext(); ) {
+      name = secTypeName((Integer)i.next());
+      if (name.startsWith("[")) /* Unknown security type */
+        continue;
+
+      if (!firstpass)
+        out = out.concat(",");
+      else
+        firstpass = false;
+      out = out.concat(name);
+    }
+
+    return out;
+  }
+
+  public void DisableSecType(int secType) { enabledSecTypes.remove((Object)secType); }
 
   public static int secTypeNum(String name) {
     if (name.equalsIgnoreCase("None"))      return secTypeNone;
@@ -203,7 +227,9 @@
     return (result);
   }
 
-  public final void SetSecTypes(List<Integer> secTypes) { enabledSecTypes = secTypes; }
+  public final void SetSecTypes(List<Integer> secTypes) {
+    enabledSecTypes = secTypes;
+  }
 
   static LogWriter vlog = new LogWriter("Security");
 }
diff --git a/java/com/tigervnc/rfb/SecurityClient.java b/java/com/tigervnc/rfb/SecurityClient.java
index 59499b1..ff2433c 100644
--- a/java/com/tigervnc/rfb/SecurityClient.java
+++ b/java/com/tigervnc/rfb/SecurityClient.java
@@ -78,9 +78,9 @@
   //UserPasswdGetter upg = null;
   String msg = null;
 
-  static StringParameter secTypes
+  public static StringParameter secTypes
   = new StringParameter("SecurityTypes",
-                        "Specify which security scheme to use (None, VncAuth)",
-                        "Ident,TLSIdent,X509Ident,X509Plain,TLSPlain,X509Vnc,TLSVnc,X509None,TLSNone,VncAuth,None", Configuration.ConfigurationObject.ConfViewer);
+                        "Specify which security scheme to use (None, VncAuth, Plain, Ident, TLSNone, TLSVnc, TLSPlain, TLSIdent, X509None, X509Vnc, X509Plain, X509Ident)",
+                        "X509Ident,X509Plain,TLSIdent,TLSPlain,X509Vnc,TLSVnc,X509None,TLSNone,Ident,VncAuth,None", Configuration.ConfigurationObject.ConfViewer);
 
 }
diff --git a/java/com/tigervnc/rfb/TightDecoder.java b/java/com/tigervnc/rfb/TightDecoder.java
index b644cdb..aa468eb 100644
--- a/java/com/tigervnc/rfb/TightDecoder.java
+++ b/java/com/tigervnc/rfb/TightDecoder.java
@@ -22,56 +22,181 @@
 package com.tigervnc.rfb;
 
 import com.tigervnc.rdr.InStream;
+import com.tigervnc.rdr.MemInStream;
+import com.tigervnc.rdr.OutStream;
 import com.tigervnc.rdr.ZlibInStream;
 import java.util.ArrayList;
 import java.io.InputStream;
 import java.awt.image.*;
 import java.awt.*;
+import java.math.BigInteger;
+import java.io.*;
+import java.nio.*;
+import javax.imageio.*;
+import javax.imageio.stream.*;
 
 public class TightDecoder extends Decoder {
 
   final static int TIGHT_MAX_WIDTH = 2048;
+  final static int TIGHT_MIN_TO_COMPRESS = 12;
 
   // Compression control
-  final static int rfbTightExplicitFilter = 0x04;
-  final static int rfbTightFill = 0x08;
-  final static int rfbTightJpeg = 0x09;
-  final static int rfbTightMaxSubencoding = 0x09;
+  final static int tightExplicitFilter = 0x04;
+  final static int tightFill = 0x08;
+  final static int tightJpeg = 0x09;
+  final static int tightMaxSubencoding = 0x09;
 
   // Filters to improve compression efficiency
-  final static int rfbTightFilterCopy = 0x00;
-  final static int rfbTightFilterPalette = 0x01;
-  final static int rfbTightFilterGradient = 0x02;
-  final static int rfbTightMinToCompress = 12;
+  final static int tightFilterCopy = 0x00;
+  final static int tightFilterPalette = 0x01;
+  final static int tightFilterGradient = 0x02;
 
-  final static Toolkit tk = Toolkit.getDefaultToolkit();
-
-  public TightDecoder(CMsgReader reader_) {
-    reader = reader_;
+  public TightDecoder() {
+    super(DecoderFlags.DecoderPartiallyOrdered);
     zis = new ZlibInStream[4];
     for (int i = 0; i < 4; i++)
       zis[i] = new ZlibInStream();
   }
 
-  public void readRect(Rect r, CMsgHandler handler)
+  public void readRect(Rect r, InStream is,
+                       ConnParams cp, OutStream os)
   {
-    InStream is = reader.getInStream();
-    boolean cutZeros = false;
-    clientpf = handler.getPreferredPF();
-    serverpf = handler.cp.pf();
-    int bpp = serverpf.bpp;
-    cutZeros = false;
-    if (bpp == 32) {
-      if (serverpf.is888()) {
-        cutZeros = true;
+    int comp_ctl;
+
+    comp_ctl = is.readU8();
+    os.writeU8(comp_ctl);
+
+    comp_ctl >>= 4;
+
+    // "Fill" compression type.
+    if (comp_ctl == tightFill) {
+      if (cp.pf().is888())
+        os.copyBytes(is, 3);
+      else
+        os.copyBytes(is, cp.pf().bpp/8);
+      return;
+    }
+
+    // "JPEG" compression type.
+    if (comp_ctl == tightJpeg) {
+      int len;
+
+      len = readCompact(is);
+      os.writeOpaque32(len);
+      os.copyBytes(is, len);
+      return;
+    }
+
+    // Quit on unsupported compression type.
+    if (comp_ctl > tightMaxSubencoding)
+      throw new Exception("TightDecoder: bad subencoding value received");
+
+    // "Basic" compression type.
+
+    int palSize = 0;
+
+    if (r.width() > TIGHT_MAX_WIDTH)
+      throw new Exception("TightDecoder: too large rectangle ("+r.width()+" pixels)");
+
+    // Possible palette
+    if ((comp_ctl & tightExplicitFilter) != 0) {
+      int filterId;
+
+      filterId = is.readU8() & 0xff;
+      os.writeU8(filterId);
+
+      switch (filterId) {
+      case tightFilterPalette:
+        palSize = is.readU8() + 1;
+        os.writeU32(palSize - 1);
+
+        if (cp.pf().is888())
+          os.copyBytes(is, palSize * 3);
+        else
+          os.copyBytes(is, palSize * cp.pf().bpp/8);
+        break;
+      case tightFilterGradient:
+        if (cp.pf().bpp == 8)
+          throw new Exception("TightDecoder: invalid BPP for gradient filter");
+        break;
+      case tightFilterCopy:
+        break;
+      default:
+        throw new Exception("TightDecoder: unknown filter code received");
       }
     }
 
-    int comp_ctl = is.readU8();
+    int rowSize, dataSize;
 
-    boolean bigEndian = handler.cp.pf().bigEndian;
+    if (palSize != 0) {
+      if (palSize <= 2)
+        rowSize = (r.width() + 7) / 8;
+      else
+        rowSize = r.width();
+    } else if (cp.pf().is888()) {
+      rowSize = r.width() * 3;
+    } else {
+      rowSize = r.width() * cp.pf().bpp/8;
+    }
 
-    // Flush zlib streams if we are told by the server to do so.
+    dataSize = r.height() * rowSize;
+
+    if (dataSize < TIGHT_MIN_TO_COMPRESS) {
+      os.copyBytes(is, dataSize);
+    } else {
+      int len;
+
+      len = readCompact(is);
+      os.writeOpaque32(len);
+      os.copyBytes(is, len);
+    }
+  }
+
+  public boolean doRectsConflict(Rect rectA,
+                                 Object bufferA,
+                                 int buflenA,
+                                 Rect rectB,
+                                 Object bufferB,
+                                 int buflenB,
+                                 ConnParams cp)
+  {
+    byte comp_ctl_a, comp_ctl_b;
+
+    assert(buflenA >= 1);
+    assert(buflenB >= 1);
+
+    comp_ctl_a = ((byte[])bufferA)[0];
+    comp_ctl_b = ((byte[])bufferB)[0];
+
+    // Resets or use of zlib pose the same problem, so merge them
+    if ((comp_ctl_a & 0x80) == 0x00)
+      comp_ctl_a |= 1 << ((comp_ctl_a >> 4) & 0x03);
+    if ((comp_ctl_b & 0x80) == 0x00)
+      comp_ctl_b |= 1 << ((comp_ctl_b >> 4) & 0x03);
+
+    if (((comp_ctl_a & 0x0f) & (comp_ctl_b & 0x0f)) != 0)
+      return true;
+
+    return false;
+  }
+
+  public void decodeRect(Rect r, Object buffer,
+                         int buflen, ConnParams cp,
+                         ModifiablePixelBuffer pb)
+  {
+    ByteBuffer bufptr;
+    PixelFormat pf = cp.pf();
+
+    int comp_ctl;
+
+    bufptr = ByteBuffer.wrap((byte[])buffer);
+
+    assert(buflen >= 1);
+
+    comp_ctl = bufptr.get() & 0xff;
+    buflen -= 1;
+
+    // Reset zlib streams if we are told by the server to do so.
     for (int i = 0; i < 4; i++) {
       if ((comp_ctl & 1) != 0) {
         zis[i].reset();
@@ -80,190 +205,216 @@
     }
 
     // "Fill" compression type.
-    if (comp_ctl == rfbTightFill) {
-      int[] pix = new int[1];
-      if (cutZeros) {
-        byte[] bytebuf = new byte[3];
-        is.readBytes(bytebuf, 0, 3);
-        serverpf.bufferFromRGB(pix, 0, bytebuf, 0, 1);
+    if (comp_ctl == tightFill) {
+      if (pf.is888()) {
+        ByteBuffer pix = ByteBuffer.allocate(4);
+
+        assert(buflen >= 3);
+
+        pf.bufferFromRGB(pix, bufptr, 1);
+        pb.fillRect(pf, r, pix.array());
       } else {
-        pix[0] = is.readPixel(serverpf.bpp/8, serverpf.bigEndian);
+        assert(buflen >= pf.bpp/8);
+        byte[] pix = new byte[pf.bpp/8];
+        bufptr.get(pix);
+        pb.fillRect(pf, r, pix);
       }
-      handler.fillRect(r, pix[0]);
       return;
     }
 
     // "JPEG" compression type.
-    if (comp_ctl == rfbTightJpeg) {
-      DECOMPRESS_JPEG_RECT(r, is, handler);
+    if (comp_ctl == tightJpeg) {
+      int len;
+
+      WritableRaster buf;
+
+      JpegDecompressor jd = new JpegDecompressor();
+
+      assert(buflen >= 4);
+
+      len = bufptr.getInt();
+      buflen -= 4;
+
+      // We always use direct decoding with JPEG images
+      buf = pb.getBufferRW(r);
+      jd.decompress(bufptr, len, buf, r, pb.getPF());
+      pb.commitBufferRW(r);
       return;
     }
 
     // Quit on unsupported compression type.
-    if (comp_ctl > rfbTightMaxSubencoding) {
+    if (comp_ctl > tightMaxSubencoding)
       throw new Exception("TightDecoder: bad subencoding value received");
-    }
 
     // "Basic" compression type.
     int palSize = 0;
-    int[] palette = new int[256];
+    ByteBuffer palette = ByteBuffer.allocate(256 * 4);
     boolean useGradient = false;
 
-    if ((comp_ctl & rfbTightExplicitFilter) != 0) {
-      int filterId = is.readU8();
+    if ((comp_ctl & tightExplicitFilter) != 0) {
+      int filterId;
+
+      assert(buflen >= 1);
+
+      filterId = bufptr.get();
 
       switch (filterId) {
-      case rfbTightFilterPalette:
-        palSize = is.readU8() + 1;
-        byte[] tightPalette;
-        if (cutZeros) {
-          tightPalette = new byte[256 * 3];
-          is.readBytes(tightPalette, 0, palSize * 3);
-          serverpf.bufferFromRGB(palette, 0, tightPalette, 0, palSize);
+      case tightFilterPalette:
+        assert(buflen >= 1);
+
+        palSize = bufptr.getInt() + 1;
+        buflen -= 4;
+
+        if (pf.is888()) {
+          ByteBuffer tightPalette = ByteBuffer.allocate(palSize * 3);
+
+          assert(buflen >= tightPalette.capacity());
+
+          bufptr.get(tightPalette.array(), 0, tightPalette.capacity());
+          buflen -= tightPalette.capacity();
+
+          pf.bufferFromRGB(palette.duplicate(), tightPalette, palSize);
         } else {
-          is.readPixels(palette, palSize, serverpf.bpp/8, serverpf.bigEndian);
+          int len;
+
+          len = palSize * pf.bpp/8;
+
+          assert(buflen >= len);
+
+          bufptr.get(palette.array(), 0, len);
+          buflen -= len;
         }
         break;
-      case rfbTightFilterGradient:
+      case tightFilterGradient:
         useGradient = true;
         break;
-      case rfbTightFilterCopy:
+      case tightFilterCopy:
         break;
       default:
-        throw new Exception("TightDecoder: unknown filter code recieved");
+        assert(false);
       }
     }
 
-    int bppp = bpp;
-    if (palSize != 0) {
-      bppp = (palSize <= 2) ? 1 : 8;
-    } else if (cutZeros) {
-      bppp = 24;
-    }
-
     // Determine if the data should be decompressed or just copied.
-    int rowSize = (r.width() * bppp + 7) / 8;
-    int dataSize = r.height() * rowSize;
-    int streamId = -1;
-    InStream input;
-    if (dataSize < rfbTightMinToCompress) {
-      input = is;
+    int rowSize, dataSize;
+    byte[] netbuf;
+
+    if (palSize != 0) {
+      if (palSize <= 2)
+        rowSize = (r.width() + 7) / 8;
+      else
+        rowSize = r.width();
+    } else if (pf.is888()) {
+      rowSize = r.width() * 3;
     } else {
-      int length = is.readCompactLength();
-      streamId = comp_ctl & 0x03;
-      zis[streamId].setUnderlying(is, length);
-      input = (ZlibInStream)zis[streamId];
+      rowSize = r.width() * pf.bpp/8;
     }
 
-    // Allocate netbuf and read in data
-    byte[] netbuf = new byte[dataSize];
-    input.readBytes(netbuf, 0, dataSize);
+    dataSize = r.height() * rowSize;
 
+    if (dataSize < TIGHT_MIN_TO_COMPRESS) {
+      assert(buflen >= dataSize);
+    } else {
+      int len;
+      int streamId;
+      MemInStream ms;
+
+      assert(buflen >= 4);
+
+      len = bufptr.getInt();
+      buflen -= 4;
+
+      assert(buflen >= len);
+
+      streamId = comp_ctl & 0x03;
+      ms = new MemInStream(bufptr.array(), bufptr.position(), len);
+      zis[streamId].setUnderlying(ms, len);
+
+      // Allocate netbuf and read in data
+      netbuf = new byte[dataSize];
+
+      zis[streamId].readBytes(netbuf, 0, dataSize);
+
+      zis[streamId].removeUnderlying();
+      ms = null;
+
+      bufptr = ByteBuffer.wrap(netbuf);
+      buflen = dataSize;
+    }
+
+    ByteBuffer outbuf = ByteBuffer.allocate(r.area() * pf.bpp/8);
     int stride = r.width();
-    int[] buf = reader.getImageBuf(r.area());
 
     if (palSize == 0) {
       // Truecolor data.
       if (useGradient) {
-        if (bpp == 32 && cutZeros) {
-          FilterGradient24(netbuf, buf, stride, r);
+        if (pf.is888()) {
+          FilterGradient24(bufptr, pf, outbuf, stride, r);
         } else {
-          FilterGradient(netbuf, buf, stride, r);
+          switch (pf.bpp) {
+          case 8:
+            assert(false);
+            break;
+          case 16:
+            FilterGradient(bufptr, pf, outbuf, stride, r);
+            break;
+          case 32:
+            FilterGradient(bufptr, pf, outbuf, stride, r);
+            break;
+          }
         }
       } else {
         // Copy
-        int h = r.height();
-        int ptr = 0;
-        int srcPtr = 0;
+        ByteBuffer ptr = (ByteBuffer)outbuf.duplicate().mark();
+        ByteBuffer srcPtr = bufptr.duplicate();
         int w = r.width();
-        if (cutZeros) {
-          serverpf.bufferFromRGB(buf, ptr, netbuf, srcPtr, w*h);
-        } else {
-          int pixelSize = (bpp >= 24) ? 3 : bpp/8;
+        int h = r.height();
+        if (pf.is888()) {
           while (h > 0) {
-            for (int i = 0; i < w; i++) {
-              if (bpp == 8) {
-                buf[ptr+i] = netbuf[srcPtr+i] & 0xff;
-              } else {
-                for (int j = pixelSize-1; j >= 0; j--)
-                  buf[ptr+i] |= ((netbuf[srcPtr+i+j] & 0xff) << j*8);
-              }
-            }
-            ptr += stride;
-            srcPtr += w * pixelSize;
+            pf.bufferFromRGB(ptr.duplicate(), srcPtr.duplicate(), w);
+            ptr.position(ptr.position() + stride * pf.bpp/8);
+            srcPtr.position(srcPtr.position() + w * 3);
+            h--;
+          }
+        } else {
+          while (h > 0) {
+            ptr.put(srcPtr.array(), srcPtr.position(), w * pf.bpp/8);
+            ptr.reset().position(ptr.position() + stride * pf.bpp/8).mark();
+            srcPtr.position(srcPtr.position() + w * pf.bpp/8);
             h--;
           }
         }
       }
     } else {
       // Indexed color
-      int x, h = r.height(), w = r.width(), b, pad = stride - w;
-      int ptr = 0;
-      int srcPtr = 0, bits;
-      if (palSize <= 2) {
-        // 2-color palette
-        while (h > 0) {
-          for (x = 0; x < w / 8; x++) {
-            bits = netbuf[srcPtr++];
-            for(b = 7; b >= 0; b--) {
-              buf[ptr++] = palette[bits >> b & 1];
-            }
-          }
-          if (w % 8 != 0) {
-            bits = netbuf[srcPtr++];
-            for (b = 7; b >= 8 - w % 8; b--) {
-              buf[ptr++] = palette[bits >> b & 1];
-            }
-          }
-          ptr += pad;
-          h--;
-        }
-      } else {
-        // 256-color palette
-        while (h > 0) {
-          int endOfRow = ptr + w;
-          while (ptr < endOfRow) {
-            buf[ptr++] = palette[netbuf[srcPtr++] & 0xff];
-          }
-          ptr += pad;
-          h--;
-        }
+      switch (pf.bpp) {
+      case 8:
+        FilterPalette8(palette, palSize,
+                       bufptr, outbuf, stride, r);
+        break;
+      case 16:
+        FilterPalette16(palette.asShortBuffer(), palSize,
+                        bufptr, outbuf.asShortBuffer(), stride, r);
+        break;
+      case 32:
+        FilterPalette32(palette.asIntBuffer(), palSize,
+                        bufptr, outbuf.asIntBuffer(), stride, r);
+        break;
       }
     }
 
-    handler.imageRect(r, buf);
+    pb.imageRect(pf, r, outbuf.array());
 
-    if (streamId != -1) {
-      zis[streamId].reset();
-    }
   }
 
-  final private void DECOMPRESS_JPEG_RECT(Rect r, InStream is, CMsgHandler handler)
+  final private void FilterGradient24(ByteBuffer inbuf,
+                                      PixelFormat pf, ByteBuffer outbuf,
+                                      int stride, Rect r)
   {
-    // Read length
-    int compressedLen = is.readCompactLength();
-    if (compressedLen <= 0)
-      vlog.info("Incorrect data received from the server.");
-
-    // Allocate netbuf and read in data
-    byte[] netbuf = new byte[compressedLen];
-    is.readBytes(netbuf, 0, compressedLen);
-
-    // Create an Image object from the JPEG data.
-    Image jpeg = tk.createImage(netbuf);
-    jpeg.setAccelerationPriority(1);
-    handler.imageRect(r, jpeg);
-    jpeg.flush();
-  }
-
-  final private void FilterGradient24(byte[] netbuf, int[] buf, int stride,
-                                      Rect r)
-  {
-
     int x, y, c;
     byte[] prevRow = new byte[TIGHT_MAX_WIDTH*3];
     byte[] thisRow = new byte[TIGHT_MAX_WIDTH*3];
-    byte[] pix = new byte[3];
+    ByteBuffer pix = ByteBuffer.allocate(3);
     int[] est = new int[3];
 
     // Set up shortcut variables
@@ -273,38 +424,38 @@
     for (y = 0; y < rectHeight; y++) {
       /* First pixel in a row */
       for (c = 0; c < 3; c++) {
-        pix[c] = (byte)(netbuf[y*rectWidth*3+c] + prevRow[c]);
-        thisRow[c] = pix[c];
+        pix.put(c, (byte)(inbuf.get(y*rectWidth*3+c) + prevRow[c]));
+        thisRow[c] = pix.get(c);
       }
-      serverpf.bufferFromRGB(buf, y*stride, pix, 0, 1);
+      pf.bufferFromRGB((ByteBuffer)outbuf.position(y*stride), pix, 1);
 
       /* Remaining pixels of a row */
       for (x = 1; x < rectWidth; x++) {
         for (c = 0; c < 3; c++) {
-          est[c] = (int)(prevRow[x*3+c] + pix[c] - prevRow[(x-1)*3+c]);
-          if (est[c] > 0xFF) {
-            est[c] = 0xFF;
+          est[c] = prevRow[x*3+c] + pix.get(c) - prevRow[(x-1)*3+c];
+          if (est[c] > 0xff) {
+            est[c] = 0xff;
           } else if (est[c] < 0) {
             est[c] = 0;
           }
-          pix[c] = (byte)(netbuf[(y*rectWidth+x)*3+c] + est[c]);
-          thisRow[x*3+c] = pix[c];
+          pix.put(c, (byte)(inbuf.get((y*rectWidth+x)*3+c) + est[c]));
+          thisRow[x*3+c] = pix.get(c);
         }
-        serverpf.bufferFromRGB(buf, y*stride+x, pix, 0, 1);
+        pf.bufferFromRGB((ByteBuffer)outbuf.position(y*stride+x), pix, 1);
       }
 
       System.arraycopy(thisRow, 0, prevRow, 0, prevRow.length);
     }
   }
 
-  final private void FilterGradient(byte[] netbuf, int[] buf, int stride,
-                                    Rect r)
+  final private void FilterGradient(ByteBuffer inbuf,
+                                    PixelFormat pf, ByteBuffer outbuf,
+                                    int stride, Rect r)
   {
-
     int x, y, c;
     byte[] prevRow = new byte[TIGHT_MAX_WIDTH];
     byte[] thisRow = new byte[TIGHT_MAX_WIDTH];
-    byte[] pix = new byte[3];
+    ByteBuffer pix = ByteBuffer.allocate(3);
     int[] est = new int[3];
 
     // Set up shortcut variables
@@ -313,19 +464,18 @@
 
     for (y = 0; y < rectHeight; y++) {
       /* First pixel in a row */
-      // FIXME
-      //serverpf.rgbFromBuffer(pix, 0, netbuf, y*rectWidth, 1, cm);
+      pf.rgbFromBuffer(pix, (ByteBuffer)inbuf.position(y*rectWidth), 1);
       for (c = 0; c < 3; c++)
-        pix[c] += prevRow[c];
+        pix.put(c, (byte)(pix.get(c) + prevRow[c]));
 
-      System.arraycopy(pix, 0, thisRow, 0, pix.length);
+      System.arraycopy(pix.array(), 0, thisRow, 0, pix.capacity());
 
-      serverpf.bufferFromRGB(buf, y*stride, pix, 0, 1);
+      pf.bufferFromRGB((ByteBuffer)outbuf.position(y*stride), pix, 1);
 
       /* Remaining pixels of a row */
       for (x = 1; x < rectWidth; x++) {
         for (c = 0; c < 3; c++) {
-          est[c] = (int)(prevRow[x*3+c] + pix[c] - prevRow[(x-1)*3+c]);
+          est[c] = prevRow[x*3+c] + pix.get(c) - prevRow[(x-1)*3+c];
           if (est[c] > 0xff) {
             est[c] = 0xff;
           } else if (est[c] < 0) {
@@ -333,24 +483,156 @@
           }
         }
 
-        // FIXME
-        //serverpf.rgbFromBuffer(pix, 0, netbuf, y*rectWidth+x, 1, cm);
+        pf.rgbFromBuffer(pix, (ByteBuffer)inbuf.position(y*rectWidth+x), 1);
         for (c = 0; c < 3; c++)
-          pix[c] += est[c];
+          pix.put(c, (byte)(pix.get(c) + est[c]));
 
-        System.arraycopy(pix, 0, thisRow, x*3, pix.length);
+        System.arraycopy(pix.array(), 0, thisRow, x*3, pix.capacity());
 
-        serverpf.bufferFromRGB(buf, y*stride+x, pix, 0, 1);
+        pf.bufferFromRGB((ByteBuffer)outbuf.position(y*stride+x), pix, 1);
       }
 
       System.arraycopy(thisRow, 0, prevRow, 0, prevRow.length);
     }
   }
 
-  private CMsgReader reader;
+  private void FilterPalette8(ByteBuffer palette, int palSize,
+                              ByteBuffer inbuf, ByteBuffer outbuf,
+                              int stride, Rect r)
+  {
+    // Indexed color
+    int x, h = r.height(), w = r.width(), b, pad = stride - w;
+    ByteBuffer ptr = outbuf.duplicate();
+    byte bits;
+    ByteBuffer srcPtr = inbuf.duplicate();
+    if (palSize <= 2) {
+      // 2-color palette
+      while (h > 0) {
+        for (x = 0; x < w / 8; x++) {
+          bits = srcPtr.get();
+          for (b = 7; b >= 0; b--) {
+            ptr.put(palette.get(bits >> b & 1));
+          }
+        }
+        if (w % 8 != 0) {
+          bits = srcPtr.get();
+          for (b = 7; b >= 8 - w % 8; b--) {
+            ptr.put(palette.get(bits >> b & 1));
+          }
+        }
+        ptr.position(ptr.position() + pad);
+        h--;
+      }
+    } else {
+      // 256-color palette
+      while (h > 0) {
+        int endOfRow = ptr.position() + w;
+        while (ptr.position() < endOfRow) {
+          ptr.put(palette.get(srcPtr.get()));
+        }
+        ptr.position(ptr.position() + pad);
+        h--;
+      }
+    }
+  }
+
+  private void FilterPalette16(ShortBuffer palette, int palSize,
+                               ByteBuffer inbuf, ShortBuffer outbuf,
+                               int stride, Rect r)
+  {
+    // Indexed color
+    int x, h = r.height(), w = r.width(), b, pad = stride - w;
+    ShortBuffer ptr = outbuf.duplicate();
+    byte bits;
+    ByteBuffer srcPtr = inbuf.duplicate();
+    if (palSize <= 2) {
+      // 2-color palette
+      while (h > 0) {
+        for (x = 0; x < w / 8; x++) {
+          bits = srcPtr.get();
+          for (b = 7; b >= 0; b--) {
+            ptr.put(palette.get(bits >> b & 1));
+          }
+        }
+        if (w % 8 != 0) {
+          bits = srcPtr.get();
+          for (b = 7; b >= 8 - w % 8; b--) {
+            ptr.put(palette.get(bits >> b & 1));
+          }
+        }
+        ptr.position(ptr.position() + pad);
+        h--;
+      }
+    } else {
+      // 256-color palette
+      while (h > 0) {
+        int endOfRow = ptr.position() + w;
+        while (ptr.position() < endOfRow) {
+          ptr.put(palette.get(srcPtr.get()));
+        }
+        ptr.position(ptr.position() + pad);
+        h--;
+      }
+    }
+  }
+
+  private void FilterPalette32(IntBuffer palette, int palSize,
+                               ByteBuffer inbuf, IntBuffer outbuf,
+                               int stride, Rect r)
+  {
+    // Indexed color
+    int x, h = r.height(), w = r.width(), b, pad = stride - w;
+    IntBuffer ptr = outbuf.duplicate();
+    byte bits;
+    ByteBuffer srcPtr = inbuf.duplicate();
+    if (palSize <= 2) {
+      // 2-color palette
+      while (h > 0) {
+        for (x = 0; x < w / 8; x++) {
+          bits = srcPtr.get();
+          for (b = 7; b >= 0; b--) {
+            ptr.put(palette.get(bits >> b & 1));
+          }
+        }
+        if (w % 8 != 0) {
+          bits = srcPtr.get();
+          for (b = 7; b >= 8 - w % 8; b--) {
+            ptr.put(palette.get(bits >> b & 1));
+          }
+        }
+        ptr.position(ptr.position() + pad);
+        h--;
+      }
+    } else {
+      // 256-color palette
+      while (h > 0) {
+        int endOfRow = ptr.position() + w;
+        while (ptr.position() < endOfRow) {
+          ptr.put(palette.get(srcPtr.get() & 0xff));
+        }
+        ptr.position(ptr.position() + pad);
+        h--;
+      }
+    }
+  }
+
+  public final int readCompact(InStream is) {
+    byte b;
+    int result;
+
+    b = (byte)is.readU8();
+    result = (int)b & 0x7F;
+    if ((b & 0x80) != 0) {
+      b = (byte)is.readU8();
+      result |= ((int)b & 0x7F) << 7;
+      if ((b & 0x80) != 0) {
+        b = (byte)is.readU8();
+        result |= ((int)b & 0xFF) << 14;
+      }
+    }
+    return result;
+  }
+
   private ZlibInStream[] zis;
-  private PixelFormat serverpf;
-  private PixelFormat clientpf;
-  static LogWriter vlog = new LogWriter("TightDecoder");
 
 }
diff --git a/java/com/tigervnc/rfb/ZRLEDecoder.java b/java/com/tigervnc/rfb/ZRLEDecoder.java
index e706510..c1f908a 100644
--- a/java/com/tigervnc/rfb/ZRLEDecoder.java
+++ b/java/com/tigervnc/rfb/ZRLEDecoder.java
@@ -18,22 +18,143 @@
 
 package com.tigervnc.rfb;
 
+import java.awt.image.*;
+import java.nio.*;
+import java.util.*;
+
 import com.tigervnc.rdr.*;
 
 public class ZRLEDecoder extends Decoder {
 
-  public ZRLEDecoder(CMsgReader reader_) {
-    reader = reader_;
+  private static int readOpaque24A(InStream is)
+  {
+    is.check(3);
+    ByteBuffer r = ByteBuffer.allocate(4);
+    r.put(0, (byte)is.readU8());
+    r.put(1, (byte)is.readU8());
+    r.put(2, (byte)is.readU8());
+    return ((ByteBuffer)r.rewind()).getInt();
+  }
+
+  private static int readOpaque24B(InStream is)
+  {
+    is.check(3);
+    ByteBuffer r = ByteBuffer.allocate(4);
+    r.put(2, (byte)is.readU8());
+    r.put(1, (byte)is.readU8());
+    r.put(0, (byte)is.readU8());
+    return ((ByteBuffer)r.rewind()).getInt();
+  }
+
+  public ZRLEDecoder() {
+    super(DecoderFlags.DecoderOrdered);
     zis = new ZlibInStream();
   }
 
-  public void readRect(Rect r, CMsgHandler handler) {
-    InStream is = reader.getInStream();
-    int[] buf = reader.getImageBuf(64 * 64 * 4);
-    int bpp = handler.cp.pf().bpp;
-    int bytesPerPixel = (bpp > 24 ? 3 : bpp / 8);
-    boolean bigEndian = handler.cp.pf().bigEndian;
+  public void readRect(Rect r, InStream is,
+                      ConnParams cp, OutStream os)
+  {
+    int len;
 
+    len = is.readU32();
+    os.writeU32(len);
+    os.copyBytes(is, len);
+  }
+
+  public void decodeRect(Rect r, Object buffer,
+                         int buflen, ConnParams cp,
+                         ModifiablePixelBuffer pb)
+  {
+    MemInStream is = new MemInStream((byte[])buffer, 0, buflen);
+    PixelFormat pf = cp.pf();
+    ByteBuffer buf = ByteBuffer.allocate(64 * 64 * 4);
+    switch (pf.bpp) {
+    case 8:  zrleDecode8(r, is, zis, buf, pf, pb); break;
+    case 16: zrleDecode16(r, is, zis, buf, pf, pb); break;
+    case 32:
+        int maxPixel = pf.pixelFromRGB(-1, -1, -1, pf.getColorModel());
+        boolean fitsInLS3Bytes = maxPixel < (1<<24);
+        boolean fitsInMS3Bytes = (maxPixel & 0xff) == 0;
+
+        if ((fitsInLS3Bytes && pf.isLittleEndian()) ||
+            (fitsInMS3Bytes && pf.isBigEndian()))
+        {
+          zrleDecode24A(r, is, zis, buf, pf, pb);
+        }
+        else if ((fitsInLS3Bytes && pf.isBigEndian()) ||
+                (fitsInMS3Bytes && pf.isLittleEndian()))
+        {
+          zrleDecode24B(r, is, zis, buf, pf, pb);
+        }
+        else
+        {
+          zrleDecode32(r, is, zis, buf, pf, pb);
+        }
+        break;
+    }
+  }
+
+  private static enum PIXEL_T { U8, U16, U24A, U24B, U32 };
+
+  private static ByteBuffer READ_PIXEL(InStream is, PIXEL_T type) {
+    ByteBuffer b = ByteBuffer.allocate(4);
+    switch (type) {
+    case U8:
+      b.putInt(is.readOpaque8());
+      return (ByteBuffer)ByteBuffer.allocate(1).put(b.get(3)).rewind();
+    case U16:
+      b.putInt(is.readOpaque16());
+      return (ByteBuffer)ByteBuffer.allocate(2).put(b.array(), 2, 2).rewind();
+    case U24A:
+      return (ByteBuffer)b.putInt(readOpaque24A(is)).rewind();
+    case U24B:
+      return (ByteBuffer)b.putInt(readOpaque24B(is)).rewind();
+    case U32:
+    default:
+      return (ByteBuffer)b.putInt(is.readOpaque32()).rewind();
+    }
+  }
+
+  private void zrleDecode8(Rect r, InStream is,
+                           ZlibInStream zis, ByteBuffer buf,
+                           PixelFormat pf, ModifiablePixelBuffer pb)
+  {
+    ZRLE_DECODE(r, is, zis, buf, pf, pb, PIXEL_T.U8);
+  }
+
+  private void zrleDecode16(Rect r, InStream is,
+                            ZlibInStream zis, ByteBuffer buf,
+                            PixelFormat pf, ModifiablePixelBuffer pb)
+  {
+    ZRLE_DECODE(r, is, zis, buf, pf, pb, PIXEL_T.U16);
+  }
+
+  private void zrleDecode24A(Rect r, InStream is,
+                             ZlibInStream zis, ByteBuffer buf,
+                             PixelFormat pf, ModifiablePixelBuffer pb)
+  {
+    ZRLE_DECODE(r, is, zis, buf, pf, pb, PIXEL_T.U24A);
+  }
+
+  private void zrleDecode24B(Rect r, InStream is,
+                             ZlibInStream zis, ByteBuffer buf,
+                             PixelFormat pf, ModifiablePixelBuffer pb)
+  {
+    ZRLE_DECODE(r, is, zis, buf, pf, pb, PIXEL_T.U24B);
+  }
+
+  private void zrleDecode32(Rect r, InStream is,
+                            ZlibInStream zis, ByteBuffer buf,
+                            PixelFormat pf, ModifiablePixelBuffer pb)
+  {
+    ZRLE_DECODE(r, is, zis, buf, pf, pb, PIXEL_T.U32);
+  }
+
+  private void ZRLE_DECODE(Rect r, InStream is,
+                           ZlibInStream zis, ByteBuffer buf,
+                           PixelFormat pf, ModifiablePixelBuffer pb,
+                           PIXEL_T pix_t)
+  {
     int length = is.readU32();
     zis.setUnderlying(is, length);
     Rect t = new Rect();
@@ -49,13 +170,16 @@
         int mode = zis.readU8();
         boolean rle = (mode & 128) != 0;
         int palSize = mode & 127;
-        int[] palette = new int[128];
+        ByteBuffer palette = ByteBuffer.allocate(128 * pf.bpp/8);
 
-        zis.readPixels(palette, palSize, bytesPerPixel, bigEndian);
+        for (int i = 0; i < palSize; i++) {
+          palette.put(READ_PIXEL(zis, pix_t));
+        }
 
         if (palSize == 1) {
-          int pix = palette[0];
-          handler.fillRect(t, pix);
+          ByteBuffer pix = 
+            ByteBuffer.allocate(pf.bpp/8).put(palette.array(), 0, pf.bpp/8);
+          pb.fillRect(pf, t, pix.array());
           continue;
         }
 
@@ -63,8 +187,17 @@
           if (palSize == 0) {
 
             // raw
-
-            zis.readPixels(buf, t.area(), bytesPerPixel, bigEndian);
+            switch (pix_t) {
+            case U24A:
+            case U24B:
+              ByteBuffer ptr = buf.duplicate();
+              for (int iptr=0; iptr < t.area(); iptr++) {
+                ptr.put(READ_PIXEL(zis, pix_t));
+              }
+              break;
+            default:
+              zis.readBytes(buf, t.area() * (pf.bpp/8));
+            }
 
           } else {
 
@@ -72,21 +205,21 @@
             int bppp = ((palSize > 16) ? 8 :
                         ((palSize > 4) ? 4 : ((palSize > 2) ? 2 : 1)));
 
-            int ptr = 0;
+            ByteBuffer ptr = buf.duplicate();
 
             for (int i = 0; i < t.height(); i++) {
-              int eol = ptr + t.width();
+              int eol = ptr.position() + t.width()*pf.bpp/8;
               int b = 0;
               int nbits = 0;
 
-              while (ptr < eol) {
+              while (ptr.position() < eol) {
                 if (nbits == 0) {
                   b = zis.readU8();
                   nbits = 8;
                 }
                 nbits -= bppp;
                 int index = (b >> nbits) & ((1 << bppp) - 1) & 127;
-                buf[ptr++] = palette[index];
+                ptr.put(palette.array(), index*pf.bpp/8, pf.bpp/8);
               }
             }
           }
@@ -97,10 +230,10 @@
 
             // plain RLE
 
-            int ptr = 0;
-            int end = ptr + t.area();
-            while (ptr < end) {
-              int pix = zis.readPixel(bytesPerPixel, bigEndian);
+            ByteBuffer ptr = buf.duplicate();
+            int end = ptr.position() + t.area()*pf.bpp/8;
+            while (ptr.position() < end) {
+              ByteBuffer pix = READ_PIXEL(zis, pix_t);
               int len = 1;
               int b;
               do {
@@ -108,19 +241,21 @@
                 len += b;
               } while (b == 255);
 
-              if (!(len <= end - ptr))
-                throw new Exception("ZRLEDecoder: assertion (len <= end - ptr)"
-                                    +" failed");
+              if (end - ptr.position() < len*(pf.bpp/8)) {
+                System.err.println("ZRLE decode error\n");
+                throw new Exception("ZRLE decode error");
+              }
 
-              while (len-- > 0) buf[ptr++] = pix;
+              while (len-- > 0) ptr.put(pix);
+
             }
           } else {
 
             // palette RLE
 
-            int ptr = 0;
-            int end = ptr + t.area();
-            while (ptr < end) {
+            ByteBuffer ptr = buf.duplicate();
+            int end = ptr.position() + t.area()*pf.bpp/8;
+            while (ptr.position() < end) {
               int index = zis.readU8();
               int len = 1;
               if ((index & 128) != 0) {
@@ -130,27 +265,26 @@
                   len += b;
                 } while (b == 255);
 
-                if (!(len <= end - ptr))
-                  throw new Exception("ZRLEDecoder: assertion "
-                                      +"(len <= end - ptr) failed");
+                if (end - ptr.position() < len*(pf.bpp/8)) {
+                  System.err.println("ZRLE decode error\n");
+                  throw new Exception("ZRLE decode error");
+                }
               }
 
               index &= 127;
 
-              int pix = palette[index];
+              while (len-- > 0) ptr.put(palette.array(), index*pf.bpp/8, pf.bpp/8);
 
-              while (len-- > 0) buf[ptr++] = pix;
             }
           }
         }
 
-        handler.imageRect(t, buf);
+        pb.imageRect(pf, t, buf.array());
       }
     }
 
-    zis.reset();
+    zis.removeUnderlying();
   }
 
-  CMsgReader reader;
-  ZlibInStream zis;
+  private ZlibInStream zis;
 }
diff --git a/java/com/tigervnc/vncviewer/BIPixelBuffer.java b/java/com/tigervnc/vncviewer/BIPixelBuffer.java
deleted file mode 100644
index 9612b36..0000000
--- a/java/com/tigervnc/vncviewer/BIPixelBuffer.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/* Copyright (C) 2012 Brian P. Hinz
- * Copyright (C) 2012 D. R. Commander.  All Rights Reserved.
- *
- * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
- * USA.
- */
-
-package com.tigervnc.vncviewer;
-
-import java.awt.*;
-import java.awt.image.*;
-
-import com.tigervnc.rfb.*;
-import com.tigervnc.rfb.Exception;
-
-public class BIPixelBuffer extends PlatformPixelBuffer implements ImageObserver
-{
-  public BIPixelBuffer(int w, int h, CConn cc_, DesktopWindow desktop_) {
-    super(w, h, cc_, desktop_);
-    clip = new Rectangle();
-  }
-
-  public void setPF(PixelFormat pf) {
-    super.setPF(pf);
-    createImage(width(), height());
-  }
-
-  public void updateColourMap() {
-    super.updateColourMap();
-    createImage(width_, height_);
-  }
-
-  // resize() resizes the image, preserving the image data where possible.
-  public void resize(int w, int h) {
-    if (w == width() && h == height())
-      return;
-
-    width_ = w;
-    height_ = h;
-    createImage(w, h);
-  }
-
-  private void createImage(int w, int h) {
-    if (w == 0 || h == 0) return;
-    WritableRaster wr;
-    if (cm instanceof IndexColorModel)
-      wr = ((IndexColorModel)cm).createCompatibleWritableRaster(w, h);
-    else
-      wr = ((DirectColorModel)cm).createCompatibleWritableRaster(w, h);
-    image = new BufferedImage(cm, wr, true, null);
-    db = wr.getDataBuffer();
-  }
-
-  public void fillRect(int x, int y, int w, int h, int pix) {
-    Graphics2D graphics = (Graphics2D)image.getGraphics();
-    switch (format.depth) {
-    case 24:
-      graphics.setColor(new Color(pix));
-      graphics.fillRect(x, y, w, h);
-      break;
-    default:
-      Color color = new Color((0xff << 24) | (cm.getRed(pix) << 16) |
-                              (cm.getGreen(pix) << 8) | (cm.getBlue(pix)));
-      graphics.setColor(color);
-      graphics.fillRect(x, y, w, h);
-      break;
-    }
-    graphics.dispose();
-  }
-
-  public void imageRect(int x, int y, int w, int h, Object pix) {
-    if (pix instanceof Image) {
-      Image img = (Image)pix;
-      clip = new Rectangle(x, y, w, h);
-      synchronized(clip) {
-        tk.prepareImage(img, -1, -1, this);
-        try {
-          clip.wait(1000);
-        } catch (InterruptedException e) {
-          throw new Exception("Error decoding JPEG data");
-        }
-      }
-      clip = null;
-      img.flush();
-    } else {
-      if (image.getSampleModel().getTransferType() == DataBuffer.TYPE_BYTE) {
-        byte[] bytes = new byte[((int[])pix).length];
-        for (int i = 0; i < bytes.length; i++)
-          bytes[i] = (byte)((int[])pix)[i];
-        pix = bytes;
-      }
-      image.getSampleModel().setDataElements(x, y, w, h, pix, db);
-    }
-  }
-
-  public void copyRect(int x, int y, int w, int h, int srcX, int srcY) {
-    Graphics2D graphics = (Graphics2D)image.getGraphics();
-    graphics.copyArea(srcX, srcY, w, h, x - srcX, y - srcY);
-    graphics.dispose();
-  }
-
-  public Image getImage() {
-    return (Image)image;
-  }
-
-  public boolean imageUpdate(Image img, int infoflags, int x, int y, int w, int h) {
-    if ((infoflags & (ALLBITS | ABORT)) == 0) {
-      return true;
-    } else {
-      if ((infoflags & ALLBITS) != 0) {
-        if (clip != null) {
-          synchronized(clip) {
-            Graphics2D graphics = (Graphics2D)image.getGraphics();
-            graphics.drawImage(img, clip.x, clip.y, clip.width, clip.height, null);
-            graphics.dispose();
-            clip.notify();
-          }
-        }
-      }
-      return false;
-    }
-  }
-
-  BufferedImage image;
-  DataBuffer db;
-  Rectangle clip;
-
-  static LogWriter vlog = new LogWriter("BIPixelBuffer");
-}
diff --git a/java/com/tigervnc/vncviewer/CConn.java b/java/com/tigervnc/vncviewer/CConn.java
index b9680ef..128867b 100644
--- a/java/com/tigervnc/vncviewer/CConn.java
+++ b/java/com/tigervnc/vncviewer/CConn.java
@@ -35,7 +35,9 @@
 package com.tigervnc.vncviewer;
 
 import java.awt.*;
+import java.awt.datatransfer.StringSelection;
 import java.awt.event.*;
+import java.awt.Toolkit;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -49,6 +51,7 @@
 import java.net.InetSocketAddress;
 import java.net.SocketException;
 import java.util.*;
+import java.util.prefs.*;
 
 import com.tigervnc.rdr.*;
 import com.tigervnc.rfb.*;
@@ -57,17 +60,23 @@
 import com.tigervnc.network.Socket;
 import com.tigervnc.network.TcpSocket;
 
-public class CConn extends CConnection implements 
-  UserPasswdGetter, UserMsgBox, OptionsDialogCallback, 
-  FdInStreamBlockCallback, ActionListener {
+import static com.tigervnc.vncviewer.Parameters.*;
 
-  public final PixelFormat getPreferredPF() { return fullColourPF; }
-  static final PixelFormat verylowColourPF =
+public class CConn extends CConnection implements 
+  UserPasswdGetter, FdInStreamBlockCallback, ActionListener {
+
+  // 8 colours (1 bit per component)
+  static final PixelFormat verylowColorPF =
     new PixelFormat(8, 3, false, true, 1, 1, 1, 2, 1, 0);
-  static final PixelFormat lowColourPF =
+
+  // 64 colours (2 bits per component)
+  static final PixelFormat lowColorPF =
     new PixelFormat(8, 6, false, true, 3, 3, 3, 4, 2, 0);
-  static final PixelFormat mediumColourPF =
-    new PixelFormat(8, 8, false, false, 7, 7, 3, 0, 3, 6);
+
+  // 256 colours (2-3 bits per component)
+  static final PixelFormat mediumColorPF =
+    new PixelFormat(8, 8, false, true, 7, 7, 3, 5, 2, 0);
+
   static final int KEY_LOC_SHIFT_R = 0;
   static final int KEY_LOC_SHIFT_L = 16;
   static final int SUPER_MASK = 1<<15;
@@ -75,67 +84,49 @@
   ////////////////////////////////////////////////////////////////////
   // The following methods are all called from the RFB thread
 
-  public CConn(VncViewer viewer_, Socket sock_,
-               String vncServerName)
+  public CConn(String vncServerName, Socket socket)
   {
-    sock = sock_; viewer = viewer_;
+    serverHost = null; serverPort = 0; desktop = null;
     pendingPFChange = false;
     currentEncoding = Encodings.encodingTight; lastServerEncoding = -1;
-    fullColour = viewer.fullColour.getValue();
-    lowColourLevel = viewer.lowColourLevel.getValue();
-    autoSelect = viewer.autoSelect.getValue();
     formatChange = false; encodingChange = false;
-    fullScreen = viewer.fullScreen.getValue();
-    menuKeyCode = MenuKey.getMenuKeyCode();
-    options = new OptionsDialog(this);
-    options.initDialog();
-    clipboardDialog = new ClipboardDialog(this);
     firstUpdate = true; pendingUpdate = false; continuousUpdates = false;
     forceNonincremental = true; supportsSyncFence = false;
+
+    setShared(shared.getValue());
+    sock = socket;
     downKeySym = new HashMap<Integer, Integer>();
 
-    setShared(viewer.shared.getValue());
     upg = this;
-    msg = this;
 
-    String encStr = viewer.preferredEncoding.getValue();
-    int encNum = Encodings.encodingNum(encStr);
-    if (encNum != -1) {
+    int encNum = Encodings.encodingNum(preferredEncoding.getValue());
+    if (encNum != -1)
       currentEncoding = encNum;
-    }
+
+    cp.supportsLocalCursor = true;
+
     cp.supportsDesktopResize = true;
     cp.supportsExtendedDesktopSize = true;
+    cp.supportsDesktopRename = true;
+
     cp.supportsSetDesktopSize = false;
     cp.supportsClientRedirect = true;
-    cp.supportsDesktopRename = true;
-    cp.supportsLocalCursor = viewer.useLocalCursor.getValue();
-    cp.customCompressLevel = viewer.customCompressLevel.getValue();
-    cp.compressLevel = viewer.compressLevel.getValue();
-    cp.noJpeg = viewer.noJpeg.getValue();
-    cp.qualityLevel = viewer.qualityLevel.getValue();
-    initMenu();
 
-    if (sock != null) {
-      String name = sock.getPeerEndpoint();
-      vlog.info("Accepted connection from " + name);
-    } else {
-      if (vncServerName != null &&
-          !viewer.alwaysShowServerDialog.getValue()) {
-        setServerName(Hostname.getHost(vncServerName));
-        setServerPort(Hostname.getPort(vncServerName));
-      } else {
-        ServerDialog dlg = new ServerDialog(options, vncServerName, this);
-        boolean ret = dlg.showDialog();
-        if (!ret) {
-          close();
-          return;
-        }
-        setServerName(viewer.vncServerName.getValueStr());
-        setServerPort(viewer.vncServerPort.getValue());
-      }
+    if (customCompressLevel.getValue())
+      cp.compressLevel = compressLevel.getValue();
+    else
+      cp.compressLevel = -1;
 
+    if (!noJpeg.getValue())
+      cp.qualityLevel = qualityLevel.getValue();
+    else
+      cp.qualityLevel = -1;
+
+    if (sock == null) {
+      setServerName(Hostname.getHost(vncServerName));
+      setServerPort(Hostname.getPort(vncServerName));
       try {
-        if (viewer.tunnel.getValue() || (viewer.via.getValue() != null)) {
+        if (tunnel.getValue() || !via.getValue().isEmpty()) {
           int localPort = TcpSocket.findFreeTcpPort();
           if (localPort == 0)
             throw new Exception("Could not obtain free TCP port");
@@ -148,11 +139,22 @@
         throw new Exception(e.getMessage());
       }
       vlog.info("connected to host "+getServerName()+" port "+getServerPort());
+    } else {
+      String name = sock.getPeerEndpoint();
+      if (listenMode.getValue())
+        vlog.info("Accepted connection from " + name);
+      else
+        vlog.info("connected to host "+Hostname.getHost(name)+" port "+Hostname.getPort(name));
     }
 
+    // See callback below
     sock.inStream().setBlockCallback(this);
+
     setStreams(sock.inStream(), sock.outStream());
+
     initialiseProtocol();
+
+    OptionsDialog.addCallback("handleOptions", this);
   }
 
   public void refreshFramebuffer()
@@ -165,21 +167,37 @@
       requestNewUpdate();
   }
 
-  public boolean showMsgBox(int flags, String title, String text)
-  {
-    //StringBuffer titleText = new StringBuffer("VNC Viewer: "+title);
-    return true;
+  public String connectionInfo() {
+    String info = new String("Desktop name: %s%n"+
+                             "Host: %s:%d%n"+
+                             "Size: %dx%d%n"+
+                             "Pixel format: %s%n"+
+                             "  (server default: %s)%n"+
+                             "Requested encoding: %s%n"+
+                             "Last used encoding: %s%n"+
+                             "Line speed estimate: %d kbit/s%n"+
+                             "Protocol version: %d.%d%n"+
+                             "Security method: %s [%s]%n");
+    String infoText =
+      String.format(info, cp.name(),
+                    sock.getPeerName(), sock.getPeerPort(),
+                    cp.width, cp.height,
+                    cp.pf().print(),
+                    serverPF.print(),
+                    Encodings.encodingName(currentEncoding),
+                    Encodings.encodingName(lastServerEncoding),
+                    sock.inStream().kbitsPerSecond(),
+                    cp.majorVersion, cp.minorVersion,
+                    Security.secTypeName(csecurity.getType()),
+                    csecurity.description());
+
+    return infoText;
   }
 
-  // deleteWindow() is called when the user closes the desktop or menu windows.
+  // The RFB core is not properly asynchronous, so it calls this callback
+  // whenever it needs to block to wait for more data. Since FLTK is
+  // monitoring the socket, we just make sure FLTK gets to run.
 
-  void deleteWindow() {
-    if (viewport != null)
-      viewport.dispose();
-    viewport = null;
-  }
-
-  // blockCallback() is called when reading from the socket would block.
   public void blockCallback() {
     try {
       synchronized(this) {
@@ -196,7 +214,7 @@
   public final boolean getUserPasswd(StringBuffer user, StringBuffer passwd) {
     String title = ("VNC Authentication ["
                     +csecurity.description() + "]");
-    String passwordFileStr = viewer.passwordFile.getValue();
+    String passwordFileStr = passwordFile.getValue();
     PasswdDialog dlg;
 
     if (user == null && !passwordFileStr.equals("")) {
@@ -222,16 +240,16 @@
     if (user == null) {
       dlg = new PasswdDialog(title, (user == null), (passwd == null));
     } else {
-      if ((passwd == null) && viewer.sendLocalUsername.getValue()) {
+      if ((passwd == null) && sendLocalUsername.getValue()) {
          user.append((String)System.getProperties().get("user.name"));
          return true;
       }
-      dlg = new PasswdDialog(title, viewer.sendLocalUsername.getValue(),
+      dlg = new PasswdDialog(title, sendLocalUsername.getValue(),
          (passwd == null));
     }
-    if (!dlg.showDialog()) return false;
+    dlg.showDialog();
     if (user != null) {
-      if (viewer.sendLocalUsername.getValue()) {
+      if (sendLocalUsername.getValue()) {
          user.append((String)System.getProperties().get("user.name"));
       } else {
          user.append(dlg.userEntry.getText());
@@ -242,23 +260,24 @@
     return true;
   }
 
-  // CConnection callback methods
+  ////////////////////// CConnection callback methods //////////////////////
 
   // serverInit() is called when the serverInit message has been received.  At
   // this point we create the desktop window and display it.  We also tell the
   // server the pixel format and encodings to use and request the first update.
-  public void serverInit() {
+  public void serverInit()
+  {
     super.serverInit();
 
     // If using AutoSelect with old servers, start in FullColor
     // mode. See comment in autoSelectFormatAndEncoding.
-    if (cp.beforeVersion(3, 8) && autoSelect)
-      fullColour = true;
+    if (cp.beforeVersion(3, 8) && autoSelect.getValue())
+      fullColor.setParam(true);
 
     serverPF = cp.pf();
 
-    desktop = new DesktopWindow(cp.width, cp.height, serverPF, this);
-    fullColourPF = desktop.getPreferredPF();
+    desktop = new DesktopWindow(cp.width, cp.height, cp.name(), serverPF, this);
+    fullColorPF = desktop.getPreferredPF();
 
     // Force a switch to the format and encoding we'd like
     formatChange = true; encodingChange = true;
@@ -269,71 +288,22 @@
     // This initial update request is a bit of a corner case, so we need
     // to help out setting the correct format here.
     assert(pendingPFChange);
-    desktop.setServerPF(pendingPF);
     cp.setPF(pendingPF);
     pendingPFChange = false;
-
-    if (viewer.embed.getValue()) {
-      setupEmbeddedFrame();
-    } else {
-      recreateViewport();
-    }
-  }
-
-  void setupEmbeddedFrame() {
-    UIManager.getDefaults().put("ScrollPane.ancestorInputMap",
-      new UIDefaults.LazyInputMap(new Object[]{}));
-    JScrollPane sp = new JScrollPane();
-    sp.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
-    sp.getViewport().setBackground(Color.BLACK);
-    InputMap im = sp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
-    int ctrlAltShiftMask = Event.SHIFT_MASK | Event.CTRL_MASK | Event.ALT_MASK;
-    if (im != null) {
-      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, ctrlAltShiftMask),
-             "unitScrollUp");
-      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, ctrlAltShiftMask),
-             "unitScrollDown");
-      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, ctrlAltShiftMask),
-             "unitScrollLeft");
-      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, ctrlAltShiftMask),
-             "unitScrollRight");
-      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, ctrlAltShiftMask),
-             "scrollUp");
-      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, ctrlAltShiftMask),
-             "scrollDown");
-      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, ctrlAltShiftMask),
-             "scrollLeft");
-      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, ctrlAltShiftMask),
-             "scrollRight");
-    }
-    sp.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
-    sp.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
-    desktop.setViewport(sp.getViewport());
-    viewer.getContentPane().removeAll();
-    viewer.add(sp);
-    viewer.addFocusListener(new FocusAdapter() {
-      public void focusGained(FocusEvent e) {
-        if (desktop.isAncestorOf(viewer))
-          desktop.requestFocus();
-      }
-      public void focusLost(FocusEvent e) {
-        releaseDownKeys();
-      }
-    });
-    viewer.validate();
-    desktop.requestFocus();
   }
 
   // setDesktopSize() is called when the desktop size changes (including when
   // it is set initially).
-  public void setDesktopSize(int w, int h) {
+  public void setDesktopSize(int w, int h)
+  {
     super.setDesktopSize(w, h);
     resizeFramebuffer();
   }
 
   // setExtendedDesktopSize() is a more advanced version of setDesktopSize()
   public void setExtendedDesktopSize(int reason, int result, int w, int h,
-                                     ScreenSet layout) {
+                                     ScreenSet layout)
+  {
     super.setExtendedDesktopSize(reason, result, w, h, layout);
 
     if ((reason == screenTypes.reasonClient) &&
@@ -346,26 +316,30 @@
   }
 
   // clientRedirect() migrates the client to another host/port
-  public void clientRedirect(int port, String host,
-                             String x509subject) {
+  public void clientRedirect(int port, String host, String x509subject)
+  {
     try {
       sock.close();
-      setServerPort(port);
       sock = new TcpSocket(host, port);
       vlog.info("Redirected to "+host+":"+port);
-      VncViewer.newViewer(viewer, sock, true);
+      setServerName(host);
+      setServerPort(port);
+      sock.inStream().setBlockCallback(this);
+      setStreams(sock.inStream(), sock.outStream());
+      if (desktop != null)
+        desktop.dispose();
+      initialiseProtocol();
     } catch (java.lang.Exception e) {
       throw new Exception(e.getMessage());
     }
   }
 
   // setName() is called when the desktop name changes
-  public void setName(String name) {
+  public void setName(String name)
+  {
     super.setName(name);
-
-    if (viewport != null) {
-      viewport.setTitle(name+" - TigerVNC");
-    }
+    if (desktop != null)
+      desktop.setName(name);
   }
 
   // framebufferUpdateStart() is called at the beginning of an update.
@@ -374,10 +348,23 @@
   // one.
   public void framebufferUpdateStart()
   {
+    ModifiablePixelBuffer pb;
+    PlatformPixelBuffer ppb;
+
+    super.framebufferUpdateStart();
+
     // Note: This might not be true if sync fences are supported
     pendingUpdate = false;
 
     requestNewUpdate();
+
+    // We might still be rendering the previous update
+    pb = getFramebuffer();
+    assert(pb != null);
+    ppb = (PlatformPixelBuffer)pb;
+    assert(ppb != null);
+
+    //FIXME
   }
 
   // framebufferUpdateEnd() is called at the end of an update.
@@ -386,110 +373,69 @@
   // appropriately, and then request another incremental update.
   public void framebufferUpdateEnd()
   {
+    super.framebufferUpdateEnd();
 
     desktop.updateWindow();
 
     if (firstUpdate) {
-      int width, height;
-
       // We need fences to make extra update requests and continuous
       // updates "safe". See fence() for the next step.
       if (cp.supportsFence)
         writer().writeFence(fenceTypes.fenceFlagRequest | fenceTypes.fenceFlagSyncNext, 0, null);
 
-      if (cp.supportsSetDesktopSize &&
-          viewer.desktopSize.getValue() != null &&
-          viewer.desktopSize.getValue().split("x").length == 2) {
-        width = Integer.parseInt(viewer.desktopSize.getValue().split("x")[0]);
-        height = Integer.parseInt(viewer.desktopSize.getValue().split("x")[1]);
-        ScreenSet layout;
-
-        layout = cp.screenLayout;
-
-        if (layout.num_screens() == 0)
-          layout.add_screen(new Screen());
-        else if (layout.num_screens() != 1) {
-
-          while (true) {
-            Iterator<Screen> iter = layout.screens.iterator();
-            Screen screen = (Screen)iter.next();
-
-            if (!iter.hasNext())
-              break;
-
-            layout.remove_screen(screen.id);
-          }
-        }
-
-        Screen screen0 = (Screen)layout.screens.iterator().next();
-        screen0.dimensions.tl.x = 0;
-        screen0.dimensions.tl.y = 0;
-        screen0.dimensions.br.x = width;
-        screen0.dimensions.br.y = height;
-
-        writer().writeSetDesktopSize(width, height, layout);
-      }
-
       firstUpdate = false;
     }
 
     // A format change has been scheduled and we are now past the update
     // with the old format. Time to active the new one.
     if (pendingPFChange) {
-      desktop.setServerPF(pendingPF);
       cp.setPF(pendingPF);
       pendingPFChange = false;
     }
 
     // Compute new settings based on updated bandwidth values
-    if (autoSelect)
+    if (autoSelect.getValue())
       autoSelectFormatAndEncoding();
   }
 
   // The rest of the callbacks are fairly self-explanatory...
 
-  public void setColourMapEntries(int firstColour, int nColours, int[] rgbs) {
-    desktop.setColourMapEntries(firstColour, nColours, rgbs);
+  public void setColourMapEntries(int firstColor, int nColors, int[] rgbs)
+  {
+    vlog.error("Invalid SetColourMapEntries from server!");
   }
 
-  public void bell() {
-    if (viewer.acceptBell.getValue())
+  public void bell()
+  {
+    if (acceptBell.getValue())
       desktop.getToolkit().beep();
   }
 
-  public void serverCutText(String str, int len) {
-    if (viewer.acceptClipboard.getValue())
-      clipboardDialog.serverCutText(str, len);
+  public void serverCutText(String str, int len)
+  {
+    StringSelection buffer;
+
+    if (!acceptClipboard.getValue())
+      return;
+
+    ClipboardDialog.serverCutText(str);
   }
 
-  // We start timing on beginRect and stop timing on endRect, to
-  // avoid skewing the bandwidth estimation as a result of the server
-  // being slow or the network having high latency
-  public void beginRect(Rect r, int encoding) {
+  public void dataRect(Rect r, int encoding)
+  {
     sock.inStream().startTiming();
-    if (encoding != Encodings.encodingCopyRect) {
-      lastServerEncoding = encoding;
-    }
-  }
 
-  public void endRect(Rect r, int encoding) {
+    if (encoding != Encodings.encodingCopyRect)
+      lastServerEncoding = encoding;
+
+    super.dataRect(r, encoding);
+
     sock.inStream().stopTiming();
   }
 
-  public void fillRect(Rect r, int p) {
-    desktop.fillRect(r.tl.x, r.tl.y, r.width(), r.height(), p);
-  }
-
-  public void imageRect(Rect r, Object p) {
-    desktop.imageRect(r.tl.x, r.tl.y, r.width(), r.height(), p);
-  }
-
-  public void copyRect(Rect r, int sx, int sy) {
-    desktop.copyRect(r.tl.x, r.tl.y, r.width(), r.height(), sx, sy);
-  }
-
   public void setCursor(int width, int height, Point hotspot,
-                        int[] data, byte[] mask) {
+                        byte[] data, byte[] mask)
+  {
     desktop.setCursor(width, height, hotspot, data, mask);
   }
 
@@ -524,11 +470,11 @@
 
       pf.read(memStream);
 
-      desktop.setServerPF(pf);
       cp.setPF(pf);
     }
   }
 
+  ////////////////////// Internal methods //////////////////////
   private void resizeFramebuffer()
   {
     if (desktop == null)
@@ -537,78 +483,7 @@
     if (continuousUpdates)
       writer().writeEnableContinuousUpdates(true, 0, 0, cp.width, cp.height);
 
-    if ((cp.width == 0) && (cp.height == 0))
-      return;
-    if ((desktop.width() == cp.width) && (desktop.height() == cp.height))
-      return;
-
-    desktop.resize();
-    if (viewer.embed.getValue()) {
-      setupEmbeddedFrame();
-    } else {
-      recreateViewport();
-    }
-  }
-  
-  public void setEmbeddedFeatures(boolean s) {
-    menu.restore.setEnabled(s);
-    menu.minimize.setEnabled(s);
-    menu.maximize.setEnabled(s);
-    menu.fullScreen.setEnabled(s);
-    menu.newConn.setEnabled(s);
-    options.fullScreen.setEnabled(s);
-    options.fullScreenAllMonitors.setEnabled(s);
-    options.scalingFactor.setEnabled(s);
-  }
-
-  // recreateViewport() recreates our top-level window.  This seems to be
-  // better than attempting to resize the existing window, at least with
-  // various X window managers.
-
-  public void recreateViewport() {
-    if (viewer.embed.getValue())
-      return;
-    if (viewport != null) viewport.dispose();
-    viewport = new Viewport(cp.name(), this);
-    viewport.setUndecorated(fullScreen);
-    desktop.setViewport(viewport.getViewport());
-    reconfigureViewport();
-    if ((cp.width > 0) && (cp.height > 0))
-      viewport.setVisible(true);
-    desktop.requestFocusInWindow();
-  }
-
-  private void reconfigureViewport() {
-    Dimension dpySize = viewport.getScreenSize();
-    int w = desktop.scaledWidth;
-    int h = desktop.scaledHeight;
-    if (fullScreen) {
-      if (!viewer.fullScreenAllMonitors.getValue())
-        viewport.setExtendedState(JFrame.MAXIMIZED_BOTH);
-      viewport.setBounds(viewport.getScreenBounds());
-      if (!viewer.fullScreenAllMonitors.getValue())
-        Viewport.setFullScreenWindow(viewport);
-    } else {
-      int wmDecorationWidth = viewport.getInsets().left + viewport.getInsets().right;
-      int wmDecorationHeight = viewport.getInsets().top + viewport.getInsets().bottom;
-      if (w + wmDecorationWidth >= dpySize.width)
-        w = dpySize.width - wmDecorationWidth;
-      if (h + wmDecorationHeight >= dpySize.height)
-        h = dpySize.height - wmDecorationHeight;
-      if (viewport.getExtendedState() == JFrame.MAXIMIZED_BOTH) {
-        w = viewport.getSize().width;
-        h = viewport.getSize().height;
-        int x = viewport.getLocation().x;
-        int y = viewport.getLocation().y;
-        viewport.setGeometry(x, y, w, h);
-      } else {
-        int x = (dpySize.width - w - wmDecorationWidth) / 2;
-        int y = (dpySize.height - h - wmDecorationHeight)/2;
-        viewport.setExtendedState(JFrame.NORMAL);
-        viewport.setGeometry(x, y, w, h);
-      }
-      Viewport.setFullScreenWindow(null);
-    }
+    desktop.resizeFramebuffer(cp.width, cp.height);
   }
 
   // autoSelectFormatAndEncoding() chooses the format and encoding appropriate
@@ -626,11 +501,12 @@
   //   Note: The system here is fairly arbitrary and should be replaced
   //         with something more intelligent at the server end.
   //
-  private void autoSelectFormatAndEncoding() {
+  private void autoSelectFormatAndEncoding()
+  {
     long kbitsPerSecond = sock.inStream().kbitsPerSecond();
     long timeWaited = sock.inStream().timeWaited();
-    boolean newFullColour = fullColour;
-    int newQualityLevel = cp.qualityLevel;
+    boolean newFullColor = fullColor.getValue();
+    int newQualityLevel = qualityLevel.getValue();
 
     // Always use Tight
     if (currentEncoding != Encodings.encodingTight) {
@@ -643,17 +519,17 @@
       return;
 
     // Select appropriate quality level
-    if (!cp.noJpeg) {
+    if (!noJpeg.getValue()) {
       if (kbitsPerSecond > 16000)
         newQualityLevel = 8;
       else
         newQualityLevel = 6;
 
-      if (newQualityLevel != cp.qualityLevel) {
+      if (newQualityLevel != qualityLevel.getValue()) {
         vlog.info("Throughput "+kbitsPerSecond+
                   " kbit/s - changing to quality "+newQualityLevel);
         cp.qualityLevel = newQualityLevel;
-        viewer.qualityLevel.setParam(Integer.toString(newQualityLevel));
+        qualityLevel.setParam(newQualityLevel);
         encodingChange = true;
       }
     }
@@ -670,14 +546,24 @@
     }
 
     // Select best color level
-    newFullColour = (kbitsPerSecond > 256);
-    if (newFullColour != fullColour) {
+    newFullColor = (kbitsPerSecond > 256);
+    if (newFullColor != fullColor.getValue()) {
       vlog.info("Throughput "+kbitsPerSecond+
                 " kbit/s - full color is now "+
-  	            (newFullColour ? "enabled" : "disabled"));
-      fullColour = newFullColour;
+                (newFullColor ? "enabled" : "disabled"));
+      fullColor.setParam(newFullColor);
       formatChange = true;
-      forceNonincremental = true;
+    }
+  }
+
+  // checkEncodings() sends a setEncodings message if one is needed.
+  private void checkEncodings()
+  {
+    if (encodingChange && (writer() != null)) {
+      vlog.info("Using " + Encodings.encodingName(currentEncoding) +
+        " encoding");
+      writer().writeSetEncodings(currentEncoding, true);
+      encodingChange = false;
     }
   }
 
@@ -691,15 +577,15 @@
       /* Catch incorrect requestNewUpdate calls */
       assert(!pendingUpdate || supportsSyncFence);
 
-      if (fullColour) {
-        pf = fullColourPF;
+      if (fullColor.getValue()) {
+        pf = fullColorPF;
       } else {
-        if (lowColourLevel == 0) {
-          pf = verylowColourPF;
-        } else if (lowColourLevel == 1) {
-          pf = lowColourPF;
+        if (lowColorLevel.getValue() == 0) {
+          pf = verylowColorPF;
+        } else if (lowColorLevel.getValue() == 1) {
+          pf = lowColorPF;
         } else {
-          pf = mediumColourPF;
+          pf = mediumColorPF;
         }
       }
 
@@ -739,6 +625,60 @@
     forceNonincremental = false;
   }
 
+  public void handleOptions()
+  {
+
+    // 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.getValue()) {
+      int encNum = Encodings.encodingNum(preferredEncoding.getValue());
+
+      if (encNum != -1)
+        this.currentEncoding = encNum;
+    }
+
+    this.cp.supportsLocalCursor = true;
+
+    if (customCompressLevel.getValue())
+      this.cp.compressLevel = compressLevel.getValue();
+    else
+      this.cp.compressLevel = -1;
+
+    if (!noJpeg.getValue() && !autoSelect.getValue())
+      this.cp.qualityLevel = qualityLevel.getValue();
+    else
+      this.cp.qualityLevel = -1;
+
+    this.encodingChange = true;
+
+    // Format changes refreshes the entire screen though and are therefore
+    // very costly. It's probably worth the effort to see if it is necessary
+    // here.
+    PixelFormat pf;
+
+    if (fullColor.getValue()) {
+      pf = fullColorPF;
+    } else {
+      if (lowColorLevel.getValue() == 0)
+        pf = verylowColorPF;
+      else if (lowColorLevel.getValue() == 1)
+        pf = lowColorPF;
+      else
+        pf = mediumColorPF;
+    }
+
+    if (!pf.equal(this.cp.pf())) {
+      this.formatChange = true;
+
+      // Without fences, we cannot safely trigger an update request directly
+      // but must wait for the next update to arrive.
+      if (this.supportsSyncFence)
+        this.requestNewUpdate();
+    }
+
+  }
 
   ////////////////////////////////////////////////////////////////////
   // The following methods are all called from the GUI thread
@@ -746,14 +686,12 @@
   // close() shuts down the socket, thus waking up the RFB thread.
   public void close() {
     if (closeListener != null) {
-      viewer.embed.setParam(true);
-      if (VncViewer.nViewers == 1) {
-        JFrame f = (JFrame)JOptionPane.getFrameForComponent(viewer);
-        if (f != null)
-          f.dispatchEvent(new WindowEvent(f, WindowEvent.WINDOW_CLOSING));
-      }
+      embed.setParam(true);
+      JFrame f =
+        (JFrame)SwingUtilities.getAncestorOfClass(JFrame.class, desktop);
+      if (f != null)
+        f.dispatchEvent(new WindowEvent(f, WindowEvent.WINDOW_CLOSING));
     }
-    deleteWindow();
     shuttingDown = true;
     try {
       if (sock != null)
@@ -763,51 +701,10 @@
     }
   }
 
-  // Menu callbacks.  These are guaranteed only to be called after serverInit()
-  // has been called, since the menu is only accessible from the DesktopWindow
-
-  private void initMenu() {
-    menu = new F8Menu(this);
-  }
-
-  void showMenu(int x, int y) {
-    String os = System.getProperty("os.name");
-    if (os.startsWith("Windows"))
-      com.sun.java.swing.plaf.windows.WindowsLookAndFeel.setMnemonicHidden(false);
-    menu.show(desktop, x, y);
-  }
-
-  void showAbout() {
-    String pkgDate = "";
-    String pkgTime = "";
-    try {
-      Manifest manifest = new Manifest(VncViewer.timestamp);
-      Attributes attributes = manifest.getMainAttributes();
-      pkgDate = attributes.getValue("Package-Date");
-      pkgTime = attributes.getValue("Package-Time");
-    } catch (java.lang.Exception e) { }
-
-    Window fullScreenWindow = Viewport.getFullScreenWindow();
-    if (fullScreenWindow != null)
-      Viewport.setFullScreenWindow(null);
-    String msg =
-      String.format(VncViewer.aboutText, VncViewer.version, VncViewer.build,
-                    VncViewer.buildDate, VncViewer.buildTime);
-    JOptionPane op =
-      new JOptionPane(msg, JOptionPane.INFORMATION_MESSAGE,
-                      JOptionPane.DEFAULT_OPTION, VncViewer.logoIcon);
-    JDialog dlg = op.createDialog(desktop, "About TigerVNC Viewer for Java");
-    dlg.setIconImage(VncViewer.frameIcon);
-    dlg.setAlwaysOnTop(true);
-    dlg.setVisible(true);
-    if (fullScreenWindow != null)
-      Viewport.setFullScreenWindow(fullScreenWindow);
-  }
-
   void showInfo() {
-    Window fullScreenWindow = Viewport.getFullScreenWindow();
+    Window fullScreenWindow = DesktopWindow.getFullScreenWindow();
     if (fullScreenWindow != null)
-      Viewport.setFullScreenWindow(null);
+      DesktopWindow.setFullScreenWindow(null);
     String info = new String("Desktop name: %s%n"+
                              "Host: %s:%d%n"+
                              "Size: %dx%d%n"+
@@ -822,7 +719,7 @@
       String.format(info, cp.name(),
                     sock.getPeerName(), sock.getPeerPort(),
                     cp.width, cp.height,
-                    desktop.getPF().print(),
+                    cp.pf().print(),
                     serverPF.print(),
                     Encodings.encodingName(currentEncoding),
                     Encodings.encodingName(lastServerEncoding),
@@ -837,7 +734,7 @@
     dlg.setAlwaysOnTop(true);
     dlg.setVisible(true);
     if (fullScreenWindow != null)
-      Viewport.setFullScreenWindow(fullScreenWindow);
+      DesktopWindow.setFullScreenWindow(fullScreenWindow);
   }
 
   public void refresh() {
@@ -845,516 +742,6 @@
     pendingUpdate = true;
   }
 
-
-  // OptionsDialogCallback.  setOptions() sets the options dialog's checkboxes
-  // etc to reflect our flags.  getOptions() sets our flags according to the
-  // options dialog's checkboxes.  They are both called from the GUI thread.
-  // Some of the flags are also accessed by the RFB thread.  I believe that
-  // reading and writing boolean and int values in java is atomic, so there is
-  // no need for synchronization.
-
-  public void setOptions() {
-    int digit;
-    options.autoSelect.setSelected(autoSelect);
-    options.fullColour.setSelected(fullColour);
-    options.veryLowColour.setSelected(!fullColour && lowColourLevel == 0);
-    options.lowColour.setSelected(!fullColour && lowColourLevel == 1);
-    options.mediumColour.setSelected(!fullColour && lowColourLevel == 2);
-    options.tight.setSelected(currentEncoding == Encodings.encodingTight);
-    options.zrle.setSelected(currentEncoding == Encodings.encodingZRLE);
-    options.hextile.setSelected(currentEncoding == Encodings.encodingHextile);
-    options.raw.setSelected(currentEncoding == Encodings.encodingRaw);
-
-    options.customCompressLevel.setSelected(viewer.customCompressLevel.getValue());
-    digit = 0 + viewer.compressLevel.getValue();
-    if (digit >= 0 && digit <= 9) {
-      options.compressLevel.setSelectedItem(digit);
-    } else {
-      options.compressLevel.setSelectedItem(Integer.parseInt(viewer.compressLevel.getDefaultStr()));
-    }
-    options.noJpeg.setSelected(!viewer.noJpeg.getValue());
-    digit = 0 + viewer.qualityLevel.getValue();
-    if (digit >= 0 && digit <= 9) {
-      options.qualityLevel.setSelectedItem(digit);
-    } else {
-      options.qualityLevel.setSelectedItem(Integer.parseInt(viewer.qualityLevel.getDefaultStr()));
-    }
-
-    options.viewOnly.setSelected(viewer.viewOnly.getValue());
-    options.acceptClipboard.setSelected(viewer.acceptClipboard.getValue());
-    options.sendClipboard.setSelected(viewer.sendClipboard.getValue());
-    options.menuKey.setSelectedItem(KeyEvent.getKeyText(MenuKey.getMenuKeyCode()));
-    options.sendLocalUsername.setSelected(viewer.sendLocalUsername.getValue());
-
-    if (state() == RFBSTATE_NORMAL) {
-      options.shared.setEnabled(false);
-      options.secVeNCrypt.setEnabled(false);
-      options.encNone.setEnabled(false);
-      options.encTLS.setEnabled(false);
-      options.encX509.setEnabled(false);
-      options.x509ca.setEnabled(false);
-      options.caButton.setEnabled(false);
-      options.x509crl.setEnabled(false);
-      options.crlButton.setEnabled(false);
-      options.secIdent.setEnabled(false);
-      options.secNone.setEnabled(false);
-      options.secVnc.setEnabled(false);
-      options.secPlain.setEnabled(false);
-      options.sendLocalUsername.setEnabled(false);
-      options.cfLoadButton.setEnabled(false);
-      options.cfSaveAsButton.setEnabled(true);
-      options.sshTunnel.setEnabled(false);
-      options.sshUseGateway.setEnabled(false);
-      options.sshUser.setEnabled(false);
-      options.sshHost.setEnabled(false);
-      options.sshPort.setEnabled(false);
-      options.sshUseExt.setEnabled(false);
-      options.sshClient.setEnabled(false);
-      options.sshClientBrowser.setEnabled(false);
-      options.sshArgsDefault.setEnabled(false);
-      options.sshArgsCustom.setEnabled(false);
-      options.sshArguments.setEnabled(false);
-      options.sshConfig.setEnabled(false);
-      options.sshConfigBrowser.setEnabled(false);
-      options.sshKeyFile.setEnabled(false);
-      options.sshKeyFileBrowser.setEnabled(false);
-    } else {
-      options.shared.setSelected(viewer.shared.getValue());
-      options.sendLocalUsername.setSelected(viewer.sendLocalUsername.getValue());
-      options.cfSaveAsButton.setEnabled(false);
-      if (viewer.tunnel.getValue() || viewer.via.getValue() != null)
-        options.sshTunnel.setSelected(true);
-      if (viewer.via.getValue() != null)
-        options.sshUseGateway.setSelected(true);
-      options.sshUser.setText(Tunnel.getSshUser(this));
-      options.sshHost.setText(Tunnel.getSshHost(this));
-      options.sshPort.setText(Integer.toString(Tunnel.getSshPort(this)));
-      options.sshUseExt.setSelected(viewer.extSSH.getValue());
-      File client = new File(viewer.extSSHClient.getValue());
-      if (client.exists() && client.canRead())
-        options.sshClient.setText(client.getAbsolutePath());
-      if (viewer.extSSHArgs.getValue() == null) {
-        options.sshArgsDefault.setSelected(true);
-        options.sshArguments.setText("");
-      } else {
-        options.sshArgsCustom.setSelected(true);
-        options.sshArguments.setText(viewer.extSSHArgs.getValue());
-      }
-      File config = new File(viewer.sshConfig.getValue());
-      if (config.exists() && config.canRead())
-        options.sshConfig.setText(config.getAbsolutePath());
-      options.sshKeyFile.setText(Tunnel.getSshKeyFile(this));
-
-      /* Process non-VeNCrypt sectypes */
-      java.util.List<Integer> secTypes = new ArrayList<Integer>();
-      secTypes = Security.GetEnabledSecTypes();
-      for (Iterator<Integer> i = secTypes.iterator(); i.hasNext();) {
-        switch ((Integer)i.next()) {
-        case Security.secTypeVeNCrypt:
-          options.secVeNCrypt.setSelected(UserPreferences.getBool("viewer", "secVeNCrypt", true));
-          break;
-        case Security.secTypeNone:
-          options.encNone.setSelected(true);
-          options.secNone.setSelected(UserPreferences.getBool("viewer", "secTypeNone", true));
-          break;
-        case Security.secTypeVncAuth:
-          options.encNone.setSelected(true);
-          options.secVnc.setSelected(UserPreferences.getBool("viewer", "secTypeVncAuth", true));
-          break;
-        }
-      }
-
-      /* Process VeNCrypt subtypes */
-      if (options.secVeNCrypt.isSelected()) {
-        java.util.List<Integer> secTypesExt = new ArrayList<Integer>();
-        secTypesExt = Security.GetEnabledExtSecTypes();
-        for (Iterator<Integer> iext = secTypesExt.iterator(); iext.hasNext();) {
-          switch ((Integer)iext.next()) {
-          case Security.secTypePlain:
-            options.encNone.setSelected(UserPreferences.getBool("viewer", "encNone", true));
-            options.secPlain.setSelected(UserPreferences.getBool("viewer", "secPlain", true));
-            break;
-          case Security.secTypeIdent:
-            options.encNone.setSelected(UserPreferences.getBool("viewer", "encNone", true));
-            options.secIdent.setSelected(UserPreferences.getBool("viewer", "secIdent", true));
-            break;
-          case Security.secTypeTLSNone:
-            options.encTLS.setSelected(UserPreferences.getBool("viewer", "encTLS", true));
-            options.secNone.setSelected(UserPreferences.getBool("viewer", "secNone", true));
-            break;
-          case Security.secTypeTLSVnc:
-            options.encTLS.setSelected(UserPreferences.getBool("viewer", "encTLS", true));
-            options.secVnc.setSelected(UserPreferences.getBool("viewer", "secVnc", true));
-            break;
-          case Security.secTypeTLSPlain:
-            options.encTLS.setSelected(UserPreferences.getBool("viewer", "encTLS", true));
-            options.secPlain.setSelected(UserPreferences.getBool("viewer", "secPlain", true));
-            break;
-          case Security.secTypeTLSIdent:
-            options.encTLS.setSelected(UserPreferences.getBool("viewer", "encTLS", true));
-            options.secIdent.setSelected(UserPreferences.getBool("viewer", "secIdent", true));
-            break;
-          case Security.secTypeX509None:
-            options.encX509.setSelected(UserPreferences.getBool("viewer", "encX509", true));
-            options.secNone.setSelected(UserPreferences.getBool("viewer", "secNone", true));
-            break;
-          case Security.secTypeX509Vnc:
-            options.encX509.setSelected(UserPreferences.getBool("viewer", "encX509", true));
-            options.secVnc.setSelected(UserPreferences.getBool("viewer", "secVnc", true));
-            break;
-          case Security.secTypeX509Plain:
-            options.encX509.setSelected(UserPreferences.getBool("viewer", "encX509", true));
-            options.secPlain.setSelected(UserPreferences.getBool("viewer", "secPlain", true));
-            break;
-          case Security.secTypeX509Ident:
-            options.encX509.setSelected(UserPreferences.getBool("viewer", "encX509", true));
-            options.secIdent.setSelected(UserPreferences.getBool("viewer", "secIdent", true));
-            break;
-          }
-        }
-      }
-      File caFile = new File(viewer.x509ca.getValue());
-      if (caFile.exists() && caFile.canRead())
-        options.x509ca.setText(caFile.getAbsolutePath());
-      File crlFile = new File(viewer.x509crl.getValue());
-      if (crlFile.exists() && crlFile.canRead())
-        options.x509crl.setText(crlFile.getAbsolutePath());
-      options.encNone.setEnabled(options.secVeNCrypt.isSelected());
-      options.encTLS.setEnabled(options.secVeNCrypt.isSelected());
-      options.encX509.setEnabled(options.secVeNCrypt.isSelected());
-      options.x509ca.setEnabled(options.secVeNCrypt.isSelected() &&
-                                options.encX509.isSelected());
-      options.caButton.setEnabled(options.secVeNCrypt.isSelected() &&
-                                  options.encX509.isSelected());
-      options.x509crl.setEnabled(options.secVeNCrypt.isSelected() &&
-                                 options.encX509.isSelected());
-      options.crlButton.setEnabled(options.secVeNCrypt.isSelected() &&
-                                   options.encX509.isSelected());
-      options.secIdent.setEnabled(options.secVeNCrypt.isSelected());
-      options.secPlain.setEnabled(options.secVeNCrypt.isSelected());
-      options.sendLocalUsername.setEnabled(options.secPlain.isSelected()||
-        options.secIdent.isSelected());
-      options.sshTunnel.setEnabled(true);
-        options.sshUseGateway.setEnabled(options.sshTunnel.isSelected());
-        options.sshUser.setEnabled(options.sshTunnel.isSelected() &&
-                                   options.sshUseGateway.isEnabled() &&
-                                   options.sshUseGateway.isSelected());
-        options.sshHost.setEnabled(options.sshTunnel.isSelected() &&
-                                   options.sshUseGateway.isEnabled() &&
-                                   options.sshUseGateway.isSelected());
-        options.sshPort.setEnabled(options.sshTunnel.isSelected() &&
-                                   options.sshUseGateway.isEnabled() &&
-                                   options.sshUseGateway.isSelected());
-        options.sshUseExt.setEnabled(options.sshTunnel.isSelected());
-        options.sshClient.setEnabled(options.sshTunnel.isSelected() &&
-                                     options.sshUseExt.isEnabled() &&
-                                     options.sshUseExt.isSelected());
-        options.sshClientBrowser.setEnabled(options.sshTunnel.isSelected() &&
-                                            options.sshUseExt.isEnabled() &&
-                                            options.sshUseExt.isSelected());
-        options.sshArgsDefault.setEnabled(options.sshTunnel.isSelected() &&
-                                          options.sshUseExt.isEnabled() &&
-                                          options.sshUseExt.isSelected());
-        options.sshArgsCustom.setEnabled(options.sshTunnel.isSelected() &&
-                                         options.sshUseExt.isEnabled() &&
-                                         options.sshUseExt.isSelected());
-        options.sshArguments.setEnabled(options.sshTunnel.isSelected() &&
-                                        options.sshUseExt.isEnabled() &&
-                                        options.sshUseExt.isSelected() &&
-                                        options.sshArgsCustom.isSelected());
-        options.sshConfig.setEnabled(options.sshTunnel.isSelected() &&
-                                     options.sshUseExt.isEnabled() &&
-                                     !options.sshUseExt.isSelected());
-        options.sshConfigBrowser.setEnabled(options.sshTunnel.isSelected() &&
-                                            options.sshUseExt.isEnabled() &&
-                                            !options.sshUseExt.isSelected());
-        options.sshKeyFile.setEnabled(options.sshTunnel.isSelected() &&
-                                      options.sshUseExt.isEnabled() &&
-                                      !options.sshUseExt.isSelected());
-        options.sshKeyFileBrowser.setEnabled(options.sshTunnel.isSelected() &&
-                                             options.sshUseExt.isEnabled() &&
-                                             !options.sshUseExt.isSelected());
-    }
-
-    options.fullScreen.setSelected(fullScreen);
-    options.fullScreenAllMonitors.setSelected(viewer.fullScreenAllMonitors.getValue());
-    options.useLocalCursor.setSelected(viewer.useLocalCursor.getValue());
-    options.acceptBell.setSelected(viewer.acceptBell.getValue());
-    String scaleString = viewer.scalingFactor.getValue();
-    if (scaleString.equalsIgnoreCase("Auto")) {
-      options.scalingFactor.setSelectedItem("Auto");
-    } else if(scaleString.equalsIgnoreCase("FixedRatio")) {
-      options.scalingFactor.setSelectedItem("Fixed Aspect Ratio");
-    } else {
-      digit = Integer.parseInt(scaleString);
-      if (digit >= 1 && digit <= 1000) {
-        options.scalingFactor.setSelectedItem(digit+"%");
-      } else {
-        digit = Integer.parseInt(viewer.scalingFactor.getDefaultStr());
-        options.scalingFactor.setSelectedItem(digit+"%");
-      }
-      int scaleFactor =
-        Integer.parseInt(scaleString.substring(0, scaleString.length()));
-    }
-    if (viewer.desktopSize.getValue() != null &&
-        viewer.desktopSize.getValue().split("x").length == 2) {
-      options.desktopSize.setSelected(true);
-      String desktopWidth = viewer.desktopSize.getValue().split("x")[0];
-      options.desktopWidth.setText(desktopWidth);
-      String desktopHeight = viewer.desktopSize.getValue().split("x")[1];
-      options.desktopHeight.setText(desktopHeight);
-    }
-  }
-
-  public void getOptions() {
-    autoSelect = options.autoSelect.isSelected();
-    if (fullColour != options.fullColour.isSelected()) {
-      formatChange = true;
-      forceNonincremental = true;
-    }
-    fullColour = options.fullColour.isSelected();
-    if (!fullColour) {
-      int newLowColourLevel = (options.veryLowColour.isSelected() ? 0 :
-                               options.lowColour.isSelected() ? 1 : 2);
-      if (newLowColourLevel != lowColourLevel) {
-        lowColourLevel = newLowColourLevel;
-        formatChange = true;
-        forceNonincremental = true;
-      }
-    }
-    int newEncoding = (options.zrle.isSelected() ?  Encodings.encodingZRLE :
-                       options.hextile.isSelected() ?  Encodings.encodingHextile :
-                       options.tight.isSelected() ?  Encodings.encodingTight :
-                       Encodings.encodingRaw);
-    if (newEncoding != currentEncoding) {
-      currentEncoding = newEncoding;
-      encodingChange = true;
-    }
-
-    viewer.customCompressLevel.setParam(options.customCompressLevel.isSelected());
-    if (cp.customCompressLevel != viewer.customCompressLevel.getValue()) {
-      cp.customCompressLevel = viewer.customCompressLevel.getValue();
-      encodingChange = true;
-    }
-    if (Integer.parseInt(options.compressLevel.getSelectedItem().toString()) >= 0 &&
-        Integer.parseInt(options.compressLevel.getSelectedItem().toString()) <= 9) {
-      viewer.compressLevel.setParam(options.compressLevel.getSelectedItem().toString());
-    } else {
-      viewer.compressLevel.setParam(viewer.compressLevel.getDefaultStr());
-    }
-    if (cp.compressLevel != viewer.compressLevel.getValue()) {
-      cp.compressLevel = viewer.compressLevel.getValue();
-      encodingChange = true;
-    }
-    viewer.noJpeg.setParam(!options.noJpeg.isSelected());
-    if (cp.noJpeg != viewer.noJpeg.getValue()) {
-      cp.noJpeg = viewer.noJpeg.getValue();
-      encodingChange = true;
-    }
-    viewer.qualityLevel.setParam(options.qualityLevel.getSelectedItem().toString());
-    if (cp.qualityLevel != viewer.qualityLevel.getValue()) {
-      cp.qualityLevel = viewer.qualityLevel.getValue();
-      encodingChange = true;
-    }
-    if (!options.x509ca.getText().equals(""))
-        CSecurityTLS.x509ca.setParam(options.x509ca.getText());
-    if (!options.x509crl.getText().equals(""))
-        CSecurityTLS.x509crl.setParam(options.x509crl.getText());
-    viewer.sendLocalUsername.setParam(options.sendLocalUsername.isSelected());
-
-    viewer.viewOnly.setParam(options.viewOnly.isSelected());
-    viewer.acceptClipboard.setParam(options.acceptClipboard.isSelected());
-    viewer.sendClipboard.setParam(options.sendClipboard.isSelected());
-    viewer.acceptBell.setParam(options.acceptBell.isSelected());
-    String scaleString =
-      options.scalingFactor.getSelectedItem().toString();
-    String oldScaleFactor = viewer.scalingFactor.getValue();
-    if (scaleString.equalsIgnoreCase("Fixed Aspect Ratio")) {
-      scaleString = new String("FixedRatio");
-    } else if (scaleString.equalsIgnoreCase("Auto")) {
-      scaleString = new String("Auto");
-    } else {
-      scaleString=scaleString.substring(0, scaleString.length()-1);
-    }
-    if (!oldScaleFactor.equals(scaleString)) {
-      viewer.scalingFactor.setParam(scaleString);
-      if ((options.fullScreen.isSelected() == fullScreen) &&
-          (desktop != null))
-        recreateViewport();
-    }
-
-    clipboardDialog.setSendingEnabled(viewer.sendClipboard.getValue());
-    viewer.menuKey.setParam(MenuKey.getMenuKeySymbols()[options.menuKey.getSelectedIndex()].name);
-    F8Menu.f8.setText("Send "+KeyEvent.getKeyText(MenuKey.getMenuKeyCode()));
-
-    setShared(options.shared.isSelected());
-    viewer.useLocalCursor.setParam(options.useLocalCursor.isSelected());
-    if (cp.supportsLocalCursor != viewer.useLocalCursor.getValue()) {
-      cp.supportsLocalCursor = viewer.useLocalCursor.getValue();
-      encodingChange = true;
-      if (desktop != null)
-        desktop.resetLocalCursor();
-    }
-    viewer.extSSH.setParam(options.sshUseExt.isSelected());
-
-    checkEncodings();
-
-    if (state() != RFBSTATE_NORMAL) {
-      /* Process security types which don't use encryption */
-      if (options.encNone.isSelected()) {
-        if (options.secNone.isSelected())
-          Security.EnableSecType(Security.secTypeNone);
-        if (options.secVnc.isSelected())
-          Security.EnableSecType(Security.secTypeVncAuth);
-        if (options.secPlain.isSelected())
-          Security.EnableSecType(Security.secTypePlain);
-        if (options.secIdent.isSelected())
-          Security.EnableSecType(Security.secTypeIdent);
-      } else {
-        Security.DisableSecType(Security.secTypeNone);
-        Security.DisableSecType(Security.secTypeVncAuth);
-        Security.DisableSecType(Security.secTypePlain);
-        Security.DisableSecType(Security.secTypeIdent);
-      }
-
-      /* Process security types which use TLS encryption */
-      if (options.encTLS.isSelected()) {
-        if (options.secNone.isSelected())
-          Security.EnableSecType(Security.secTypeTLSNone);
-        if (options.secVnc.isSelected())
-          Security.EnableSecType(Security.secTypeTLSVnc);
-        if (options.secPlain.isSelected())
-          Security.EnableSecType(Security.secTypeTLSPlain);
-        if (options.secIdent.isSelected())
-          Security.EnableSecType(Security.secTypeTLSIdent);
-      } else {
-        Security.DisableSecType(Security.secTypeTLSNone);
-        Security.DisableSecType(Security.secTypeTLSVnc);
-        Security.DisableSecType(Security.secTypeTLSPlain);
-        Security.DisableSecType(Security.secTypeTLSIdent);
-      }
-
-      /* Process security types which use X509 encryption */
-      if (options.encX509.isSelected()) {
-        if (options.secNone.isSelected())
-          Security.EnableSecType(Security.secTypeX509None);
-        if (options.secVnc.isSelected())
-          Security.EnableSecType(Security.secTypeX509Vnc);
-        if (options.secPlain.isSelected())
-          Security.EnableSecType(Security.secTypeX509Plain);
-        if (options.secIdent.isSelected())
-          Security.EnableSecType(Security.secTypeX509Ident);
-      } else {
-        Security.DisableSecType(Security.secTypeX509None);
-        Security.DisableSecType(Security.secTypeX509Vnc);
-        Security.DisableSecType(Security.secTypeX509Plain);
-        Security.DisableSecType(Security.secTypeX509Ident);
-      }
-
-      /* Process *None security types */
-      if (options.secNone.isSelected()) {
-        if (options.encNone.isSelected())
-          Security.EnableSecType(Security.secTypeNone);
-        if (options.encTLS.isSelected())
-          Security.EnableSecType(Security.secTypeTLSNone);
-        if (options.encX509.isSelected())
-          Security.EnableSecType(Security.secTypeX509None);
-      } else {
-        Security.DisableSecType(Security.secTypeNone);
-        Security.DisableSecType(Security.secTypeTLSNone);
-        Security.DisableSecType(Security.secTypeX509None);
-      }
-
-      /* Process *Vnc security types */
-      if (options.secVnc.isSelected()) {
-        if (options.encNone.isSelected())
-          Security.EnableSecType(Security.secTypeVncAuth);
-        if (options.encTLS.isSelected())
-          Security.EnableSecType(Security.secTypeTLSVnc);
-        if (options.encX509.isSelected())
-          Security.EnableSecType(Security.secTypeX509Vnc);
-      } else {
-        Security.DisableSecType(Security.secTypeVncAuth);
-        Security.DisableSecType(Security.secTypeTLSVnc);
-        Security.DisableSecType(Security.secTypeX509Vnc);
-      }
-
-      /* Process *Plain security types */
-      if (options.secPlain.isSelected()) {
-        if (options.encNone.isSelected())
-          Security.EnableSecType(Security.secTypePlain);
-        if (options.encTLS.isSelected())
-          Security.EnableSecType(Security.secTypeTLSPlain);
-        if (options.encX509.isSelected())
-          Security.EnableSecType(Security.secTypeX509Plain);
-      } else {
-        Security.DisableSecType(Security.secTypePlain);
-        Security.DisableSecType(Security.secTypeTLSPlain);
-        Security.DisableSecType(Security.secTypeX509Plain);
-      }
-
-      /* Process *Ident security types */
-      if (options.secIdent.isSelected()) {
-        if (options.encNone.isSelected())
-          Security.EnableSecType(Security.secTypeIdent);
-        if (options.encTLS.isSelected())
-          Security.EnableSecType(Security.secTypeTLSIdent);
-        if (options.encX509.isSelected())
-          Security.EnableSecType(Security.secTypeX509Ident);
-      } else {
-        Security.DisableSecType(Security.secTypeIdent);
-        Security.DisableSecType(Security.secTypeTLSIdent);
-        Security.DisableSecType(Security.secTypeX509Ident);
-      }
-      if (options.sshTunnel.isSelected()) {
-        if (options.sshUseGateway.isSelected()) {
-          String user = options.sshUser.getText();
-          String host = options.sshHost.getText();
-          String port = options.sshPort.getText();
-          viewer.via.setParam(user+"@"+host+":"+port);
-        } else {
-          viewer.tunnel.setParam(true);
-        }
-      }
-      viewer.extSSH.setParam(options.sshUseExt.isSelected());
-      viewer.extSSHClient.setParam(options.sshClient.getText());
-      if (options.sshArgsCustom.isSelected())
-        viewer.extSSHArgs.setParam(options.sshArguments.getText());
-      viewer.sshConfig.setParam(options.sshConfig.getText());
-      viewer.sshKeyFile.setParam(options.sshKeyFile.getText());
-    }
-    String desktopSize = (options.desktopSize.isSelected()) ?
-        options.desktopWidth.getText() + "x" + options.desktopHeight.getText() : "";
-    viewer.desktopSize.setParam(desktopSize);
-    if (options.fullScreen.isSelected() ^ fullScreen) {
-      viewer.fullScreenAllMonitors.setParam(options.fullScreenAllMonitors.isSelected());
-      toggleFullScreen();
-    } else {
-      if (viewer.fullScreenAllMonitors.getValue() !=
-          options.fullScreenAllMonitors.isSelected()) {
-        viewer.fullScreenAllMonitors.setParam(options.fullScreenAllMonitors.isSelected());
-        if (desktop != null)
-          recreateViewport();
-      } else {
-        viewer.fullScreenAllMonitors.setParam(options.fullScreenAllMonitors.isSelected());
-      }
-    }
-  }
-
-  public void toggleFullScreen() {
-    if (viewer.embed.getValue())
-      return;
-    fullScreen = !fullScreen;
-    menu.fullScreen.setSelected(fullScreen);
-    if (viewport != null) {
-      if (!viewport.lionFSSupported()) {
-        recreateViewport();
-      } else {
-        viewport.toggleLionFS();
-      }
-    }
-  }
-
   // writeClientCutText() is called from the clipboard dialog
   public void writeClientCutText(String str, int len) {
     if (state() != RFBSTATE_NORMAL || shuttingDown)
@@ -1369,11 +756,11 @@
   }
 
   public void writeKeyEvent(KeyEvent ev) {
-    if (viewer.viewOnly.getValue() || shuttingDown)
+    if (viewOnly.getValue() || shuttingDown)
       return;
 
     boolean down = (ev.getID() == KeyEvent.KEY_PRESSED);
-   
+
     int keySym, keyCode = ev.getKeyCode();
 
     // If neither the keyCode or keyChar are defined, then there's
@@ -1388,9 +775,9 @@
       if (iter == null) {
         // Note that dead keys will raise this sort of error falsely
         // See https://bugs.openjdk.java.net/browse/JDK-6534883 
-        vlog.error("Unexpected key release of keyCode "+keyCode);
+        vlog.debug("Unexpected key release of keyCode "+keyCode);
         String fmt = ev.paramString().replaceAll("%","%%");
-        vlog.error(String.format(fmt.replaceAll(",","%n       ")));
+        vlog.debug(String.format(fmt.replaceAll(",","%n       ")));
 
         return;
       }
@@ -1474,15 +861,6 @@
       break;
     }
 
-    if (cp.width != desktop.scaledWidth ||
-        cp.height != desktop.scaledHeight) {
-      int sx = (desktop.scaleWidthRatio == 1.00) ?
-        ev.getX() : (int)Math.floor(ev.getX() / desktop.scaleWidthRatio);
-      int sy = (desktop.scaleHeightRatio == 1.00) ?
-        ev.getY() : (int)Math.floor(ev.getY() / desktop.scaleHeightRatio);
-      ev.translatePoint(sx - ev.getX(), sy - ev.getY());
-    }
-
     writer().writePointerEvent(new Point(ev.getX(), ev.getY()), buttonMask);
   }
 
@@ -1527,30 +905,13 @@
   ////////////////////////////////////////////////////////////////////
   // The following methods are called from both RFB and GUI threads
 
-  // checkEncodings() sends a setEncodings message if one is needed.
-  private void checkEncodings() {
-    if (encodingChange && (writer() != null)) {
-      vlog.info("Requesting " + Encodings.encodingName(currentEncoding) +
-        " encoding");
-      writer().writeSetEncodings(currentEncoding, true);
-      encodingChange = false;
-    }
-  }
-
   // the following never change so need no synchronization:
 
-
-  // viewer object is only ever accessed by the GUI thread so needs no
-  // synchronization (except for one test in DesktopWindow - see comment
-  // there).
-  VncViewer viewer;
-
   // access to desktop by different threads is specified in DesktopWindow
 
   // the following need no synchronization:
 
   public static UserPasswdGetter upg;
-  public UserMsgBox msg;
 
   // shuttingDown is set by the GUI thread and only ever tested by the RFB
   // thread after the window has been destroyed.
@@ -1559,27 +920,21 @@
   // reading and writing int and boolean is atomic in java, so no
   // synchronization of the following flags is needed:
 
-  int lowColourLevel;
-
 
   // All menu, options, about and info stuff is done in the GUI thread (apart
   // from when constructed).
-  F8Menu menu;
-  OptionsDialog options;
-
-  // clipboard sync issues?
-  ClipboardDialog clipboardDialog;
 
   // the following are only ever accessed by the GUI thread:
   int buttonMask;
 
+  private String serverHost;
+  private int serverPort;
   private Socket sock;
 
   protected DesktopWindow desktop;
 
-  // FIXME: should be private
-  public PixelFormat serverPF;
-  private PixelFormat fullColourPF;
+  private PixelFormat serverPF;
+  private PixelFormat fullColorPF;
 
   private boolean pendingPFChange;
   private PixelFormat pendingPF;
@@ -1597,11 +952,6 @@
 
   private boolean supportsSyncFence;
 
-  public int menuKeyCode;
-  Viewport viewport;
-  private boolean fullColour;
-  private boolean autoSelect;
-  boolean fullScreen;
   private HashMap<Integer, Integer> downKeySym;
   public ActionListener closeListener = null;
 
diff --git a/java/com/tigervnc/vncviewer/ClipboardDialog.java b/java/com/tigervnc/vncviewer/ClipboardDialog.java
index 441846c..4a5d9f2 100644
--- a/java/com/tigervnc/vncviewer/ClipboardDialog.java
+++ b/java/com/tigervnc/vncviewer/ClipboardDialog.java
@@ -26,83 +26,125 @@
 import java.nio.*;
 import javax.swing.*;
 import javax.swing.border.*;
+import javax.swing.event.*;
 import javax.swing.text.*;
 
 import com.tigervnc.rfb.LogWriter;
 
+import static com.tigervnc.vncviewer.Parameters.*;
+
 class ClipboardDialog extends Dialog {
 
-  private class VncTransferHandler extends TransferHandler {
-    // Custom TransferHandler designed to limit the size of outbound
-    // clipboard transfers to VncViewer.maxCutText.getValue() bytes.
-    private LogWriter vlog = new LogWriter("VncTransferHandler");
+  protected static class MyJTextArea extends JTextArea {
 
-    public void exportToClipboard(JComponent c, Clipboard clip, int a)
-        throws IllegalStateException {
-      if (!(c instanceof JTextComponent)) return;
-      StringSelection selection =
-        new StringSelection(((JTextComponent)c).getText());
-      clip.setContents(selection, null);
-    }
+	  private class VncTransferHandler extends TransferHandler {
+	    // Custom TransferHandler designed to limit the size of outbound
+	    // clipboard transfers to VncViewer.maxCutText.getValue() bytes.
+	    private LogWriter vlog = new LogWriter("VncTransferHandler");
 
-    public boolean importData(JComponent c, Transferable t) {
-      if (canImport(c, t.getTransferDataFlavors())) {
+	    public void exportToClipboard(JComponent c, Clipboard clip, int a)
+	        throws IllegalStateException {
+	      if (!(c instanceof JTextComponent)) return;
+        String text = ((JTextComponent)c).getText();
         try {
-          DataFlavor VncFlavor = null;
-          for (DataFlavor f : t.getTransferDataFlavors())
-            if (f.isFlavorTextType() && f.isRepresentationClassInputStream())
-              VncFlavor = f;
-          if (VncFlavor == null) return false;
-          Reader reader = (Reader)VncFlavor.getReaderForText(t);
-          CharBuffer cbuf =
-            CharBuffer.allocate(VncViewer.maxCutText.getValue());
-          cbuf.limit(reader.read(cbuf.array(), 0, cbuf.length()));
-          reader.close();
-          if (c instanceof JTextComponent)
-            ((JTextComponent)c).setText(cbuf.toString());
-          return true;
-        } catch (OutOfMemoryError oome) {
-          vlog.error("ERROR: Too much data on local clipboard!");
-        } catch (UnsupportedFlavorException ufe) {
-          // Skip import
-          vlog.info(ufe.toString());
-        } catch (IOException ioe) {
-          // Skip import
-          vlog.info(ioe.toString());
+          if (text.equals((String)clip.getData(DataFlavor.stringFlavor))) return;
+        } catch (IOException e) {
+          // worst case we set the clipboard contents unnecessarily
+	        vlog.info(e.toString());
+        } catch (UnsupportedFlavorException e) {
+          // worst case we set the clipboard contents unnecessarily
+	        vlog.info(e.toString());
         }
-      }
-      return false;
-    }
+	      StringSelection selection = new StringSelection(text);
+	      clip.setContents(selection, null);
+	    }
 
-    public boolean canImport(JComponent c, DataFlavor[] flavors) {
-      for (DataFlavor f : flavors)
-        if (f.isFlavorTextType() && f.isRepresentationClassReader())
-          return true;
-      return false;
+	    public boolean importData(JComponent c, Transferable t) {
+	      if (canImport(c, t.getTransferDataFlavors())) {
+	        try {
+	          DataFlavor VncFlavor = null;
+	          for (DataFlavor f : t.getTransferDataFlavors()) {
+	            if (f.isMimeTypeEqual("text/plain") &&
+                  f.isRepresentationClassInputStream()) {
+	              VncFlavor = f;
+                break;
+              }
+            }
+	          if (VncFlavor == null) return false;
+	          CharBuffer cbuf =
+	            CharBuffer.allocate(maxCutText.getValue());
+	          Reader reader = (Reader)VncFlavor.getReaderForText(t);
+            int n = reader.read(cbuf.array(), 0, cbuf.length());
+	          reader.close();
+            // reader returns -1 (EOF) for empty clipboard
+            cbuf.limit(n < 0 ? 0 : n);
+	          if (c instanceof JTextComponent)
+              if (!cbuf.toString().equals(((JTextComponent)c).getText()))
+	              ((JTextComponent)c).setText(cbuf.toString());
+	          return true;
+	        } catch (OutOfMemoryError e) {
+	          vlog.error("ERROR: Too much data on local clipboard!");
+	        } catch (UnsupportedFlavorException e) {
+	          // Skip import
+	          vlog.info(e.toString());
+	        } catch (IOException e) {
+	          // Skip import
+	          vlog.info(e.toString());
+	        }
+	      }
+	      return false;
+	    }
+
+	    public boolean canImport(JComponent c, DataFlavor[] flavors) {
+	      for (DataFlavor f : flavors)
+	        if (f.isMimeTypeEqual("text/plain") &&
+              f.isRepresentationClassReader())
+	          return true;
+	      return false;
+	    }
+	  }
+
+	  private class MyTextListener implements DocumentListener {
+	    public MyTextListener() { }
+
+	    public void changedUpdate(DocumentEvent e) { } 
+
+	    public void insertUpdate(DocumentEvent e) {
+        if (!listen) return;
+	      String text = textArea.getText();
+	      if (sendClipboard.getValue())
+	        VncViewer.cc.writeClientCutText(text, text.length());
+	    }
+
+	    public void removeUpdate(DocumentEvent e) { }
+	  }
+
+    public MyJTextArea() {
+      super();
+      setTransferHandler(new VncTransferHandler());
+      getDocument().addDocumentListener(new MyTextListener());
+      // If the textArea can receive the focus, then text within the textArea
+      // can be selected.  On platforms that don't support separate selection
+      // and clipboard buffers, this triggers a replacement of the textAra's
+      // contents with the selected text.
+      setFocusable(false);
+      setLineWrap(false);
+      setWrapStyleWord(true);
     }
   }
 
-  public ClipboardDialog(CConn cc_) {
+  public ClipboardDialog() {
     super(false);
     setTitle("VNC Clipboard Viewer");
     setPreferredSize(new Dimension(640, 480));
-    addWindowFocusListener(new WindowAdapter() {
+    addWindowFocusListener(new WindowFocusListener() {
       // Necessary to ensure that updates from the system clipboard
-      // still occur when the ClipboardDialog has the focus.
-      public void WindowGainedFocus(WindowEvent e) {
+      // are propagated to the textArea when the dialog is visible.
+      public void windowGainedFocus(WindowEvent e) {
         clientCutText();
       }
+      public void windowLostFocus(WindowEvent e) { }
     });
-    cc = cc_;
-    textArea = new JTextArea();
-    textArea.setTransferHandler(new VncTransferHandler());
-    // If the textArea can receive the focus, then text within the textArea
-    // can be selected.  On platforms that don't support separate selection
-    // and clipboard buffers, this triggers a replacement of the textAra's
-    // contents with the selected text.
-    textArea.setFocusable(false);
-    textArea.setLineWrap(false);
-    textArea.setWrapStyleWord(true);
     JScrollPane sp = new JScrollPane(textArea);
     getContentPane().add(sp, BorderLayout.CENTER);
     // button panel placed below the scrollpane
@@ -118,19 +160,40 @@
     pack();
   }
 
-  public void serverCutText(String str, int len) {
-    textArea.setText(str);
-    textArea.copy();
+  public static void showDialog(Container c) {
+    if (dialog == null)
+      dialog = new ClipboardDialog();
+    dialog.show(c);
   }
 
-  public void clientCutText() {
-    int hc = textArea.getText().hashCode();
+  public void show(Container c) {
+    super.showDialog(c);
+  }
+
+  public void endDialog() {
+    super.endDialog();
+    dialog.dispose();
+  }
+
+  public static void serverCutText(String str) {
+    if (textArea.getText().equals(str))
+      return;
+    // Update the text area with incoming serverCutText.  We need to diable
+    // the DocumentListener temporarily to prevent an clientCutText msg from
+    // being sent back to the server when the textArea is updated.
+    listen = false;
+    textArea.setText(str);
+    textArea.copy();
+    listen = true;
+  }
+
+  public static void clientCutText() {
+    // Update the textArea with the current contents of the system clipboard.
+    // The TransferHandler ensures that the textArea's contents are only 
+    // changed when they differ from the clipboard's.  If the textArea is
+    // updated, the DocumentListener will trigger an RFB clientCutText msg.
     textArea.paste();
     textArea.setCaretPosition(0);
-    String text = textArea.getText();
-    if (cc.viewer.sendClipboard.getValue())
-      if (hc != text.hashCode())
-        cc.writeClientCutText(text, text.length());
   }
 
   public void setSendingEnabled(boolean b) {
@@ -140,18 +203,20 @@
   public void actionPerformed(ActionEvent e) {
     Object s = e.getSource();
     if (s instanceof JButton && (JButton)s == clearButton) {
-      serverCutText(new String(""), 0);
+      serverCutText(new String(""));
     } else if (s instanceof JButton && (JButton)s == sendButton) {
       String text = textArea.getText();
-      cc.writeClientCutText(text, text.length());
+      VncViewer.cc.writeClientCutText(text, text.length());
       endDialog();
     } else if (s instanceof JButton && (JButton)s == cancelButton) {
       endDialog();
     }
   }
 
-  CConn cc;
-  JTextArea textArea;
-  JButton clearButton, sendButton, cancelButton;
+  private JButton clearButton, sendButton, cancelButton;
+  private static boolean listen = true;
+  static ClipboardDialog dialog;
+  static MyJTextArea textArea = new MyJTextArea();
+  static Toolkit tk = Toolkit.getDefaultToolkit();
   static LogWriter vlog = new LogWriter("ClipboardDialog");
 }
diff --git a/java/com/tigervnc/vncviewer/DesktopWindow.java b/java/com/tigervnc/vncviewer/DesktopWindow.java
index ff48fc1..187fbad 100644
--- a/java/com/tigervnc/vncviewer/DesktopWindow.java
+++ b/java/com/tigervnc/vncviewer/DesktopWindow.java
@@ -1,8 +1,6 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright (C) 2006 Constantin Kaplinsky.  All Rights Reserved.
- * Copyright (C) 2009 Paul Donohue.  All Rights Reserved.
- * Copyright (C) 2010, 2012-2013 D. R. Commander.  All Rights Reserved.
- * Copyright (C) 2011-2014 Brian P. Hinz
+ * Copyright (C) 2011-2016 Brian P. Hinz
+ * Copyright (C) 2012-2013 D. R. Commander.  All Rights Reserved.
  *
  * 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,572 +18,631 @@
  * USA.
  */
 
-//
-// DesktopWindow is an AWT Canvas representing a VNC desktop.
-//
-// Methods on DesktopWindow are called from both the GUI thread and the thread
-// which processes incoming RFB messages ("the RFB thread").  This means we
-// need to be careful with synchronization here.
-//
-
 package com.tigervnc.vncviewer;
+
 import java.awt.*;
 import java.awt.event.*;
-import java.awt.image.*;
-import java.awt.datatransfer.DataFlavor;
-import java.awt.datatransfer.Transferable;
-import java.awt.datatransfer.Clipboard;
-import java.io.BufferedReader;
-import java.nio.CharBuffer;
+import java.lang.reflect.*;
+import java.util.*;
 import javax.swing.*;
+import javax.swing.Timer;
+import javax.swing.border.*;
 
 import com.tigervnc.rfb.*;
-import com.tigervnc.rfb.Cursor;
 import com.tigervnc.rfb.Point;
+import java.lang.Exception;
 
-class DesktopWindow extends JPanel implements Runnable, MouseListener,
-  MouseMotionListener, MouseWheelListener, KeyListener {
+import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER;
+import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER;
+import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED;
+import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED;
+import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS;
+import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS;
 
-  ////////////////////////////////////////////////////////////////////
-  // The following methods are all called from the RFB thread
+import static com.tigervnc.vncviewer.Parameters.*;
 
-  public DesktopWindow(int width, int height, PixelFormat serverPF,
-                       CConn cc_) {
+public class DesktopWindow extends JFrame
+{
+
+  static LogWriter vlog = new LogWriter("DesktopWindow");
+
+  public DesktopWindow(int w, int h, String name,
+                       PixelFormat serverPF, CConn cc_)
+  {
     cc = cc_;
-    setSize(width, height);
-    setScaledSize();
-    setOpaque(false);
-    GraphicsEnvironment ge =
-      GraphicsEnvironment.getLocalGraphicsEnvironment();
-    GraphicsDevice gd = ge.getDefaultScreenDevice();
-    GraphicsConfiguration gc = gd.getDefaultConfiguration();
-    BufferCapabilities bufCaps = gc.getBufferCapabilities();
-    ImageCapabilities imgCaps = gc.getImageCapabilities();
-    if (bufCaps.isPageFlipping() || bufCaps.isMultiBufferAvailable() ||
-        imgCaps.isAccelerated()) {
-      vlog.debug("GraphicsDevice supports HW acceleration.");
-    } else {
-      vlog.debug("GraphicsDevice does not support HW acceleration.");
-    }
-    im = new BIPixelBuffer(width, height, cc, this);
+    firstUpdate = true;
+    delayedFullscreen = false; delayedDesktopSize = false;
 
-    cursor = new Cursor();
-    cursorBacking = new ManagedPixelBuffer();
-    Dimension bestSize = tk.getBestCursorSize(16, 16);
-    BufferedImage cursorImage;
-    cursorImage = new BufferedImage(bestSize.width, bestSize.height,
-                                    BufferedImage.TYPE_INT_ARGB);
-    java.awt.Point hotspot = new java.awt.Point(0,0);
-    nullCursor = tk.createCustomCursor(cursorImage, hotspot, "nullCursor");
-    cursorImage.flush();
-    if (!cc.cp.supportsLocalCursor && !bestSize.equals(new Dimension(0,0)))
-      setCursor(nullCursor);
-    addMouseListener(this);
-    addMouseWheelListener(this);
-    addMouseMotionListener(this);
-    addKeyListener(this);
-    addFocusListener(new FocusAdapter() {
-      public void focusGained(FocusEvent e) {
-        cc.clipboardDialog.clientCutText();
+    setFocusable(false);
+    setFocusTraversalKeysEnabled(false);
+    getToolkit().setDynamicLayout(false);
+    if (!VncViewer.os.startsWith("mac os x"))
+      setIconImage(VncViewer.frameIcon);
+    UIManager.getDefaults().put("ScrollPane.ancestorInputMap",
+      new UIDefaults.LazyInputMap(new Object[]{}));
+    scroll = new JScrollPane(new Viewport(w, h, serverPF, cc));
+    viewport = (Viewport)scroll.getViewport().getView();
+    scroll.setBorder(BorderFactory.createEmptyBorder(0,0,0,0));
+    getContentPane().add(scroll);
+
+    setName(name);
+
+    lastScaleFactor = scalingFactor.getValue();
+    if (VncViewer.os.startsWith("mac os x"))
+      if (!noLionFS.getValue())
+        enableLionFS();
+
+    OptionsDialog.addCallback("handleOptions", this);
+
+    addWindowFocusListener(new WindowAdapter() {
+      public void windowGainedFocus(WindowEvent e) {
+        if (isVisible())
+          if (scroll.getViewport() != null)
+            scroll.getViewport().getView().requestFocusInWindow();
       }
-      public void focusLost(FocusEvent e) {
+      public void windowLostFocus(WindowEvent e) {
         cc.releaseDownKeys();
       }
     });
-    setFocusTraversalKeysEnabled(false);
-    setFocusable(true);
-  }
 
-  public int width() {
-    return getWidth();
-  }
+    addWindowListener(new WindowAdapter() {
+      public void windowClosing(WindowEvent e) {
+        cc.close();
+      }
+      public void windowDeiconified(WindowEvent e) {
+        // ViewportBorder sometimes lost when window is shaded or de-iconified
+        repositionViewport();
+      }
+    });
 
-  public int height() {
-    return getHeight();
-  }
-
-  public final PixelFormat getPF() { return im.getPF(); }
-
-  public void setViewport(JViewport viewport) {
-    setScaledSize();
-    viewport.setView(this);
-    // pack() must be called on a JFrame before getInsets()
-    // will return anything other than 0.
-    if (viewport.getRootPane() != null)
-      if (getRootPane().getParent() instanceof JFrame)
-        ((JFrame)getRootPane().getParent()).pack();
-  }
-
-  // Methods called from the RFB thread - these need to be synchronized
-  // wherever they access data shared with the GUI thread.
-
-  public void setCursor(int w, int h, Point hotspot,
-                        int[] data, byte[] mask) {
-    // strictly we should use a mutex around this test since useLocalCursor
-    // might be being altered by the GUI thread.  However it's only a single
-    // boolean and it doesn't matter if we get the wrong value anyway.
-
-    synchronized(cc.viewer.useLocalCursor) {
-      if (!cc.viewer.useLocalCursor.getValue())
-        return;
-    }
-
-    hideLocalCursor();
-
-    cursor.hotspot = (hotspot != null) ? hotspot : new Point(0, 0);
-    cursor.setSize(w, h);
-    cursor.setPF(getPF());
-
-    cursorBacking.setSize(cursor.width(), cursor.height());
-    cursorBacking.setPF(getPF());
-
-    cursor.data = new int[cursor.width() * cursor.height()];
-    cursor.mask = new byte[cursor.maskLen()];
-
-    int maskBytesPerRow = (w + 7) / 8;
-    for (int y = 0; y < h; y++) {
-      for (int x = 0; x < w; x++) {
-        int byte_ = y * maskBytesPerRow + x / 8;
-        int bit = 7 - x % 8;
-        if ((mask[byte_] & (1 << bit)) > 0) {
-          cursor.data[y * cursor.width() + x] = (0xff << 24) |
-            (im.cm.getRed(data[y * w + x]) << 16) |
-            (im.cm.getGreen(data[y * w + x]) << 8) |
-            (im.cm.getBlue(data[y * w + x]));
+    addWindowStateListener(new WindowAdapter() {
+      public void windowStateChanged(WindowEvent e) {
+        int state = e.getNewState();
+        if ((state & JFrame.MAXIMIZED_BOTH) != JFrame.MAXIMIZED_BOTH) {
+          Rectangle b = getGraphicsConfiguration().getBounds();
+          if (!b.contains(getLocationOnScreen()))
+            setLocation((int)b.getX(), (int)b.getY());
         }
+        // ViewportBorder sometimes lost when restoring on Windows
+        repositionViewport();
       }
-      System.arraycopy(mask, y * maskBytesPerRow, cursor.mask,
-        y * ((cursor.width() + 7) / 8), maskBytesPerRow);
-    }
+    });
 
-    int cw = (int)Math.floor((float)cursor.width() * scaleWidthRatio);
-    int ch = (int)Math.floor((float)cursor.height() * scaleHeightRatio);
-    Dimension bestSize = tk.getBestCursorSize(cw, ch);
-    MemoryImageSource cursorSrc;
-    cursorSrc = new MemoryImageSource(cursor.width(), cursor.height(),
-                                      ColorModel.getRGBdefault(),
-                                      cursor.data, 0, cursor.width());
-    Image srcImage = tk.createImage(cursorSrc);
-    BufferedImage cursorImage;
-    cursorImage = new BufferedImage(bestSize.width, bestSize.height,
-                                    BufferedImage.TYPE_INT_ARGB);
-    Graphics2D g2 = cursorImage.createGraphics();
-    g2.setRenderingHint(RenderingHints.KEY_RENDERING,
-                        RenderingHints.VALUE_RENDER_SPEED);
-    g2.drawImage(srcImage, 0, 0, (int)Math.min(cw, bestSize.width),
-                 (int)Math.min(ch, bestSize.height), 0, 0, cursor.width(),
-                 cursor.height(), null);
-    g2.dispose();
-    srcImage.flush();
-
-    int x = (int)Math.floor((float)cursor.hotspot.x * scaleWidthRatio);
-    int y = (int)Math.floor((float)cursor.hotspot.y * scaleHeightRatio);
-    x = (int)Math.min(x, Math.max(bestSize.width - 1, 0));
-    y = (int)Math.min(y, Math.max(bestSize.height - 1, 0));
-    java.awt.Point hs = new java.awt.Point(x, y);
-    if (!bestSize.equals(new Dimension(0, 0)))
-      softCursor = tk.createCustomCursor(cursorImage, hs, "softCursor");
-    cursorImage.flush();
-
-    if (softCursor != null) {
-      setCursor(softCursor);
-      cursorAvailable = false;
-      return;
-    }
-
-    if (!cursorAvailable) {
-      cursorAvailable = true;
-    }
-
-    showLocalCursor();
-  }
-
-  public void setServerPF(PixelFormat pf) {
-    im.setPF(pf);
-  }
-
-  public PixelFormat getPreferredPF() {
-    return im.getNativePF();
-  }
-
-  // setColourMapEntries() changes some of the entries in the colourmap.
-  // Unfortunately these messages are often sent one at a time, so we delay the
-  // settings taking effect unless the whole colourmap has changed.  This is
-  // because getting java to recalculate its internal translation table and
-  // redraw the screen is expensive.
-
-  public synchronized void setColourMapEntries(int firstColour, int nColours,
-                                               int[] rgbs) {
-    im.setColourMapEntries(firstColour, nColours, rgbs);
-    if (nColours <= 256) {
-      im.updateColourMap();
-    } else {
-      if (setColourMapEntriesTimerThread == null) {
-        setColourMapEntriesTimerThread = new Thread(this);
-        setColourMapEntriesTimerThread.start();
+    // Window resize events
+    timer = new Timer(500, new AbstractAction() {
+      public void actionPerformed(ActionEvent e) {
+        handleResizeTimeout();
       }
-    }
-  }
-
-  // Update the actual window with the changed parts of the framebuffer.
-  public void updateWindow() {
-    Rect r = damage;
-    if (!r.is_empty()) {
-      if (cc.cp.width != scaledWidth || cc.cp.height != scaledHeight) {
-        int x = (int)Math.floor(r.tl.x * scaleWidthRatio);
-        int y = (int)Math.floor(r.tl.y * scaleHeightRatio);
-        // Need one extra pixel to account for rounding.
-        int width = (int)Math.ceil(r.width() * scaleWidthRatio) + 1;
-        int height = (int)Math.ceil(r.height() * scaleHeightRatio) + 1;
-        paintImmediately(x, y, width, height);
-      } else {
-        paintImmediately(r.tl.x, r.tl.y, r.width(), r.height());
-      }
-      damage.clear();
-    }
-  }
-
-  // resize() is called when the desktop has changed size
-  public void resize() {
-    int w = cc.cp.width;
-    int h = cc.cp.height;
-    hideLocalCursor();
-    setSize(w, h);
-    setScaledSize();
-    im.resize(w, h);
-  }
-
-  public final void fillRect(int x, int y, int w, int h, int pix) {
-    if (overlapsCursor(x, y, w, h)) hideLocalCursor();
-    im.fillRect(x, y, w, h, pix);
-    damageRect(new Rect(x, y, x+w, y+h));
-    showLocalCursor();
-  }
-
-  public final void imageRect(int x, int y, int w, int h,
-                              Object pix) {
-    if (overlapsCursor(x, y, w, h)) hideLocalCursor();
-    im.imageRect(x, y, w, h, pix);
-    damageRect(new Rect(x, y, x+w, y+h));
-    showLocalCursor();
-  }
-
-  public final void copyRect(int x, int y, int w, int h,
-                             int srcX, int srcY) {
-    if (overlapsCursor(x, y, w, h) || overlapsCursor(srcX, srcY, w, h))
-      hideLocalCursor();
-    im.copyRect(x, y, w, h, srcX, srcY);
-    damageRect(new Rect(x, y, x+w, y+h));
-    showLocalCursor();
-  }
-
-
-  // mutex MUST be held when overlapsCursor() is called
-  final boolean overlapsCursor(int x, int y, int w, int h) {
-    return (x < cursorBackingX + cursorBacking.width() &&
-            y < cursorBackingY + cursorBacking.height() &&
-            x + w > cursorBackingX && y + h > cursorBackingY);
-  }
-
-
-  ////////////////////////////////////////////////////////////////////
-  // The following methods are all called from the GUI thread
-
-  void resetLocalCursor() {
-    if (cc.cp.supportsLocalCursor) {
-      if (softCursor != null)
-        setCursor(softCursor);
-    } else {
-      setCursor(nullCursor);
-    }
-    hideLocalCursor();
-    cursorAvailable = false;
-  }
-
-  //
-  // Callback methods to determine geometry of our Component.
-  //
-
-  public Dimension getPreferredSize() {
-    return new Dimension(scaledWidth, scaledHeight);
-  }
-
-  public Dimension getMinimumSize() {
-    return new Dimension(scaledWidth, scaledHeight);
-  }
-
-  public Dimension getMaximumSize() {
-    return new Dimension(scaledWidth, scaledHeight);
-  }
-
-  public void setScaledSize() {
-    String scaleString = cc.viewer.scalingFactor.getValue();
-    if (!scaleString.equalsIgnoreCase("Auto") &&
-        !scaleString.equalsIgnoreCase("FixedRatio")) {
-      int scalingFactor = Integer.parseInt(scaleString);
-      scaledWidth =
-        (int)Math.floor((float)cc.cp.width * (float)scalingFactor/100.0);
-      scaledHeight =
-        (int)Math.floor((float)cc.cp.height * (float)scalingFactor/100.0);
-    } else {
-      if (cc.viewport == null) {
-        scaledWidth = cc.cp.width;
-        scaledHeight = cc.cp.height;
-      } else {
-        Dimension vpSize = cc.viewport.getSize();
-        Insets vpInsets = cc.viewport.getInsets();
-        Dimension availableSize =
-          new Dimension(vpSize.width - vpInsets.left - vpInsets.right,
-                        vpSize.height - vpInsets.top - vpInsets.bottom);
-        if (availableSize.width == 0 || availableSize.height == 0)
-          availableSize = new Dimension(cc.cp.width, cc.cp.height);
-        if (scaleString.equalsIgnoreCase("FixedRatio")) {
-          float widthRatio = (float)availableSize.width / (float)cc.cp.width;
-          float heightRatio = (float)availableSize.height / (float)cc.cp.height;
-          float ratio = Math.min(widthRatio, heightRatio);
-          scaledWidth = (int)Math.floor(cc.cp.width * ratio);
-          scaledHeight = (int)Math.floor(cc.cp.height * ratio);
+    });
+    timer.setRepeats(false);
+    addComponentListener(new ComponentAdapter() {
+      public void componentResized(ComponentEvent e) {
+        if (remoteResize.getValue()) {
+          if (timer.isRunning())
+            timer.restart();
+          else
+            // Try to get the remote size to match our window size, provided
+            // the following conditions are true:
+            //
+            // a) The user has this feature turned on
+            // b) The server supports it
+            // c) We're not still waiting for a chance to handle DesktopSize
+            // d) We're not still waiting for startup fullscreen to kick in
+            if (!firstUpdate && !delayedFullscreen &&
+                remoteResize.getValue() && cc.cp.supportsSetDesktopSize)
+              timer.start();
         } else {
-          scaledWidth = availableSize.width;
-          scaledHeight = availableSize.height;
+          String scaleString = scalingFactor.getValue();
+          if (!scaleString.matches("^[0-9]+$")) {
+            Dimension maxSize = getContentPane().getSize();
+            if ((maxSize.width != viewport.scaledWidth) ||
+                (maxSize.height != viewport.scaledHeight))
+              viewport.setScaledSize(maxSize.width, maxSize.height);
+            if (!scaleString.equals("Auto")) {
+              if (!isMaximized() && !fullscreen_active()) {
+                int dx = getInsets().left + getInsets().right;
+                int dy = getInsets().top + getInsets().bottom;
+                setSize(viewport.scaledWidth+dx, viewport.scaledHeight+dy);
+              }
+            }
+          }
+          repositionViewport();
         }
       }
-    }
-    scaleWidthRatio = (float)scaledWidth / (float)cc.cp.width;
-    scaleHeightRatio = (float)scaledHeight / (float)cc.cp.height;
+    });
+
   }
 
-  public void paintComponent(Graphics g) {
-    Graphics2D g2 = (Graphics2D) g;
-    if (cc.cp.width != scaledWidth || cc.cp.height != scaledHeight) {
-      g2.setRenderingHint(RenderingHints.KEY_RENDERING,
-                          RenderingHints.VALUE_RENDER_QUALITY);
-      g2.drawImage(im.getImage(), 0, 0, scaledWidth, scaledHeight, null);
+  // Remove resize listener in order to prevent recursion when resizing
+  @Override
+  public void setSize(Dimension d)
+  {
+    ComponentListener[] listeners = getListeners(ComponentListener.class);
+    for (ComponentListener l : listeners)
+      removeComponentListener(l);
+    super.setSize(d);
+    for (ComponentListener l : listeners)
+      addComponentListener(l);
+  }
+
+  @Override
+  public void setSize(int width, int height)
+  {
+    ComponentListener[] listeners = getListeners(ComponentListener.class);
+    for (ComponentListener l : listeners)
+      removeComponentListener(l);
+    super.setSize(width, height);
+    for (ComponentListener l : listeners)
+      addComponentListener(l);
+  }
+
+  @Override
+  public void setBounds(Rectangle r)
+  {
+    ComponentListener[] listeners = getListeners(ComponentListener.class);
+    for (ComponentListener l : listeners)
+      removeComponentListener(l);
+    super.setBounds(r);
+    for (ComponentListener l : listeners)
+      addComponentListener(l);
+  }
+
+  private void repositionViewport()
+  {
+    scroll.revalidate();
+    Rectangle r = scroll.getViewportBorderBounds();
+    int dx = r.width - viewport.scaledWidth;
+    int dy = r.height - viewport.scaledHeight;
+    int top = (int)Math.max(Math.floor(dy/2), 0);
+    int left = (int)Math.max(Math.floor(dx/2), 0);
+    int bottom = (int)Math.max(dy - top, 0);
+    int right = (int)Math.max(dx - left, 0);
+    Insets insets = new Insets(top, left, bottom, right);
+    scroll.setViewportBorder(new MatteBorder(insets, Color.BLACK));
+    scroll.revalidate();
+  }
+
+  public PixelFormat getPreferredPF()
+  {
+    return viewport.getPreferredPF();
+  }
+
+  public void setName(String name)
+  {
+    setTitle(name);
+  }
+
+  // Copy the areas of the framebuffer that have been changed (damaged)
+  // to the displayed window.
+
+  public void updateWindow()
+  {
+    if (firstUpdate) {
+      pack();
+      if (embed.getValue()) {
+        scroll.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED);
+        scroll.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED);
+        VncViewer.setupEmbeddedFrame(scroll);
+      } else {
+        if (fullScreen.getValue())
+          fullscreen_on();
+        else
+          setVisible(true);
+
+        if (maximize.getValue())
+          setExtendedState(JFrame.MAXIMIZED_BOTH);
+      }
+
+      if (cc.cp.supportsSetDesktopSize && !desktopSize.getValue().equals("")) {
+        // Hack: Wait until we're in the proper mode and position until
+        // resizing things, otherwise we might send the wrong thing.
+        if (delayedFullscreen)
+          delayedDesktopSize = true;
+        else
+          handleDesktopSize();
+      }
+      firstUpdate = false;
+    }
+
+    viewport.updateWindow();
+  }
+
+  public void resizeFramebuffer(int new_w, int new_h)
+  {
+    if ((new_w == viewport.scaledWidth) && (new_h == viewport.scaledHeight))
+      return;
+
+    // If we're letting the viewport match the window perfectly, then
+    // keep things that way for the new size, otherwise just keep things
+    // like they are.
+    int dx = getInsets().left + getInsets().right;
+    int dy = getInsets().top + getInsets().bottom;
+    if (!fullscreen_active()) {
+      if ((w() == viewport.scaledWidth) && (h() == viewport.scaledHeight))
+        setSize(new_w+dx, new_h+dy);
+      else {
+        // Make sure the window isn't too big. We do this manually because
+        // we have to disable the window size restriction (and it isn't
+        // entirely trustworthy to begin with).
+        if ((w() > new_w) || (h() > new_h))
+          setSize(Math.min(w(), new_w)+dx, Math.min(h(), new_h)+dy);
+      }
+    }
+
+    viewport.resize(0, 0, new_w, new_h);
+
+    // We might not resize the main window, so we need to manually call this
+    // to make sure the viewport is centered.
+    repositionViewport();
+
+    // repositionViewport() makes sure the scroll widget notices any changes
+    // in position, but it might be just the size that changes so we also
+    // need a poke here as well.
+    validate();
+  }
+
+  public void setCursor(int width, int height, Point hotspot,
+                        byte[] data, byte[] mask)
+  {
+    viewport.setCursor(width, height, hotspot, data, mask);
+  }
+
+  public void fullscreen_on()
+  {
+    fullScreen.setParam(true);
+    lastState = getExtendedState();
+    lastBounds = getBounds();
+    dispose();
+    // Screen bounds calculation affected by maximized window?
+    setExtendedState(JFrame.NORMAL);
+    setUndecorated(true);
+    setVisible(true);
+    setBounds(getScreenBounds());
+  }
+
+  public void fullscreen_off()
+  {
+    fullScreen.setParam(false);
+    dispose();
+    setUndecorated(false);
+    setExtendedState(lastState);
+    setBounds(lastBounds);
+    setVisible(true);
+  }
+
+  public boolean fullscreen_active()
+  {
+    return isUndecorated();
+  }
+
+  private void handleDesktopSize()
+  {
+    if (!desktopSize.getValue().equals("")) {
+      int width, height;
+
+      // An explicit size has been requested
+
+      if (desktopSize.getValue().split("x").length != 2)
+        return;
+
+      width = Integer.parseInt(desktopSize.getValue().split("x")[0]);
+      height = Integer.parseInt(desktopSize.getValue().split("x")[1]);
+      remoteResize(width, height);
+    } else if (remoteResize.getValue()) {
+      // No explicit size, but remote resizing is on so make sure it
+      // matches whatever size the window ended up being
+      remoteResize(w(), h());
+    }
+  }
+
+  public void handleResizeTimeout()
+  {
+    DesktopWindow self = (DesktopWindow)this;
+
+    assert(self != null);
+
+    self.remoteResize(self.w(), self.h());
+  }
+
+  private void remoteResize(int width, int height)
+  {
+    ScreenSet layout;
+    ListIterator<Screen> iter;
+
+    if (!fullscreen_active() || (width > w()) || (height > h())) {
+      // In windowed mode (or the framebuffer is so large that we need
+      // to scroll) we just report a single virtual screen that covers
+      // the entire framebuffer.
+
+      layout = cc.cp.screenLayout;
+
+      // Not sure why we have no screens, but adding a new one should be
+      // safe as there is nothing to conflict with...
+      if (layout.num_screens() == 0)
+        layout.add_screen(new Screen());
+      else if (layout.num_screens() != 1) {
+        // More than one screen. Remove all but the first (which we
+        // assume is the "primary").
+
+        while (true) {
+          iter = layout.begin();
+          Screen screen = iter.next();
+
+          if (iter == layout.end())
+            break;
+
+          layout.remove_screen(screen.id);
+        }
+      }
+
+      // Resize the remaining single screen to the complete framebuffer
+      ((Screen)layout.begin().next()).dimensions.tl.x = 0;
+      ((Screen)layout.begin().next()).dimensions.tl.y = 0;
+      ((Screen)layout.begin().next()).dimensions.br.x = width;
+      ((Screen)layout.begin().next()).dimensions.br.y = height;
     } else {
-      g2.drawImage(im.getImage(), 0, 0, null);
-    }
-    g2.dispose();
-  }
+      layout = new ScreenSet();
+      int id;
+      int sx, sy, sw, sh;
+      Rect viewport_rect = new Rect();
+      Rect screen_rect = new Rect();
 
-  // Mouse-Motion callback function
-  private void mouseMotionCB(MouseEvent e) {
-    if (!cc.viewer.viewOnly.getValue() &&
-        e.getX() >= 0 && e.getX() <= scaledWidth &&
-        e.getY() >= 0 && e.getY() <= scaledHeight)
-      cc.writePointerEvent(e);
-    // - If local cursor rendering is enabled then use it
-    if (cursorAvailable) {
-      // - Render the cursor!
-      if (e.getX() != cursorPosX || e.getY() != cursorPosY) {
-        hideLocalCursor();
-        if (e.getX() >= 0 && e.getX() < im.width() &&
-            e.getY() >= 0 && e.getY() < im.height()) {
-          cursorPosX = e.getX();
-          cursorPosY = e.getY();
-          showLocalCursor();
+      // In full screen we report all screens that are fully covered.
+
+      viewport_rect.setXYWH(x() + (w() - width)/2, y() + (h() - height)/2,
+                            width, height);
+
+      // If we can find a matching screen in the existing set, we use
+      // that, otherwise we create a brand new screen.
+      //
+      // FIXME: We should really track screens better so we can handle
+      //        a resized one.
+      //
+      GraphicsEnvironment ge =
+        GraphicsEnvironment.getLocalGraphicsEnvironment();
+      for (GraphicsDevice gd : ge.getScreenDevices()) {
+        for (GraphicsConfiguration gc : gd.getConfigurations()) {
+          Rectangle bounds = gc.getBounds();
+          sx = bounds.x;
+          sy = bounds.y;
+          sw = bounds.width;
+          sh = bounds.height;
+
+          // Check that the screen is fully inside the framebuffer
+          screen_rect.setXYWH(sx, sy, sw, sh);
+          if (!screen_rect.enclosed_by(viewport_rect))
+            continue;
+
+          // Adjust the coordinates so they are relative to our viewport
+          sx -= viewport_rect.tl.x;
+          sy -= viewport_rect.tl.y;
+
+          // Look for perfectly matching existing screen...
+          for (iter = cc.cp.screenLayout.begin();
+              iter != cc.cp.screenLayout.end(); iter.next()) {
+            Screen screen = iter.next(); iter.previous();
+            if ((screen.dimensions.tl.x == sx) &&
+                (screen.dimensions.tl.y == sy) &&
+                (screen.dimensions.width() == sw) &&
+                (screen.dimensions.height() == sh))
+              break;
+          }
+
+          // Found it?
+          if (iter != cc.cp.screenLayout.end()) {
+            layout.add_screen(iter.next());
+            continue;
+          }
+
+          // Need to add a new one, which means we need to find an unused id
+          Random rng = new Random();
+          while (true) {
+            id = rng.nextInt();
+            for (iter = cc.cp.screenLayout.begin();
+                iter != cc.cp.screenLayout.end(); iter.next()) {
+              Screen screen = iter.next(); iter.previous();
+              if (screen.id == id)
+                break;
+            }
+
+            if (iter == cc.cp.screenLayout.end())
+              break;
+          }
+
+          layout.add_screen(new Screen(id, sx, sy, sw, sh, 0));
         }
+
+        // If the viewport doesn't match a physical screen, then we might
+        // end up with no screens in the layout. Add a fake one...
+        if (layout.num_screens() == 0)
+          layout.add_screen(new Screen(0, 0, 0, width, height, 0));
       }
     }
-    lastX = e.getX();
-    lastY = e.getY();
-  }
-  public void mouseDragged(MouseEvent e) { mouseMotionCB(e); }
-  public void mouseMoved(MouseEvent e) { mouseMotionCB(e); }
 
-  // Mouse callback function
-  private void mouseCB(MouseEvent e) {
-    if (!cc.viewer.viewOnly.getValue()) {
-      if ((e.getID() == MouseEvent.MOUSE_RELEASED) ||
-          (e.getX() >= 0 && e.getX() <= scaledWidth &&
-           e.getY() >= 0 && e.getY() <= scaledHeight))
-        cc.writePointerEvent(e);
-    }
-    lastX = e.getX();
-    lastY = e.getY();
-  }
-  public void mouseReleased(MouseEvent e) { mouseCB(e); }
-  public void mousePressed(MouseEvent e) { mouseCB(e); }
-  public void mouseClicked(MouseEvent e) {}
-  public void mouseEntered(MouseEvent e) {
-    if (cc.viewer.embed.getValue())
-      requestFocus();
-  }
-  public void mouseExited(MouseEvent e) {}
+    // Do we actually change anything?
+    if ((width == cc.cp.width) &&
+        (height == cc.cp.height) &&
+        (layout == cc.cp.screenLayout))
+      return;
 
-  // MouseWheel callback function
-  private void mouseWheelCB(MouseWheelEvent e) {
-    if (!cc.viewer.viewOnly.getValue())
-      cc.writeWheelEvent(e);
-  }
+    String buffer;
+    vlog.debug(String.format("Requesting framebuffer resize from %dx%d to %dx%d",
+               cc.cp.width, cc.cp.height, width, height));
+    layout.debug_print();
 
-  public void mouseWheelMoved(MouseWheelEvent e) {
-    mouseWheelCB(e);
-  }
-
-  private static final Integer keyEventLock = 0; 
-
-  // Handle the key-typed event.
-  public void keyTyped(KeyEvent e) { }
-
-  // Handle the key-released event.
-  public void keyReleased(KeyEvent e) {
-    synchronized(keyEventLock) {
-      cc.writeKeyEvent(e);
-    }
-  }
-
-  // Handle the key-pressed event.
-  public void keyPressed(KeyEvent e) {
-    if (e.getKeyCode() == MenuKey.getMenuKeyCode()) {
-      int sx = (scaleWidthRatio == 1.00) ?
-        lastX : (int)Math.floor(lastX * scaleWidthRatio);
-      int sy = (scaleHeightRatio == 1.00) ?
-        lastY : (int)Math.floor(lastY * scaleHeightRatio);
-      java.awt.Point ev = new java.awt.Point(lastX, lastY);
-      ev.translate(sx - lastX, sy - lastY);
-      cc.showMenu((int)ev.getX(), (int)ev.getY());
+    if (!layout.validate(width, height)) {
+      vlog.error("Invalid screen layout computed for resize request!");
       return;
     }
-    int ctrlAltShiftMask = Event.SHIFT_MASK | Event.CTRL_MASK | Event.ALT_MASK;
-    if ((e.getModifiers() & ctrlAltShiftMask) == ctrlAltShiftMask) {
-      switch (e.getKeyCode()) {
-        case KeyEvent.VK_A:
-          cc.showAbout();
-          return;
-        case KeyEvent.VK_F:
-          cc.toggleFullScreen();
-          return;
-        case KeyEvent.VK_H:
-          cc.refresh();
-          return;
-        case KeyEvent.VK_I:
-          cc.showInfo();
-          return;
-        case KeyEvent.VK_O:
-          cc.options.showDialog(cc.viewport);
-          return;
-        case KeyEvent.VK_W:
-          VncViewer.newViewer(cc.viewer);
-          return;
-        case KeyEvent.VK_LEFT:
-        case KeyEvent.VK_RIGHT:
-        case KeyEvent.VK_UP:
-        case KeyEvent.VK_DOWN:
-          return;
-      }
-    }
-    if ((e.getModifiers() & Event.META_MASK) == Event.META_MASK) {
-      switch (e.getKeyCode()) {
-        case KeyEvent.VK_COMMA:
-        case KeyEvent.VK_N:
-        case KeyEvent.VK_W:
-        case KeyEvent.VK_I:
-        case KeyEvent.VK_R:
-        case KeyEvent.VK_L:
-        case KeyEvent.VK_F:
-        case KeyEvent.VK_Z:
-        case KeyEvent.VK_T:
-          return;
-      }
-    }
-    synchronized(keyEventLock) {
-      cc.writeKeyEvent(e);
-    }
+
+    cc.writer().writeSetDesktopSize(width, height, layout);
   }
 
-  ////////////////////////////////////////////////////////////////////
-  // The following methods are called from both RFB and GUI threads
+  boolean lionFSSupported() { return canDoLionFS; }
 
-  // Note that mutex MUST be held when hideLocalCursor() and showLocalCursor()
-  // are called.
+  private int x() { return getContentPane().getX(); }
+  private int y() { return getContentPane().getY(); }
+  private int w() { return getContentPane().getWidth(); }
+  private int h() { return getContentPane().getHeight(); }
 
-  private synchronized void hideLocalCursor() {
-    // - Blit the cursor backing store over the cursor
-    if (cursorVisible) {
-      cursorVisible = false;
-      im.imageRect(cursorBackingX, cursorBackingY, cursorBacking.width(),
-                   cursorBacking.height(), cursorBacking.data);
-      damageRect(new Rect(cursorBackingX, cursorBackingY,
-                          cursorBackingX+cursorBacking.width(),
-                          cursorBackingY+cursorBacking.height()));
-    }
-  }
-
-  private synchronized void showLocalCursor() {
-    if (cursorAvailable && !cursorVisible) {
-      if (!im.getPF().equal(cursor.getPF()) ||
-          cursor.width() == 0 || cursor.height() == 0) {
-        vlog.debug("attempting to render invalid local cursor");
-        cursorAvailable = false;
-        return;
-      }
-      cursorVisible = true;
-
-      int cursorLeft = cursor.hotspot.x;
-      int cursorTop = cursor.hotspot.y;
-      int cursorRight = cursorLeft + cursor.width();
-      int cursorBottom = cursorTop + cursor.height();
-
-      int x = (cursorLeft >= 0 ? cursorLeft : 0);
-      int y = (cursorTop >= 0 ? cursorTop : 0);
-      int w = ((cursorRight < im.width() ? cursorRight : im.width()) - x);
-      int h = ((cursorBottom < im.height() ? cursorBottom : im.height()) - y);
-
-      cursorBackingX = x;
-      cursorBackingY = y;
-      cursorBacking.setSize(w, h);
-
-      for (int j = 0; j < h; j++)
-        System.arraycopy(im.data, (y + j) * im.width() + x,
-                         cursorBacking.data, j * w, w);
-
-      im.maskRect(cursorLeft, cursorTop, cursor.width(), cursor.height(),
-                  cursor.data, cursor.mask);
-      damageRect(new Rect(x, y, x+w, y+h));
-    }
-  }
-
-  void damageRect(Rect r) {
-    if (damage.is_empty()) {
-      damage.setXYWH(r.tl.x, r.tl.y, r.width(), r.height());
-    } else {
-      r = damage.union_boundary(r);
-      damage.setXYWH(r.tl.x, r.tl.y, r.width(), r.height());
-    }
-  }
-
-  // run() is executed by the setColourMapEntriesTimerThread - it sleeps for
-  // 100ms before actually updating the colourmap.
-  public synchronized void run() {
+  void enableLionFS() {
     try {
-      Thread.sleep(100);
-    } catch(InterruptedException e) {}
-    im.updateColourMap();
-    setColourMapEntriesTimerThread = null;
+      String version = System.getProperty("os.version");
+      int firstDot = version.indexOf('.');
+      int lastDot = version.lastIndexOf('.');
+      if (lastDot > firstDot && lastDot >= 0) {
+        version = version.substring(0, version.indexOf('.', firstDot + 1));
+      }
+      double v = Double.parseDouble(version);
+      if (v < 10.7)
+        throw new Exception("Operating system version is " + v);
+
+      Class fsuClass = Class.forName("com.apple.eawt.FullScreenUtilities");
+      Class argClasses[] = new Class[]{Window.class, Boolean.TYPE};
+      Method setWindowCanFullScreen =
+        fsuClass.getMethod("setWindowCanFullScreen", argClasses);
+      setWindowCanFullScreen.invoke(fsuClass, this, true);
+
+      canDoLionFS = true;
+    } catch (Exception e) {
+      vlog.debug("Could not enable OS X 10.7+ full-screen mode: " +
+                 e.getMessage());
+    }
   }
 
-  // access to cc by different threads is specified in CConn
-  CConn cc;
+  public void toggleLionFS() {
+    try {
+      Class appClass = Class.forName("com.apple.eawt.Application");
+      Method getApplication = appClass.getMethod("getApplication",
+                                                 (Class[])null);
+      Object app = getApplication.invoke(appClass);
+      Method requestToggleFullScreen =
+        appClass.getMethod("requestToggleFullScreen", Window.class);
+      requestToggleFullScreen.invoke(app, this);
+    } catch (Exception e) {
+      vlog.debug("Could not toggle OS X 10.7+ full-screen mode: " +
+                 e.getMessage());
+    }
+  }
 
-  // access to the following must be synchronized:
-  PlatformPixelBuffer im;
-  Thread setColourMapEntriesTimerThread;
 
-  Cursor cursor;
-  boolean cursorVisible = false;     // Is cursor currently rendered?
-  boolean cursorAvailable = false;   // Is cursor available for rendering?
-  int cursorPosX, cursorPosY;
-  ManagedPixelBuffer cursorBacking;
-  int cursorBackingX, cursorBackingY;
-  java.awt.Cursor softCursor, nullCursor;
-  static Toolkit tk = Toolkit.getDefaultToolkit();
+  public boolean isMaximized()
+  {
+    int state = getExtendedState();
+    return ((state & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH);
+  }
 
-  public int scaledWidth = 0, scaledHeight = 0;
-  float scaleWidthRatio, scaleHeightRatio;
+  public Dimension getScreenSize() {
+    return getScreenBounds().getSize();
+  }
 
-  // the following are only ever accessed by the GUI thread:
-  int lastX, lastY;
-  Rect damage = new Rect();
+  public Rectangle getScreenBounds() {
+    GraphicsEnvironment ge =
+      GraphicsEnvironment.getLocalGraphicsEnvironment();
+    Rectangle r = new Rectangle();
+    if (fullScreenAllMonitors.getValue()) {
+      for (GraphicsDevice gd : ge.getScreenDevices())
+        for (GraphicsConfiguration gc : gd.getConfigurations())
+          r = r.union(gc.getBounds());
+    } else {
+      GraphicsConfiguration gc = getGraphicsConfiguration();
+      r = gc.getBounds();
+    }
+    return r;
+  }
 
-  static LogWriter vlog = new LogWriter("DesktopWindow");
+  public static Window getFullScreenWindow() {
+    GraphicsEnvironment ge =
+      GraphicsEnvironment.getLocalGraphicsEnvironment();
+    for (GraphicsDevice gd : ge.getScreenDevices()) {
+      Window fullScreenWindow = gd.getFullScreenWindow();
+      if (fullScreenWindow != null)
+        return fullScreenWindow;
+    }
+    return null;
+  }
+
+  public static void setFullScreenWindow(Window fullScreenWindow) {
+    GraphicsEnvironment ge =
+      GraphicsEnvironment.getLocalGraphicsEnvironment();
+    if (fullScreenAllMonitors.getValue()) {
+      for (GraphicsDevice gd : ge.getScreenDevices())
+        gd.setFullScreenWindow(fullScreenWindow);
+    } else {
+      GraphicsDevice gd = ge.getDefaultScreenDevice();
+      gd.setFullScreenWindow(fullScreenWindow);
+    }
+  }
+
+  public void handleOptions()
+  {
+
+    if (fullScreen.getValue() && !fullscreen_active())
+      fullscreen_on();
+    else if (!fullScreen.getValue() && fullscreen_active())
+      fullscreen_off();
+
+    if (remoteResize.getValue()) {
+      scroll.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED);
+      scroll.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED);
+      remoteResize(w(), h());
+    } else {
+      String scaleString = scalingFactor.getValue();
+      if (!scaleString.equals(lastScaleFactor)) {
+        if (scaleString.matches("^[0-9]+$")) {
+          scroll.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED);
+          scroll.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED);
+          viewport.setScaledSize(cc.cp.width, cc.cp.height);
+        } else {
+          scroll.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER);
+          scroll.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_NEVER);
+          viewport.setScaledSize(w(), h());
+        }
+
+        if (isMaximized() || fullscreen_active()) {
+          repositionViewport();
+        } else {
+          int dx = getInsets().left + getInsets().right;
+          int dy = getInsets().top + getInsets().bottom;
+          setSize(viewport.scaledWidth+dx, viewport.scaledHeight+dy);
+        }
+
+        repositionViewport();
+        lastScaleFactor = scaleString;
+      }
+    }
+
+    if (isVisible()) {
+      toFront();
+      requestFocus();
+    }
+  }
+
+  public void handleFullscreenTimeout()
+  {
+    DesktopWindow self = (DesktopWindow)this;
+
+    assert(self != null);
+
+    self.delayedFullscreen = false;
+
+    if (self.delayedDesktopSize) {
+      self.handleDesktopSize();
+      self.delayedDesktopSize = false;
+    }
+  }
+
+  private CConn cc;
+  private JScrollPane scroll;
+  public Viewport viewport;
+
+  private boolean firstUpdate;
+  private boolean delayedFullscreen;
+  private boolean delayedDesktopSize;
+  private boolean canDoLionFS;
+  private String lastScaleFactor;
+  private Rectangle lastBounds;
+  private int lastState;
+  private Timer timer;
 }
+
diff --git a/java/com/tigervnc/vncviewer/Dialog.java b/java/com/tigervnc/vncviewer/Dialog.java
index 8bf1979..a2fb04f 100644
--- a/java/com/tigervnc/vncviewer/Dialog.java
+++ b/java/com/tigervnc/vncviewer/Dialog.java
@@ -31,8 +31,10 @@
 import java.awt.*;
 import java.awt.Dialog.*;
 import java.awt.event.*;
+import java.io.File;
 import javax.swing.*;
 import javax.swing.border.*;
+import javax.swing.filechooser.*;
 import javax.swing.text.*;
 
 class Dialog extends JDialog implements ActionListener,
@@ -52,7 +54,7 @@
     }
   }
 
-  public boolean showDialog(Component c) {
+  public void showDialog(Component c) {
     initDialog();
     if (c != null) {
       setLocationRelativeTo(c);
@@ -63,26 +65,19 @@
       int y = (dpySize.height - mySize.height) / 2;
       setLocation(x, y);
     }
-    fullScreenWindow = Viewport.getFullScreenWindow();
-    if (fullScreenWindow != null)
-      Viewport.setFullScreenWindow(null);
 
     if (getModalityType() == ModalityType.APPLICATION_MODAL)
       setAlwaysOnTop(true);
     setVisible(true);
-    return ret;
   }
 
-  public boolean showDialog() {
-    return showDialog(null);
+  public void showDialog() {
+    showDialog(null);
   }
 
   public void endDialog() {
     setVisible(false);
     setAlwaysOnTop(false);
-    fullScreenWindow = Viewport.getFullScreenWindow();
-    if (fullScreenWindow != null)
-      Viewport.setFullScreenWindow(fullScreenWindow);
   }
 
   // initDialog() can be overridden in a derived class.  Typically it is used
@@ -137,6 +132,33 @@
     return width + gap;
   }
 
+  public static File showChooser(String title, File defFile,
+                                 Container c, FileNameExtensionFilter f) {
+    JFileChooser fc = new JFileChooser(defFile);
+    fc.setDialogTitle(title);
+    fc.setApproveButtonText("OK  \u21B5");
+    fc.setFileHidingEnabled(false);
+    if (f != null)
+      fc.setFileFilter(f);
+    if (fc.showOpenDialog(c) == JFileChooser.APPROVE_OPTION)
+      return fc.getSelectedFile();
+    else
+      return null;
+  }
+
+  public static File showChooser(String title, File defFile, Container c) {
+    return showChooser(title, defFile, c, null);
+  }
+
+  protected File showChooser(String title, File defFile,
+                             FileNameExtensionFilter f) {
+    return showChooser(title, defFile, this, f);
+  }
+
+  protected File showChooser(String title, File defFile) {
+    return showChooser(title, defFile, this);
+  }
+
   protected class GroupedJRadioButton extends JRadioButton {
     public GroupedJRadioButton(String l, ButtonGroup g, JComponent c) {
       super(l);
@@ -181,9 +203,9 @@
       JTextField jtf = (JTextField)editor.getEditorComponent();
       jtf.setDocument(doc);
     }
+
   }
 
   private Window fullScreenWindow;
-  protected boolean ret = true;
 
 }
diff --git a/java/com/tigervnc/vncviewer/ExtProcess.java b/java/com/tigervnc/vncviewer/ExtProcess.java
new file mode 100644
index 0000000..730a742
--- /dev/null
+++ b/java/com/tigervnc/vncviewer/ExtProcess.java
@@ -0,0 +1,125 @@
+/*
+ *  Copyright (C) 2016 Brian P. Hinz. All Rights Reserved.
+ *
+ *  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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ *  USA.
+ */
+
+package com.tigervnc.vncviewer;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.*;
+
+import com.tigervnc.rdr.*;
+import com.tigervnc.rfb.*;
+import com.tigervnc.rfb.Exception;
+import com.tigervnc.network.*;
+
+import static com.tigervnc.vncviewer.Parameters.*;
+
+public class ExtProcess implements Runnable {
+
+  private String cmd = null;
+  private LogWriter vlog = null;
+  private boolean shutdown = false;
+  private Process pid = null;
+
+  private static class MyProcessLogger extends Thread {
+    private final BufferedReader err;
+    private final LogWriter vlog;
+
+    public MyProcessLogger(Process p, LogWriter vlog) {
+      InputStreamReader reader = 
+        new InputStreamReader(p.getErrorStream());
+      err = new BufferedReader(reader);
+      this.vlog = vlog;
+    }
+
+    @Override
+    public void run() {
+      try {
+        while (true) {
+          String msg = err.readLine();
+          if (msg != null)
+            vlog.info(msg);
+        }
+      } catch(java.io.IOException e) {
+        vlog.info(e.getMessage());
+      } finally {
+        try {
+          if (err != null)
+            err.close();
+        } catch (java.io.IOException e ) { }
+      }
+    }
+  }
+
+  private static class MyShutdownHook extends Thread {
+
+    private Process proc = null;
+
+    public MyShutdownHook(Process p) {
+      proc = p;
+    }
+
+    @Override
+    public void run() {
+      try {
+        proc.exitValue();
+      } catch (IllegalThreadStateException e) {
+        try {
+          // wait for CConn to shutdown the socket
+          Thread.sleep(500);
+        } catch(InterruptedException ie) { }
+        proc.destroy();
+      }
+    }
+  }
+
+  public ExtProcess(String command, LogWriter vlog, boolean shutdown) {
+    cmd = command;  
+    this.vlog = vlog;
+    this.shutdown = shutdown;
+  }
+
+  public ExtProcess(String command, LogWriter vlog) {
+    this(command, vlog, false);
+  }
+
+  public ExtProcess(String command) {
+    this(command, null, false);
+  }
+
+  public void run() {
+    try {
+      Runtime runtime = Runtime.getRuntime();
+      pid = runtime.exec(cmd);
+      if (shutdown)
+        runtime.addShutdownHook(new MyShutdownHook(pid));
+      if (vlog != null)
+        new MyProcessLogger(pid, vlog).start();
+      pid.waitFor();
+    } catch(InterruptedException e) {
+      vlog.info(e.getMessage());
+    } catch(java.io.IOException e) {
+      vlog.info(e.getMessage());
+    }
+  }
+
+  //static LogWriter vlog = new LogWriter("ExtProcess");
+}
diff --git a/java/com/tigervnc/vncviewer/F8Menu.java b/java/com/tigervnc/vncviewer/F8Menu.java
index 472f11f..0c67305 100644
--- a/java/com/tigervnc/vncviewer/F8Menu.java
+++ b/java/com/tigervnc/vncviewer/F8Menu.java
@@ -22,48 +22,59 @@
 import java.awt.*;
 import java.awt.Cursor;
 import java.awt.event.*;
+import java.io.File;
+import javax.swing.filechooser.*;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JDialog;
 import javax.swing.JFrame;
 import javax.swing.JFileChooser;
-import javax.swing.JPopupMenu;
 import javax.swing.JMenuItem;
-import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPopupMenu;
 
 import com.tigervnc.rfb.*;
 
+import static com.tigervnc.vncviewer.Parameters.*;
+
 public class F8Menu extends JPopupMenu implements ActionListener {
-  public F8Menu(CConn cc_) {
+  public F8Menu(CConn cc) {
     super("VNC Menu");
     setLightWeightPopupEnabled(false);
-    cc = cc_;
+    String os = System.getProperty("os.name");
+    if (os.startsWith("Windows"))
+      com.sun.java.swing.plaf.windows.WindowsLookAndFeel.setMnemonicHidden(false);
+    this.cc = cc;
     restore    = addMenuItem("Restore",KeyEvent.VK_R);
-    restore.setEnabled(!cc.viewer.embed.getValue());
+    restore.setEnabled(!embed.getValue());
     move       = addMenuItem("Move");
     move.setEnabled(false);
     size       = addMenuItem("Size");
     size.setEnabled(false);
     minimize   = addMenuItem("Minimize", KeyEvent.VK_N);
-    minimize.setEnabled(!cc.viewer.embed.getValue());
+    minimize.setEnabled(!embed.getValue());
     maximize   = addMenuItem("Maximize", KeyEvent.VK_X);
-    maximize.setEnabled(!cc.viewer.embed.getValue());
+    maximize.setEnabled(!embed.getValue());
     addSeparator();
     exit       = addMenuItem("Close Viewer", KeyEvent.VK_C);
     addSeparator();
-    fullScreen = new JCheckBoxMenuItem("Full Screen");
-    fullScreen.setMnemonic(KeyEvent.VK_F);
-    fullScreen.setSelected(cc.fullScreen);
-    fullScreen.addActionListener(this);
-    fullScreen.setEnabled(!cc.viewer.embed.getValue());
-    add(fullScreen);
+    fullScreenCheckbox = new JCheckBoxMenuItem("Full Screen");
+    fullScreenCheckbox.setMnemonic(KeyEvent.VK_F);
+    fullScreenCheckbox.setSelected(fullScreen.getValue());
+    fullScreenCheckbox.addActionListener(this);
+    fullScreenCheckbox.setEnabled(!embed.getValue());
+    add(fullScreenCheckbox);
     addSeparator();
     clipboard  = addMenuItem("Clipboard...");
     addSeparator();
-    f8 = addMenuItem("Send "+KeyEvent.getKeyText(MenuKey.getMenuKeyCode()), MenuKey.getMenuKeyCode());
+    int keyCode = MenuKey.getMenuKeyCode();
+    String keyText = KeyEvent.getKeyText(keyCode);
+    f8 = addMenuItem("Send "+keyText, keyCode);
     ctrlAltDel = addMenuItem("Send Ctrl-Alt-Del");
     addSeparator();
     refresh    = addMenuItem("Refresh Screen", KeyEvent.VK_H);
     addSeparator();
     newConn    = addMenuItem("New connection...", KeyEvent.VK_W);
-    newConn.setEnabled(!cc.viewer.embed.getValue());
+    newConn.setEnabled(!embed.getValue());
     options    = addMenuItem("Options...", KeyEvent.VK_O);
     save       = addMenuItem("Save connection info as...", KeyEvent.VK_S);
     info       = addMenuItem("Connection info...", KeyEvent.VK_I);
@@ -94,19 +105,25 @@
   public void actionPerformed(ActionEvent ev) {
     if (actionMatch(ev, exit)) {
       cc.close();
-    } else if (actionMatch(ev, fullScreen)) {
-      cc.toggleFullScreen();
+    } else if (actionMatch(ev, fullScreenCheckbox)) {
+      if (fullScreenCheckbox.isSelected())
+        cc.desktop.fullscreen_on();
+      else
+        cc.desktop.fullscreen_off();
     } else if (actionMatch(ev, restore)) {
-      if (cc.fullScreen) cc.toggleFullScreen();
-      cc.viewport.setExtendedState(JFrame.NORMAL);
+      if (cc.desktop.fullscreen_active())
+        cc.desktop.fullscreen_off();
+      cc.desktop.setExtendedState(JFrame.NORMAL);
     } else if (actionMatch(ev, minimize)) {
-      if (cc.fullScreen) cc.toggleFullScreen();
-      cc.viewport.setExtendedState(JFrame.ICONIFIED);
+      if (cc.desktop.fullscreen_active())
+        cc.desktop.fullscreen_off();
+      cc.desktop.setExtendedState(JFrame.ICONIFIED);
     } else if (actionMatch(ev, maximize)) {
-      if (cc.fullScreen) cc.toggleFullScreen();
-      cc.viewport.setExtendedState(JFrame.MAXIMIZED_BOTH);
+      if (cc.desktop.fullscreen_active())
+        cc.desktop.fullscreen_off();
+      cc.desktop.setExtendedState(JFrame.MAXIMIZED_BOTH);
     } else if (actionMatch(ev, clipboard)) {
-      cc.clipboardDialog.showDialog(cc.viewport);
+      ClipboardDialog.showDialog(cc.desktop);
     } else if (actionMatch(ev, f8)) {
       cc.writeKeyEvent(MenuKey.getMenuKeySym(), true);
       cc.writeKeyEvent(MenuKey.getMenuKeySym(), false);
@@ -120,39 +137,67 @@
     } else if (actionMatch(ev, refresh)) {
       cc.refresh();
     } else if (actionMatch(ev, newConn)) {
-      VncViewer.newViewer(cc.viewer);
+      VncViewer.newViewer();
     } else if (actionMatch(ev, options)) {
-      cc.options.showDialog(cc.viewport);
+      OptionsDialog.showDialog(cc.desktop);
     } else if (actionMatch(ev, save)) {
-      JFileChooser fc = new JFileChooser();
-      fc.setDialogTitle("Save current configuration as:");
-      fc.setApproveButtonText("OK");
-      fc.setFileHidingEnabled(false);
-      Window fullScreenWindow = Viewport.getFullScreenWindow();
-      if (fullScreenWindow != null)
-        Viewport.setFullScreenWindow(null);
-      int ret = fc.showOpenDialog(cc.viewport);
-      if (fullScreenWindow != null)
-        Viewport.setFullScreenWindow(fullScreenWindow);
-      if (ret == JFileChooser.APPROVE_OPTION) {
-        String filename = fc.getSelectedFile().toString();
-        if (filename != null)
-          Configuration.save(filename);
-      }
+	    String title = "Save the TigerVNC configuration to file";
+	    File dflt = new File(FileUtils.getVncHomeDir().concat("default.tigervnc"));
+	    if (!dflt.exists() || !dflt.isFile())
+	      dflt = new File(FileUtils.getVncHomeDir());
+      FileNameExtensionFilter filter =
+        new FileNameExtensionFilter("TigerVNC configuration (*.tigervnc)", "tigervnc");
+	    File f = Dialog.showChooser(title, dflt, this, filter);
+	    while (f != null && f.exists() && f.isFile()) {
+	      String msg = f.getAbsolutePath();
+	      msg = msg.concat(" already exists. Do you want to overwrite?");
+	      Object[] options = {"Overwrite", "No  \u21B5"};
+	      JOptionPane op =
+	        new JOptionPane(msg, JOptionPane.QUESTION_MESSAGE,
+	                        JOptionPane.OK_CANCEL_OPTION, null, options, options[1]);
+	      JDialog dlg = op.createDialog(this, "TigerVNC Viewer");
+	      dlg.setIconImage(VncViewer.frameIcon);
+	      dlg.setAlwaysOnTop(true);
+	      dlg.setVisible(true);
+	      if (op.getValue() == options[0])
+	        break;
+	      else
+	        f = Dialog.showChooser(title, f, this, filter);
+	    }
+	    if (f != null && (!f.exists() || f.canWrite()))
+	      saveViewerParameters(f.getAbsolutePath(), vncServerName.getValue());
     } else if (actionMatch(ev, info)) {
       cc.showInfo();
     } else if (actionMatch(ev, about)) {
-      cc.showAbout();
+      VncViewer.showAbout(cc.desktop);
     } else if (actionMatch(ev, dismiss)) {
       firePopupMenuCanceled();
     }
   }
 
+  public void show(Component invoker, int x, int y) {
+    // lightweight components can't show in FullScreen Exclusive mode
+    /*
+    Window fsw = DesktopWindow.getFullScreenWindow();
+    GraphicsDevice gd = null;
+    if (fsw != null) {
+      gd = fsw.getGraphicsConfiguration().getDevice();
+      if (gd.isFullScreenSupported())
+        DesktopWindow.setFullScreenWindow(null);
+    }
+    */
+    super.show(invoker, x, y);
+    /*
+    if (fsw != null && gd.isFullScreenSupported())
+      DesktopWindow.setFullScreenWindow(fsw);
+      */
+  }
+
   CConn cc;
   JMenuItem restore, move, size, minimize, maximize;
   JMenuItem exit, clipboard, ctrlAltDel, refresh;
   JMenuItem newConn, options, save, info, about, dismiss;
   static JMenuItem f8;
-  JCheckBoxMenuItem fullScreen;
+  JCheckBoxMenuItem fullScreenCheckbox;
   static LogWriter vlog = new LogWriter("F8Menu");
 }
diff --git a/java/com/tigervnc/vncviewer/JavaPixelBuffer.java b/java/com/tigervnc/vncviewer/JavaPixelBuffer.java
new file mode 100644
index 0000000..b639673
--- /dev/null
+++ b/java/com/tigervnc/vncviewer/JavaPixelBuffer.java
@@ -0,0 +1,59 @@
+/* Copyright (C) 2012-2016 Brian P. Hinz
+ * Copyright (C) 2012 D. R. Commander.  All Rights Reserved.
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ * USA.
+ */
+
+package com.tigervnc.vncviewer;
+
+import java.awt.*;
+import java.awt.image.*;
+import java.nio.ByteOrder;
+
+import com.tigervnc.rfb.*;
+
+public class JavaPixelBuffer extends PlatformPixelBuffer 
+{
+
+  public JavaPixelBuffer(int w, int h) {
+    super(getPreferredPF(), w, h,
+          getPreferredPF().getColorModel().createCompatibleWritableRaster(w,h));
+  }
+
+  private static PixelFormat getPreferredPF()
+  {
+    GraphicsEnvironment ge =
+      GraphicsEnvironment.getLocalGraphicsEnvironment();
+    GraphicsDevice gd = ge.getDefaultScreenDevice();
+    GraphicsConfiguration gc = gd.getDefaultConfiguration();
+    ColorModel cm = gc.getColorModel();
+    int depth = ((cm.getPixelSize() > 24) ? 24 : cm.getPixelSize());
+    int bpp = (depth > 16 ? 32 : (depth > 8 ? 16 : 8));
+    ByteOrder byteOrder = ByteOrder.nativeOrder();
+    boolean bigEndian = (byteOrder == ByteOrder.BIG_ENDIAN ? true : false);
+    boolean trueColour = true;
+    int redShift    = cm.getComponentSize()[0] + cm.getComponentSize()[1];
+    int greenShift  = cm.getComponentSize()[0];
+    int blueShift   = 0;
+    int redMask   = ((int)Math.pow(2, cm.getComponentSize()[2]) - 1);
+    int greenMask = ((int)Math.pow(2, cm.getComponentSize()[1]) - 1);
+    int blueMmask = ((int)Math.pow(2, cm.getComponentSize()[0]) - 1);
+    return new PixelFormat(bpp, depth, bigEndian, trueColour,
+                           redMask, greenMask, blueMmask,
+                           redShift, greenShift, blueShift);
+  }
+
+}
diff --git a/java/com/tigervnc/vncviewer/OptionsDialog.java b/java/com/tigervnc/vncviewer/OptionsDialog.java
index 369b965..a7c8778 100644
--- a/java/com/tigervnc/vncviewer/OptionsDialog.java
+++ b/java/com/tigervnc/vncviewer/OptionsDialog.java
@@ -22,14 +22,18 @@
 import java.awt.*;
 import java.awt.event.*;
 import java.io.File;
+import java.lang.reflect.*;
 import java.text.Format;
 import java.text.NumberFormat;
 import javax.swing.*;
 import javax.swing.border.*;
+import javax.swing.filechooser.*;
 import javax.swing.UIManager.*;
 import javax.swing.text.*;
 import java.util.*;
+import java.util.List;
 import java.util.Map.Entry;
+import java.util.prefs.*;
 
 import com.tigervnc.rfb.*;
 
@@ -44,6 +48,8 @@
 import static java.awt.GridBagConstraints.REMAINDER;
 import static java.awt.GridBagConstraints.VERTICAL;
 
+import static com.tigervnc.vncviewer.Parameters.*;
+
 class OptionsDialog extends Dialog {
 
   private class IntegerDocument extends PlainDocument {
@@ -88,47 +94,558 @@
     }
   }
 
-  // Constants
-  static LogWriter vlog = new LogWriter("OptionsDialog");
+  private static Map<Object, String> callbacks = new HashMap<Object, String>();
+  /* Compression */
+  JCheckBox autoselectCheckbox;
 
-  CConn cc;
-  @SuppressWarnings({"rawtypes"})
-  JComboBox menuKey, compressLevel, qualityLevel, scalingFactor;
-  ButtonGroup encodingGroup, colourGroup, sshArgsGroup;
-  JRadioButton zrle, hextile, tight, raw, fullColour, mediumColour,
-               lowColour, veryLowColour, sshArgsDefault, sshArgsCustom;
-  JCheckBox autoSelect, customCompressLevel, noJpeg, viewOnly,
-            acceptClipboard, sendClipboard, acceptBell, desktopSize,
-            fullScreen, fullScreenAllMonitors, shared, useLocalCursor,
-            secVeNCrypt, encNone, encTLS, encX509, secNone, secVnc,
-            secPlain, secIdent, sendLocalUsername, sshTunnel, sshUseExt,
-            sshUseGateway;
-  JButton okButton, cancelButton, caButton, crlButton, cfLoadButton,
-          cfSaveAsButton, defSaveButton, defReloadButton, defClearButton,
-          sshConfigBrowser, sshKeyFileBrowser, sshClientBrowser;
-  JTextField desktopWidth, desktopHeight, x509ca, x509crl, sshUser, sshHost,
-             sshPort, sshClient, sshArguments, sshConfig, sshKeyFile;
-  JTabbedPane tabPane;
+  ButtonGroup encodingGroup;
+  JRadioButton tightButton;
+  JRadioButton zrleButton;
+  JRadioButton hextileButton;
+  JRadioButton rawButton;
+
+  ButtonGroup colorlevelGroup;
+  JRadioButton fullcolorButton;
+  JRadioButton mediumcolorButton;
+  JRadioButton lowcolorButton;
+  JRadioButton verylowcolorButton;
+
+  JCheckBox compressionCheckbox;
+  JCheckBox jpegCheckbox;
+  JComboBox compressionInput;
+  JComboBox jpegInput;
+
+  /* Security */
+  JCheckBox encNoneCheckbox;
+  JCheckBox encTLSCheckbox;
+  JCheckBox encX509Checkbox;
+  JTextField caInput;
+  JTextField crlInput;
+  JButton caChooser;
+  JButton crlChooser;
+
+  JCheckBox authNoneCheckbox;
+  JCheckBox authVncCheckbox;
+  JCheckBox authPlainCheckbox;
+  JCheckBox authIdentCheckbox;
+  JCheckBox sendLocalUsernameCheckbox;
+
+  /* Input */
+  JCheckBox viewOnlyCheckbox;
+  JCheckBox acceptClipboardCheckbox;
+  JCheckBox sendClipboardCheckbox;
+  JComboBox menuKeyChoice;
+
+  /* Screen */
+  JCheckBox desktopSizeCheckbox;
+  JTextField desktopWidthInput;
+  JTextField desktopHeightInput;
+
+  ButtonGroup sizingGroup;
+  JRadioButton remoteResizeButton;
+  JRadioButton remoteScaleButton;
+  JComboBox scalingFactorInput;
+
+  JCheckBox fullScreenCheckbox;
+  JCheckBox fullScreenAllMonitorsCheckbox;
+
+  /* Misc. */
+  JCheckBox sharedCheckbox;
+  JCheckBox dotWhenNoCursorCheckbox;
+  JCheckBox acceptBellCheckbox;
+
+  /* SSH */
+  JCheckBox tunnelCheckbox;
+  JCheckBox viaCheckbox;
+  JTextField viaUserInput;
+  JTextField viaHostInput;
+  JTextField viaPortInput;
+  JCheckBox extSSHCheckbox;
+  JTextField sshClientInput;
+  JButton sshClientChooser;
+  JRadioButton sshArgsDefaultButton;
+  JRadioButton sshArgsCustomButton;
+  JTextField sshArgsInput;
+  JTextField sshConfigInput;
+  JTextField sshKeyFileInput;
+  JButton sshConfigChooser;
+  JButton sshKeyFileChooser;
 
   @SuppressWarnings({"rawtypes","unchecked"})
-  public OptionsDialog(CConn cc_) {
+  public OptionsDialog() {
     super(true);
-    cc = cc_;
     setTitle("VNC Viewer Options");
     setResizable(false);
 
     getContentPane().setLayout(
       new BoxLayout(getContentPane(), BoxLayout.PAGE_AXIS));
 
-    tabPane = new JTabbedPane();
+    JTabbedPane tabPane = new JTabbedPane();
     tabPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
 
     encodingGroup = new ButtonGroup();
-    colourGroup = new ButtonGroup();
-    sshArgsGroup = new ButtonGroup();
-    int indent = 0;
+    colorlevelGroup = new ButtonGroup();
 
-    // Compression tab
+    // tabPane
+    tabPane.addTab("Compression", createCompressionPanel());
+    tabPane.addTab("Security", createSecurityPanel());
+    tabPane.addTab("Input", createInputPanel());
+    tabPane.addTab("Screen", createScreenPanel());
+    tabPane.addTab("Misc", createMiscPanel());
+    tabPane.addTab("SSH", createSshPanel());
+    tabPane.setBorder(BorderFactory.createEmptyBorder());
+    // Resize the tabPane if necessary to prevent scrolling
+    int minWidth = 0;
+    Object tpi = UIManager.get("TabbedPane:TabbedPaneTabArea.contentMargins");
+    if (tpi != null)
+      minWidth += ((Insets)tpi).left + ((Insets)tpi).right;
+    for (int i = 0; i < tabPane.getTabCount(); i++)
+      minWidth += tabPane.getBoundsAt(i).width;
+    int minHeight = tabPane.getPreferredSize().height;
+    if (tabPane.getPreferredSize().width < minWidth)
+      tabPane.setPreferredSize(new Dimension(minWidth, minHeight));
+
+    // button pane
+    JButton okButton = new JButton("OK  \u21B5");
+    okButton.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        storeOptions();
+        endDialog();
+      }
+    });
+    JButton cancelButton = new JButton("Cancel");
+    cancelButton.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        endDialog();
+      }
+    });
+
+    JPanel buttonPane = new JPanel(new GridLayout(1, 5, 10, 10));
+    buttonPane.setBorder(BorderFactory.createEmptyBorder(10, 5, 5, 5));
+    buttonPane.add(Box.createRigidArea(new Dimension()));
+    buttonPane.add(Box.createRigidArea(new Dimension()));
+    buttonPane.add(Box.createRigidArea(new Dimension()));
+    buttonPane.add(cancelButton);
+    buttonPane.add(okButton);
+
+    this.add(tabPane);
+    this.add(buttonPane);
+    addListeners(this);
+    pack();
+  }
+
+  public static void showDialog(Container c) {
+    OptionsDialog dialog = new OptionsDialog();
+    dialog.show(c);
+  }
+
+  public void show(Container c) {
+    loadOptions();
+    super.showDialog(c);
+  }
+
+  public static void addCallback(String cb, Object obj)
+  {
+    callbacks.put(obj, cb);
+  }
+
+  public static void removeCallback(Object obj)
+  {
+    callbacks.remove(obj);
+  }
+
+  public void endDialog() {
+    super.endDialog();
+    // Making a new dialog is so cheap that it's not worth keeping
+    this.dispose();
+  }
+
+  public void setEmbeddedFeatures(boolean s) {
+    fullScreenCheckbox.setEnabled(s);
+    fullScreenAllMonitorsCheckbox.setEnabled(s);
+    scalingFactorInput.setEnabled(s);
+    Enumeration<AbstractButton> e = sizingGroup.getElements();
+    while (e.hasMoreElements())
+      e.nextElement().setEnabled(s);
+  }
+
+  private void loadOptions()
+  {
+    /* Compression */
+    autoselectCheckbox.setSelected(autoSelect.getValue());
+
+    int encNum = Encodings.encodingNum(preferredEncoding.getValueStr());
+
+    switch (encNum) {
+    case Encodings.encodingTight:
+      tightButton.setSelected(true);
+      break;
+    case Encodings.encodingZRLE:
+      zrleButton.setSelected(true);
+      break;
+    case Encodings.encodingHextile:
+      hextileButton.setSelected(true);
+      break;
+    case Encodings.encodingRaw:
+      rawButton.setSelected(true);
+      break;
+    }
+
+    if (fullColor.getValue())
+      fullcolorButton.setSelected(true);
+    else {
+      switch (lowColorLevel.getValue()) {
+      case 0:
+        verylowcolorButton.setSelected(true);
+        break;
+      case 1:
+        lowcolorButton.setSelected(true);
+        break;
+      case 2:
+        mediumcolorButton.setSelected(true);
+        break;
+      }
+    }
+
+    int digit = 0;
+
+    compressionCheckbox.setSelected(customCompressLevel.getValue());
+    jpegCheckbox.setSelected(!noJpeg.getValue());
+    digit = 0 + compressLevel.getValue();
+    compressionInput.setSelectedItem(digit);
+    digit = 0 + qualityLevel.getValue();
+    jpegInput.setSelectedItem(digit);
+
+    handleAutoselect();
+    handleCompression();
+    handleJpeg();
+
+    /* Security */
+    Security security = new Security(SecurityClient.secTypes);
+
+    List<Integer> secTypes;
+    Iterator<Integer> iter;
+
+    List<Integer> secTypesExt;
+    Iterator<Integer> iterExt;
+
+    encNoneCheckbox.setSelected(false);
+    encTLSCheckbox.setSelected(false);
+    encX509Checkbox.setSelected(false);
+
+    authNoneCheckbox.setSelected(false);
+    authVncCheckbox.setSelected(false);
+    authPlainCheckbox.setSelected(false);
+    authIdentCheckbox.setSelected(false);
+    sendLocalUsernameCheckbox.setSelected(sendLocalUsername.getValue());
+
+    secTypes = security.GetEnabledSecTypes();
+    for (iter = secTypes.iterator(); iter.hasNext(); ) {
+      switch ((Integer)iter.next()) {
+      case Security.secTypeNone:
+        encNoneCheckbox.setSelected(true);
+        authNoneCheckbox.setSelected(true);
+        break;
+      case Security.secTypeVncAuth:
+        encNoneCheckbox.setSelected(true);
+        authVncCheckbox.setSelected(true);
+        break;
+      }
+    }
+
+    secTypesExt = security.GetEnabledExtSecTypes();
+    for (iterExt = secTypesExt.iterator(); iterExt.hasNext(); ) {
+      switch ((Integer)iterExt.next()) {
+      case Security.secTypePlain:
+        encNoneCheckbox.setSelected(true);
+        authPlainCheckbox.setSelected(true);
+        break;
+      case Security.secTypeIdent:
+        encNoneCheckbox.setSelected(true);
+        authIdentCheckbox.setSelected(true);
+        break;
+      case Security.secTypeTLSNone:
+        encTLSCheckbox.setSelected(true);
+        authNoneCheckbox.setSelected(true);
+        break;
+      case Security.secTypeTLSVnc:
+        encTLSCheckbox.setSelected(true);
+        authVncCheckbox.setSelected(true);
+        break;
+      case Security.secTypeTLSPlain:
+        encTLSCheckbox.setSelected(true);
+        authPlainCheckbox.setSelected(true);
+        break;
+      case Security.secTypeTLSIdent:
+        encTLSCheckbox.setSelected(true);
+        authIdentCheckbox.setSelected(true);
+        break;
+      case Security.secTypeX509None:
+        encX509Checkbox.setSelected(true);
+        authNoneCheckbox.setSelected(true);
+        break;
+      case Security.secTypeX509Vnc:
+        encX509Checkbox.setSelected(true);
+        authVncCheckbox.setSelected(true);
+        break;
+      case Security.secTypeX509Plain:
+        encX509Checkbox.setSelected(true);
+        authPlainCheckbox.setSelected(true);
+        break;
+      case Security.secTypeX509Ident:
+        encX509Checkbox.setSelected(true);
+        authIdentCheckbox.setSelected(true);
+        break;
+      }
+    }
+
+    File caFile = new File(CSecurityTLS.X509CA.getValueStr());
+    if (caFile.exists() && caFile.canRead())
+      caInput.setText(caFile.getAbsolutePath());
+    File crlFile = new File(CSecurityTLS.X509CRL.getValueStr());
+    if (crlFile.exists() && crlFile.canRead())
+      crlInput.setText(crlFile.getAbsolutePath());
+
+    handleX509();
+    handleSendLocalUsername();
+
+    /* Input */
+    viewOnlyCheckbox.setSelected(viewOnly.getValue());
+    acceptClipboardCheckbox.setSelected(acceptClipboard.getValue());
+    sendClipboardCheckbox.setSelected(sendClipboard.getValue());
+
+    menuKeyChoice.setSelectedIndex(0);
+
+    String menuKeyStr = menuKey.getValueStr();
+    for (int i = 0; i < menuKeyChoice.getItemCount(); i++)
+      if (menuKeyStr.equals(menuKeyChoice.getItemAt(i)))
+        menuKeyChoice.setSelectedIndex(i);
+
+    /* Screen */
+    String width, height;
+
+    if (desktopSize.getValueStr().isEmpty() ||
+        desktopSize.getValueStr().split("x").length != 2) {
+      desktopSizeCheckbox.setSelected(false);
+      desktopWidthInput.setText("1024");
+      desktopHeightInput.setText("768");
+    } else {
+      desktopSizeCheckbox.setSelected(true);
+      width = desktopSize.getValueStr().split("x")[0];
+      desktopWidthInput.setText(width);
+      height = desktopSize.getValueStr().split("x")[1];
+      desktopHeightInput.setText(height);
+    }
+    if (remoteResize.getValue())
+      remoteResizeButton.setSelected(true);
+    else
+      remoteScaleButton.setSelected(true);
+    fullScreenCheckbox.setSelected(fullScreen.getValue());
+    fullScreenAllMonitorsCheckbox.setSelected(fullScreenAllMonitors.getValue());
+
+    scalingFactorInput.setSelectedItem("100%");
+    String scaleStr = scalingFactor.getValueStr();
+    if (scaleStr.matches("^[0-9]+$"))
+      scaleStr = scaleStr.concat("%");
+    if (scaleStr.matches("^FixedRatio$"))
+      scaleStr = new String("Fixed Aspect Ratio");
+    for (int i = 0; i < scalingFactorInput.getItemCount(); i++)
+      if (scaleStr.equals(scalingFactorInput.getItemAt(i)))
+        scalingFactorInput.setSelectedIndex(i);
+
+    handleDesktopSize();
+
+    /* Misc. */
+    sharedCheckbox.setSelected(shared.getValue());
+    dotWhenNoCursorCheckbox.setSelected(dotWhenNoCursor.getValue());
+    acceptBellCheckbox.setSelected(acceptBell.getValue());
+
+    /* SSH */
+    File f;
+    tunnelCheckbox.setSelected(tunnel.getValue() || !via.getValueStr().isEmpty());
+    viaCheckbox.setSelected(!via.getValueStr().isEmpty());
+    if (viaCheckbox.isSelected()) {
+      viaUserInput.setText(Tunnel.getSshUser());
+      viaHostInput.setText(Tunnel.getSshHost());
+      viaPortInput.setText(Integer.toString(Tunnel.getSshPort()));
+    }
+    extSSHCheckbox.setSelected(extSSH.getValue());
+    f = new File(extSSHClient.getValueStr());
+    if (f.exists() && f.isFile() && f.canExecute())
+      sshClientInput.setText(f.getAbsolutePath());
+    if (extSSHArgs.getValueStr().isEmpty()) {
+      sshArgsDefaultButton.setSelected(true);
+    } else {
+      sshArgsCustomButton.setSelected(true);
+      sshArgsInput.setText(extSSHArgs.getValueStr());
+    }
+    f = new File(sshKeyFile.getValueStr());
+    if (f.exists() && f.isFile() && f.canRead())
+      sshKeyFileInput.setText(f.getAbsolutePath());
+    f = new File(sshConfig.getValueStr());
+    if (f.exists() && f.isFile() && f.canRead())
+      sshConfigInput.setText(f.getAbsolutePath());
+
+    handleTunnel();
+    handleVia();
+    handleExtSSH();
+    handleEmbed();
+    handleRfbState();
+  }
+
+  private void storeOptions() {
+    /* Compression */
+    autoSelect.setParam(autoselectCheckbox.isSelected());
+
+    if (tightButton.isSelected())
+      preferredEncoding.setParam(Encodings.encodingName(Encodings.encodingTight));
+    else if (zrleButton.isSelected())
+      preferredEncoding.setParam(Encodings.encodingName(Encodings.encodingZRLE));
+    else if (hextileButton.isSelected())
+      preferredEncoding.setParam(Encodings.encodingName(Encodings.encodingHextile));
+    else if (rawButton.isSelected())
+      preferredEncoding.setParam(Encodings.encodingName(Encodings.encodingRaw));
+
+    fullColor.setParam(fullcolorButton.isSelected());
+    if (verylowcolorButton.isSelected())
+      lowColorLevel.setParam(0);
+    else if (lowcolorButton.isSelected())
+      lowColorLevel.setParam(1);
+    else if (mediumcolorButton.isSelected())
+      lowColorLevel.setParam(2);
+
+    customCompressLevel.setParam(compressionCheckbox.isSelected());
+    noJpeg.setParam(!jpegCheckbox.isSelected());
+    compressLevel.setParam((Integer)compressionInput.getSelectedItem());
+    qualityLevel.setParam((Integer)jpegInput.getSelectedItem());
+
+    /* Security */
+    Security security = new Security();
+
+    /* Process security types which don't use encryption */
+    if (encNoneCheckbox.isSelected()) {
+      if (authNoneCheckbox.isSelected())
+        security.EnableSecType(Security.secTypeNone);
+      if (authVncCheckbox.isSelected())
+        security.EnableSecType(Security.secTypeVncAuth);
+      if (authPlainCheckbox.isSelected())
+        security.EnableSecType(Security.secTypePlain);
+      if (authIdentCheckbox.isSelected())
+        security.EnableSecType(Security.secTypeIdent);
+    }
+
+    /* Process security types which use TLS encryption */
+    if (encTLSCheckbox.isSelected()) {
+      if (authNoneCheckbox.isSelected())
+        security.EnableSecType(Security.secTypeTLSNone);
+      if (authVncCheckbox.isSelected())
+        security.EnableSecType(Security.secTypeTLSVnc);
+      if (authPlainCheckbox.isSelected())
+        security.EnableSecType(Security.secTypeTLSPlain);
+      if (authIdentCheckbox.isSelected())
+        security.EnableSecType(Security.secTypeTLSIdent);
+    }
+
+    /* Process security types which use X509 encryption */
+    if (encX509Checkbox.isSelected()) {
+      if (authNoneCheckbox.isSelected())
+        security.EnableSecType(Security.secTypeX509None);
+      if (authVncCheckbox.isSelected())
+        security.EnableSecType(Security.secTypeX509Vnc);
+      if (authPlainCheckbox.isSelected())
+        security.EnableSecType(Security.secTypeX509Plain);
+      if (authIdentCheckbox.isSelected())
+        security.EnableSecType(Security.secTypeX509Ident);
+    }
+
+    if (authIdentCheckbox.isSelected() ||
+        authPlainCheckbox.isSelected()) {
+      sendLocalUsername.setParam(sendLocalUsernameCheckbox.isSelected());
+    }
+
+    SecurityClient.secTypes.setParam(security.ToString());
+
+    File caFile = new File(caInput.getText());
+    if (caFile.exists() && caFile.canRead())
+      CSecurityTLS.X509CA.setParam(caFile.getAbsolutePath());
+    File crlFile = new File(crlInput.getText());
+    if (crlFile.exists() && crlFile.canRead())
+      CSecurityTLS.X509CRL.setParam(crlFile.getAbsolutePath());
+
+    /* Input */
+    viewOnly.setParam(viewOnlyCheckbox.isSelected());
+    acceptClipboard.setParam(acceptClipboardCheckbox.isSelected());
+    sendClipboard.setParam(sendClipboardCheckbox.isSelected());
+
+    String menuKeyStr =
+      MenuKey.getMenuKeySymbols()[menuKeyChoice.getSelectedIndex()].name;
+    menuKey.setParam(menuKeyStr);
+
+    /* Screen */
+    if (desktopSizeCheckbox.isSelected() &&
+        !desktopWidthInput.getText().isEmpty() &&
+        !desktopHeightInput.getText().isEmpty()) {
+      String width = desktopWidthInput.getText();
+      String height = desktopHeightInput.getText();
+      desktopSize.setParam(width.concat("x").concat(height));
+    } else {
+      desktopSize.setParam("");
+    }
+    remoteResize.setParam(remoteResizeButton.isSelected());
+    fullScreen.setParam(fullScreenCheckbox.isSelected());
+    fullScreenAllMonitors.setParam(fullScreenAllMonitorsCheckbox.isSelected());
+
+    String scaleStr =
+      ((String)scalingFactorInput.getSelectedItem()).replace("%", "");
+    scaleStr.replace("Fixed Aspect Ratio", "FixedRatio");
+    scalingFactor.setParam(scaleStr);
+
+    /* Misc. */
+    shared.setParam(sharedCheckbox.isSelected());
+    dotWhenNoCursor.setParam(dotWhenNoCursorCheckbox.isSelected());
+    acceptBell.setParam(acceptBellCheckbox.isSelected());
+
+    /* SSH */
+    tunnel.setParam(tunnelCheckbox.isSelected());
+    if (viaCheckbox.isSelected() &&
+        !viaUserInput.getText().isEmpty() &&
+        !viaHostInput.getText().isEmpty() &&
+        !viaPortInput.getText().isEmpty()) {
+      String sshUser = viaUserInput.getText();
+      String sshHost = viaHostInput.getText();
+      String sshPort = viaPortInput.getText();
+      String viaStr = sshUser.concat("@").concat(sshHost).concat(":").concat(sshPort);
+      via.setParam(viaStr);
+    }
+    extSSH.setParam(extSSHCheckbox.isSelected());
+    if (!sshClientInput.getText().isEmpty())
+      extSSHClient.setParam(sshClientInput.getText());
+    if (sshArgsDefaultButton.isSelected())
+      if (!sshArgsInput.getText().isEmpty())
+        extSSHArgs.setParam(sshArgsInput.getText());
+    if (!sshConfigInput.getText().isEmpty())
+      sshConfig.setParam(sshConfigInput.getText());
+    if (!sshKeyFileInput.getText().isEmpty())
+      sshKeyFile.setParam(sshKeyFileInput.getText());
+
+    try {
+      for (Map.Entry<Object, String> iter : callbacks.entrySet()) {
+        Object obj = iter.getKey();
+        Method cb = obj.getClass().getMethod(iter.getValue(), new Class[]{});
+        if (cb == null)
+          vlog.info(obj.getClass().getName());
+        cb.invoke(obj);
+      }
+    } catch (NoSuchMethodException e) {
+      vlog.error("NoSuchMethodException: "+e.getMessage());
+    } catch (IllegalAccessException e) {
+      vlog.error("IllegalAccessException: "+e.getMessage());
+    } catch (InvocationTargetException e) {
+      vlog.error("InvocationTargetException: "+e.getMessage());
+    }
+  }
+
+  private JPanel createCompressionPanel() {
     JPanel FormatPanel = new JPanel();
     FormatPanel.setLayout(new BoxLayout(FormatPanel,
                                         BoxLayout.PAGE_AXIS));
@@ -138,61 +655,72 @@
     autoSelectPane.setLayout(new BoxLayout(autoSelectPane,
                                            BoxLayout.LINE_AXIS));
     autoSelectPane.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
-    autoSelect = new JCheckBox("Auto Select");
-    autoSelectPane.add(autoSelect);
+    autoselectCheckbox = new JCheckBox("Auto Select");
+    autoselectCheckbox.addItemListener(new ItemListener() {
+      public void itemStateChanged(ItemEvent e) {
+        handleAutoselect();
+      }
+    });
+    autoSelectPane.add(autoselectCheckbox);
     autoSelectPane.add(Box.createHorizontalGlue());
 
     JPanel encodingPanel = new JPanel(new GridLayout(4, 1));
-    encodingPanel.
-      setBorder(BorderFactory.createTitledBorder("Preferred encoding"));
-    tight = new GroupedJRadioButton("Tight",
-                                    encodingGroup, encodingPanel);
-    zrle = new GroupedJRadioButton("ZRLE",
-                                    encodingGroup, encodingPanel);
-    hextile = new GroupedJRadioButton("Hextile",
-                                      encodingGroup, encodingPanel);
-    raw = new GroupedJRadioButton("Raw", encodingGroup, encodingPanel);
+    encodingPanel.setBorder(BorderFactory.createTitledBorder("Preferred encoding"));
+    tightButton = new GroupedJRadioButton("Tight", encodingGroup, encodingPanel);
+    zrleButton = new GroupedJRadioButton("ZRLE", encodingGroup, encodingPanel);
+    hextileButton = new GroupedJRadioButton("Hextile", encodingGroup, encodingPanel);
+    rawButton = new GroupedJRadioButton("Raw", encodingGroup, encodingPanel);
 
-    JPanel colourPanel = new JPanel(new GridLayout(4, 1));
-    colourPanel.setBorder(BorderFactory.createTitledBorder("Color level"));
-    fullColour = new GroupedJRadioButton("Full (all available colors)",
-                                          colourGroup, colourPanel);
-    mediumColour = new GroupedJRadioButton("Medium (256 colors)",
-                                            colourGroup, colourPanel);
-    lowColour = new GroupedJRadioButton("Low (64 colours)",
-                                        colourGroup, colourPanel);
-    veryLowColour = new GroupedJRadioButton("Very low(8 colors)",
-                                            colourGroup, colourPanel);
+    JPanel colorPanel = new JPanel(new GridLayout(4, 1));
+    colorPanel.setBorder(BorderFactory.createTitledBorder("Color level"));
+    fullcolorButton = new GroupedJRadioButton("Full (all available colors)",
+                                              colorlevelGroup, colorPanel);
+    mediumcolorButton = new GroupedJRadioButton("Medium (256 colors)",
+                                                colorlevelGroup, colorPanel);
+    lowcolorButton = new GroupedJRadioButton("Low (64 colors)",
+                                             colorlevelGroup, colorPanel);
+    verylowcolorButton = new GroupedJRadioButton("Very low (8 colors)",
+                                                 colorlevelGroup, colorPanel);
 
     JPanel encodingPane = new JPanel(new GridLayout(1, 2, 5, 0));
     encodingPane.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
     encodingPane.add(encodingPanel);
-    encodingPane.add(colourPanel);
+    encodingPane.add(colorPanel);
 
     JPanel tightPanel = new JPanel(new GridBagLayout());
-    customCompressLevel = new JCheckBox("Custom Compression Level");
+    compressionCheckbox = new JCheckBox("Custom Compression Level");
+    compressionCheckbox.addItemListener(new ItemListener() {
+      public void itemStateChanged(ItemEvent e) {
+        handleCompression();
+      }
+    });
     Object[] compressionLevels = { 1, 2, 3, 4, 5, 6 };
-    compressLevel  = new MyJComboBox(compressionLevels);
-    ((MyJComboBox)compressLevel).setDocument(new IntegerDocument(1));
-    compressLevel.setPrototypeDisplayValue("0.");
-    compressLevel.setEditable(true);
+    compressionInput = new MyJComboBox(compressionLevels);
+    ((MyJComboBox)compressionInput).setDocument(new IntegerDocument(1));
+    compressionInput.setPrototypeDisplayValue("0.");
+    compressionInput.setEditable(true);
     JLabel compressionLabel =
       new JLabel("Level (1=fast, 6=best [4-6 are rarely useful])");
-    noJpeg = new JCheckBox("Allow JPEG Compression");
+    jpegCheckbox = new JCheckBox("Allow JPEG Compression");
+    jpegCheckbox.addItemListener(new ItemListener() {
+      public void itemStateChanged(ItemEvent e) {
+        handleJpeg();
+      }
+    });
     Object[] qualityLevels = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
-    qualityLevel  = new MyJComboBox(qualityLevels);
-    qualityLevel.setPrototypeDisplayValue("0.");
+    jpegInput = new MyJComboBox(qualityLevels);
+    jpegInput.setPrototypeDisplayValue("0.");
     JLabel qualityLabel = new JLabel("Quality (0=poor, 9=best)");
 
-    tightPanel.add(customCompressLevel,
+    tightPanel.add(compressionCheckbox,
                    new GridBagConstraints(0, 0,
                                           REMAINDER, 1,
                                           LIGHT, LIGHT,
                                           LINE_START, NONE,
                                           new Insets(0, 0, 0, 0),
                                           NONE, NONE));
-    indent = getButtonLabelInset(customCompressLevel);
-    tightPanel.add(compressLevel,
+    int indent = getButtonLabelInset(compressionCheckbox);
+    tightPanel.add(compressionInput,
                    new GridBagConstraints(0, 1,
                                           1, 1,
                                           LIGHT, LIGHT,
@@ -206,15 +734,15 @@
                                           LINE_START, HORIZONTAL,
                                           new Insets(0, 5, 0, 0),
                                           NONE, NONE));
-    tightPanel.add(noJpeg,
+    tightPanel.add(jpegCheckbox,
                    new GridBagConstraints(0, 2,
                                           REMAINDER, 1,
                                           LIGHT, LIGHT, 
                                           LINE_START, NONE,
                                           new Insets(5, 0, 0, 0),
                                           NONE, NONE));
-    indent = getButtonLabelInset(noJpeg);
-    tightPanel.add(qualityLevel,
+    indent = getButtonLabelInset(jpegCheckbox);
+    tightPanel.add(jpegInput,
                    new GridBagConstraints(0, 3,
                                           1, 1,
                                           LIGHT, LIGHT,
@@ -238,8 +766,10 @@
     FormatPanel.add(autoSelectPane);
     FormatPanel.add(encodingPane);
     FormatPanel.add(tightPanel);
+    return FormatPanel;
+  }
 
-    // security tab
+  private JPanel createSecurityPanel() {
     JPanel SecPanel = new JPanel();
     SecPanel.setLayout(new BoxLayout(SecPanel,
                                      BoxLayout.PAGE_AXIS));
@@ -249,46 +779,67 @@
     vencryptPane.setLayout(new BoxLayout(vencryptPane,
                                          BoxLayout.LINE_AXIS));
     vencryptPane.setBorder(BorderFactory.createEmptyBorder(0,0,5,0));
-    secVeNCrypt = new JCheckBox("Extended encryption and "+
-                                "authentication methods (VeNCrypt)");
-    vencryptPane.add(secVeNCrypt);
-    vencryptPane.add(Box.createHorizontalGlue());
 
     JPanel encrPanel = new JPanel(new GridBagLayout());
     encrPanel.setBorder(BorderFactory.createTitledBorder("Encryption"));
-    encNone = new JCheckBox("None");
-    encTLS = new JCheckBox("Anonymous TLS");
-    encX509 = new JCheckBox("TLS with X.509 certificates");
+    encNoneCheckbox = new JCheckBox("None");
+    encTLSCheckbox = new JCheckBox("Anonymous TLS");
+    encX509Checkbox = new JCheckBox("TLS with X.509 certificates");
+    encX509Checkbox.addItemListener(new ItemListener() {
+      public void itemStateChanged(ItemEvent e) {
+        handleX509();
+      }
+    });
     JLabel caLabel = new JLabel("X.509 CA Certificate");
-    x509ca = new JTextField();
-    x509ca.setName(Configuration.getParam("x509ca").getName());
-    caButton = new JButton("Browse");
+    caInput = new JTextField();
+    caChooser = new JButton("Browse");
+    caChooser.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        JComponent c = ((JButton)e.getSource()).getRootPane();
+        File dflt = new File(CSecurityTLS.X509CA.getValueStr());
+        FileNameExtensionFilter filter =
+          new FileNameExtensionFilter("X.509 certificate", "crt", "cer", "pem");
+        File f = showChooser("Path to X509 CA certificate", dflt, c, filter);
+        if (f != null && f.exists() && f.canRead())
+          caInput.setText(f.getAbsolutePath());
+      }
+    });
     JLabel crlLabel = new JLabel("X.509 CRL file");
-    x509crl = new JTextField();
-    x509crl.setName(Configuration.getParam("x509crl").getName());
-    crlButton = new JButton("Browse");
-    encrPanel.add(encNone,
+    crlInput = new JTextField();
+    crlChooser = new JButton("Browse");
+    crlChooser.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        JComponent c = ((JButton)e.getSource()).getRootPane();
+        File dflt = new File(CSecurityTLS.X509CRL.getValueStr());
+        FileNameExtensionFilter filter =
+          new FileNameExtensionFilter("X.509 CRL", "crl");
+        File f = showChooser("Path to X509 CRL file", dflt, c, filter);
+        if (f != null && f.exists() && f.canRead())
+          crlInput.setText(f.getAbsolutePath());
+      }
+    });
+    encrPanel.add(encNoneCheckbox,
                   new GridBagConstraints(0, 0,
                                          REMAINDER, 1,
                                          HEAVY, LIGHT,
                                          LINE_START, NONE,
                                          new Insets(0, 0, 4, 0),
                                          NONE, NONE));
-    encrPanel.add(encTLS,
+    encrPanel.add(encTLSCheckbox,
                   new GridBagConstraints(0, 1,
                                          REMAINDER, 1,
                                          HEAVY, LIGHT,
                                          LINE_START, NONE,
                                          new Insets(0, 0, 4, 0),
                                          NONE, NONE));
-    encrPanel.add(encX509,
+    encrPanel.add(encX509Checkbox,
                   new GridBagConstraints(0, 2,
                                          3, 1,
                                          HEAVY, LIGHT,
                                          LINE_START, NONE,
                                          new Insets(0, 0, 0, 0),
                                          NONE, NONE));
-    indent = getButtonLabelInset(encX509);
+    int indent = getButtonLabelInset(encX509Checkbox);
     encrPanel.add(caLabel,
                   new GridBagConstraints(0, 3,
                                          1, 1,
@@ -296,14 +847,14 @@
                                          LINE_END, NONE,
                                          new Insets(0, indent, 5, 0),
                                          0, 0));
-    encrPanel.add(x509ca,
+    encrPanel.add(caInput,
                   new GridBagConstraints(1, 3,
                                          1, 1,
                                          HEAVY, LIGHT,
                                          LINE_START, HORIZONTAL,
                                          new Insets(0, 5, 5, 0),
                                          0, 0));
-    encrPanel.add(caButton,
+    encrPanel.add(caChooser,
                   new GridBagConstraints(2, 3,
                                          1, 1,
                                          LIGHT, LIGHT,
@@ -317,14 +868,14 @@
                                          LINE_END, NONE,
                                          new Insets(0, indent, 0, 0),
                                          0, 0));
-    encrPanel.add(x509crl,
+    encrPanel.add(crlInput,
                   new GridBagConstraints(1, 4,
                                          1, 1,
                                          HEAVY, LIGHT,
                                          LINE_START, HORIZONTAL,
                                          new Insets(0, 5, 0, 0),
                                          0, 0));
-    encrPanel.add(crlButton,
+    encrPanel.add(crlChooser,
                   new GridBagConstraints(2, 4,
                                          1, 1,
                                          LIGHT, LIGHT,
@@ -335,40 +886,50 @@
     JPanel authPanel = new JPanel(new GridBagLayout());
     authPanel.setBorder(BorderFactory.createTitledBorder("Authentication"));
 
-    secNone = new JCheckBox("None");
-    secVnc = new JCheckBox("Standard VNC");
-    secPlain = new JCheckBox("Plaintext");
-    secIdent = new JCheckBox("Ident");
-    sendLocalUsername = new JCheckBox("Send Local Username");
-    authPanel.add(secNone,
+    authNoneCheckbox = new JCheckBox("None");
+    authVncCheckbox = new JCheckBox("Standard VNC");
+    authPlainCheckbox = new JCheckBox("Plaintext");
+    authPlainCheckbox.addItemListener(new ItemListener() {
+      public void itemStateChanged(ItemEvent e) {
+        handleSendLocalUsername();
+      }
+    });
+    authIdentCheckbox = new JCheckBox("Ident");
+    authIdentCheckbox.addItemListener(new ItemListener() {
+      public void itemStateChanged(ItemEvent e) {
+        handleSendLocalUsername();
+      }
+    });
+    sendLocalUsernameCheckbox = new JCheckBox("Send Local Username");
+    authPanel.add(authNoneCheckbox,
                   new GridBagConstraints(0, 0,
                                          REMAINDER, 1,
                                          LIGHT, LIGHT,
                                          LINE_START, NONE,
                                          new Insets(0, 0, 4, 0),
                                          NONE, NONE));
-    authPanel.add(secVnc,
+    authPanel.add(authVncCheckbox,
                   new GridBagConstraints(0, 1,
                                          REMAINDER, 1,
                                          LIGHT, LIGHT,
                                          LINE_START, NONE,
                                          new Insets(0, 0, 4, 0),
                                          NONE, NONE));
-    authPanel.add(secPlain,
+    authPanel.add(authPlainCheckbox,
                   new GridBagConstraints(0, 2,
                                          1, 1,
                                          LIGHT, LIGHT,
                                          LINE_START, NONE,
                                          new Insets(0, 0, 2, 0),
                                          NONE, NONE));
-    authPanel.add(secIdent,
+    authPanel.add(authIdentCheckbox,
                   new GridBagConstraints(0, 3,
                                          1, 1,
                                          LIGHT, LIGHT,
                                          LINE_START, NONE,
                                          new Insets(2, 0, 0, 0),
                                          NONE, NONE));
-    authPanel.add(sendLocalUsername,
+    authPanel.add(sendLocalUsernameCheckbox,
                   new GridBagConstraints(1, 2,
                                          1, 2,
                                          HEAVY, LIGHT,
@@ -404,35 +965,40 @@
                                         LINE_START, BOTH,
                                         new Insets(0, 0, 0, 0),
                                         NONE, NONE));
+    return SecPanel;
+  }
 
-    // Input tab
+  private JPanel createInputPanel() {
     JPanel inputPanel = new JPanel(new GridBagLayout());
     inputPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5));
 
-    viewOnly = new JCheckBox("View Only (ignore mouse & keyboard)");
-    acceptClipboard = new JCheckBox("Accept clipboard from server");
-    sendClipboard = new JCheckBox("Send clipboard to server");
-    JLabel menuKeyLabel = new JLabel("Menu Key");
+    viewOnlyCheckbox = new JCheckBox("View only (ignore mouse and keyboard)");
+    acceptClipboardCheckbox = new JCheckBox("Accept clipboard from server");
+    sendClipboardCheckbox = new JCheckBox("Send clipboard to server");
+    JLabel menuKeyLabel = new JLabel("Menu key");
     String[] menuKeys = new String[MenuKey.getMenuKeySymbolCount()];
+    //String[] menuKeys = new String[MenuKey.getMenuKeySymbolCount()+1];
+    //menuKeys[0] = "None";
     for (int i = 0; i < MenuKey.getMenuKeySymbolCount(); i++)
       menuKeys[i] = MenuKey.getKeyText(MenuKey.getMenuKeySymbols()[i]);
-    menuKey  = new JComboBox(menuKeys);
+      //menuKeys[i+1] = MenuKey.getKeyText(MenuKey.getMenuKeySymbols()[i]);
+    menuKeyChoice = new JComboBox(menuKeys);
 
-    inputPanel.add(viewOnly,
+    inputPanel.add(viewOnlyCheckbox,
                    new GridBagConstraints(0, 0,
                                           REMAINDER, 1,
                                           HEAVY, LIGHT,
                                           LINE_START, NONE,
                                           new Insets(0, 0, 4, 0),
                                           NONE, NONE));
-    inputPanel.add(acceptClipboard,
+    inputPanel.add(acceptClipboardCheckbox,
                    new GridBagConstraints(0, 1,
                                           REMAINDER, 1,
                                           HEAVY, LIGHT,
                                           LINE_START, NONE,
                                           new Insets(0, 0, 4, 0),
                                           NONE, NONE));
-    inputPanel.add(sendClipboard,
+    inputPanel.add(sendClipboardCheckbox,
                    new GridBagConstraints(0, 2,
                                           REMAINDER, 1,
                                           HEAVY, LIGHT,
@@ -446,7 +1012,7 @@
                                           LINE_START, NONE,
                                           new Insets(0, 0, 0, 0),
                                           NONE, NONE));
-    inputPanel.add(menuKey,
+    inputPanel.add(menuKeyChoice,
                    new GridBagConstraints(1, 3,
                                           1, 1,
                                           HEAVY, LIGHT,
@@ -460,108 +1026,147 @@
                                           LINE_START, BOTH,
                                           new Insets(0, 0, 0, 0),
                                           NONE, NONE));
+    return inputPanel;
+  }
 
-    // Screen tab
+  private JPanel createScreenPanel() {
     JPanel ScreenPanel = new JPanel(new GridBagLayout());
     ScreenPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5));
-    desktopSize = new JCheckBox("Resize remote session on connect");
-    desktopSize.setEnabled(!cc.viewer.embed.getValue() &&
-                           (cc.viewer.desktopSize.getValue() != null));
-    desktopWidth = new IntegerTextField(5);
-    desktopWidth.setEnabled(desktopSize.isSelected());
-    desktopHeight = new IntegerTextField(5);
-    desktopHeight.setEnabled(desktopSize.isSelected());
+
+    JPanel SizingPanel = new JPanel(new GridBagLayout());
+    SizingPanel.setBorder(BorderFactory.createTitledBorder("Desktop Sizing"));
+    desktopSizeCheckbox = new JCheckBox("Resize remote session on connect");
+    desktopSizeCheckbox.addItemListener(new ItemListener() {
+      public void itemStateChanged(ItemEvent e) {
+        handleDesktopSize();
+      }
+    });
+    desktopWidthInput = new IntegerTextField(5);
+    desktopHeightInput = new IntegerTextField(5);
     JPanel desktopSizePanel =
       new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0));
-    desktopSizePanel.add(desktopWidth);
+    desktopSizePanel.add(desktopWidthInput);
     desktopSizePanel.add(new JLabel(" x "));
-    desktopSizePanel.add(desktopHeight);
-    fullScreen = new JCheckBox("Full-screen mode");
-    fullScreen.setEnabled(!cc.viewer.embed.getValue());
-    fullScreenAllMonitors =
-      new JCheckBox("Enable full-screen mode over all monitors");
-    fullScreenAllMonitors.setEnabled(!cc.viewer.embed.getValue());
+    desktopSizePanel.add(desktopHeightInput);
+    sizingGroup = new ButtonGroup();
+    remoteResizeButton =
+      new JRadioButton("Resize remote session to the local window");
+    sizingGroup.add(remoteResizeButton);
+    remoteScaleButton =
+      new JRadioButton("Scale remote session to the local window");
+    sizingGroup.add(remoteScaleButton);
+    remoteResizeButton.addItemListener(new ItemListener() {
+      public void itemStateChanged(ItemEvent e) {
+        handleRemoteResize();
+      }
+    });
     JLabel scalingFactorLabel = new JLabel("Scaling Factor");
     Object[] scalingFactors = {
       "Auto", "Fixed Aspect Ratio", "50%", "75%", "95%", "100%", "105%",
       "125%", "150%", "175%", "200%", "250%", "300%", "350%", "400%" };
-    scalingFactor = new MyJComboBox(scalingFactors);
-    scalingFactor.setEditable(true);
-    scalingFactor.setEnabled(!cc.viewer.embed.getValue());
-    ScreenPanel.add(desktopSize,
+    scalingFactorInput = new MyJComboBox(scalingFactors);
+    scalingFactorInput.setEditable(true);
+    fullScreenCheckbox = new JCheckBox("Full-screen mode");
+    fullScreenAllMonitorsCheckbox =
+      new JCheckBox("Enable full-screen mode over all monitors");
+    SizingPanel.add(desktopSizeCheckbox,
                     new GridBagConstraints(0, 0,
                                            REMAINDER, 1,
                                            LIGHT, LIGHT,
                                            LINE_START, NONE,
                                            new Insets(0, 0, 0, 0),
                                            NONE, NONE));
-    indent = getButtonLabelInset(desktopSize);
-    ScreenPanel.add(desktopSizePanel,
+    int indent = getButtonLabelInset(desktopSizeCheckbox);
+    SizingPanel.add(desktopSizePanel,
                     new GridBagConstraints(0, 1,
                                            REMAINDER, 1,
                                            LIGHT, LIGHT,
                                            LINE_START, NONE,
                                            new Insets(0, indent, 0, 0),
                                            NONE, NONE));
-    ScreenPanel.add(fullScreen,
+    SizingPanel.add(remoteResizeButton,
                     new GridBagConstraints(0, 2,
                                            REMAINDER, 1,
                                            LIGHT, LIGHT,
                                            LINE_START, NONE,
                                            new Insets(0, 0, 4, 0),
                                            NONE, NONE));
-    indent = getButtonLabelInset(fullScreen);
-    ScreenPanel.add(fullScreenAllMonitors,
+    SizingPanel.add(remoteScaleButton,
                     new GridBagConstraints(0, 3,
                                            REMAINDER, 1,
                                            LIGHT, LIGHT,
                                            LINE_START, NONE,
-                                           new Insets(0, indent, 4, 0),
+                                           new Insets(0, 0, 4, 0),
                                            NONE, NONE));
-    ScreenPanel.add(scalingFactorLabel,
+    indent = getButtonLabelInset(remoteScaleButton);
+    SizingPanel.add(scalingFactorLabel,
                     new GridBagConstraints(0, 4,
                                            1, 1,
                                            LIGHT, LIGHT,
                                            LINE_START, NONE,
-                                           new Insets(0, 0, 4, 0),
+                                           new Insets(0, indent, 4, 0),
                                            NONE, NONE));
-    ScreenPanel.add(scalingFactor,
+    SizingPanel.add(scalingFactorInput,
                     new GridBagConstraints(1, 4,
                                            1, 1,
                                            HEAVY, LIGHT,
                                            LINE_START, NONE,
                                            new Insets(0, 5, 4, 0),
                                            NONE, NONE));
+    ScreenPanel.add(SizingPanel,
+                    new GridBagConstraints(0, 0,
+                                           REMAINDER, 1,
+                                           LIGHT, LIGHT,
+                                           LINE_START, HORIZONTAL,
+                                           new Insets(0, 0, 4, 0),
+                                           NONE, NONE));
+    ScreenPanel.add(fullScreenCheckbox,
+                    new GridBagConstraints(0, 1,
+                                           REMAINDER, 1,
+                                           LIGHT, LIGHT,
+                                           LINE_START, NONE,
+                                           new Insets(0, 0, 4, 0),
+                                           NONE, NONE));
+    indent = getButtonLabelInset(fullScreenCheckbox);
+    ScreenPanel.add(fullScreenAllMonitorsCheckbox,
+                    new GridBagConstraints(0, 2,
+                                           REMAINDER, 1,
+                                           LIGHT, LIGHT,
+                                           LINE_START, NONE,
+                                           new Insets(0, indent, 4, 0),
+                                           NONE, NONE));
     ScreenPanel.add(Box.createRigidArea(new Dimension(5, 0)),
-                    new GridBagConstraints(0, 5,
+                    new GridBagConstraints(0, 3,
                                            REMAINDER, REMAINDER,
                                            HEAVY, HEAVY,
                                            LINE_START, BOTH,
                                            new Insets(0, 0, 0, 0),
                                            NONE, NONE));
+    return ScreenPanel;
+  }
 
-    // Misc tab
+  private JPanel createMiscPanel() {
     JPanel MiscPanel = new JPanel(new GridBagLayout());
     MiscPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5));
-    shared =
-      new JCheckBox("Shared connection (do not disconnect other viewers)");
-    useLocalCursor = new JCheckBox("Render cursor locally");
-    acceptBell = new JCheckBox("Beep when requested by the server");
-    MiscPanel.add(shared,
+    sharedCheckbox =
+      new JCheckBox("Shared (don't disconnect other viewers)");
+    dotWhenNoCursorCheckbox = new JCheckBox("Show dot when no cursor");
+    acceptBellCheckbox = new JCheckBox("Beep when requested by the server");
+    MiscPanel.add(sharedCheckbox,
                   new GridBagConstraints(0, 0,
                                          1, 1,
                                          LIGHT, LIGHT,
                                          LINE_START, NONE,
                                          new Insets(0, 0, 4, 0),
                                          NONE, NONE));
-    MiscPanel.add(useLocalCursor,
+    MiscPanel.add(dotWhenNoCursorCheckbox,
                   new GridBagConstraints(0, 1,
                                          1, 1,
                                          LIGHT, LIGHT,
                                          LINE_START, NONE,
                                          new Insets(0, 0, 4, 0),
                                          NONE, NONE));
-    MiscPanel.add(acceptBell,
+    MiscPanel.add(acceptBellCheckbox,
                   new GridBagConstraints(0, 2,
                                          1, 1,
                                          LIGHT, LIGHT,
@@ -575,52 +1180,101 @@
                                          LINE_START, BOTH,
                                          new Insets(0, 0, 0, 0),
                                          NONE, NONE));
+    return MiscPanel;
+  }
 
-    // SSH tab
+  private JPanel createSshPanel() {
     JPanel sshPanel = new JPanel(new GridBagLayout());
     sshPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5));
-    sshTunnel = new JCheckBox("Tunnel VNC over SSH");
+    ButtonGroup sshArgsGroup = new ButtonGroup();
+    tunnelCheckbox = new JCheckBox("Tunnel VNC over SSH");
+    tunnelCheckbox.addItemListener(new ItemListener() {
+      public void itemStateChanged(ItemEvent e) {
+        handleTunnel();
+      }
+    });
 
     JPanel tunnelPanel = new JPanel(new GridBagLayout());
 
-    sshUseGateway = new JCheckBox("Use SSH gateway");
+    viaCheckbox = new JCheckBox("Use SSH gateway");
+    viaCheckbox.addItemListener(new ItemListener() {
+      public void itemStateChanged(ItemEvent e) {
+        handleVia();
+      }
+    });
     JLabel sshUserLabel = new JLabel("Username");
-    sshUser = new JTextField();
+    viaUserInput = new JTextField();
     JLabel sshUserAtLabel = new JLabel("@");
     JLabel sshHostLabel = new JLabel("Hostname (or IP address)");
-    sshHost = new JTextField("");
+    viaHostInput = new JTextField("");
     JLabel sshPortLabel = new JLabel("Port");
-    sshPort = new IntegerTextField(5);
+    viaPortInput = new IntegerTextField(5);
 
-    sshUseExt = new JCheckBox("Use external SSH client");
-    sshClient = new JTextField();
-    sshClient.setName(Configuration.getParam("extSSHClient").getName());
-    sshClientBrowser = new JButton("Browse");
+    extSSHCheckbox = new JCheckBox("Use external SSH client");
+    extSSHCheckbox.addItemListener(new ItemListener() {
+      public void itemStateChanged(ItemEvent e) {
+        handleExtSSH();
+      }
+    });
+    sshClientInput = new JTextField();
+    sshClientChooser = new JButton("Browse");
+    sshClientChooser.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        JComponent c = ((JButton)e.getSource()).getRootPane();
+        File dflt = new File(extSSHClient.getValueStr());
+        File f = showChooser("Path to external SSH client", dflt, c);
+        if (f != null && f.exists() && f.isFile() && f.canExecute())
+          sshClientInput.setText(f.getAbsolutePath());
+      }
+    });
     JLabel sshConfigLabel = new JLabel("SSH config file");
-    sshConfig = new JTextField();
-    sshConfig.setName(Configuration.getParam("sshConfig").getName());
-    sshConfigBrowser = new JButton("Browse");
+    sshConfigInput = new JTextField();
+    sshConfigChooser = new JButton("Browse");
+    sshConfigChooser.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        JComponent c = ((JButton)e.getSource()).getRootPane();
+        File dflt = new File(sshConfig.getValueStr());
+        File f = showChooser("Path to OpenSSH client config file", dflt, c);
+        if (f != null && f.exists() && f.isFile() && f.canRead())
+          sshConfigInput.setText(f.getAbsolutePath());
+      }
+    });
     JLabel sshKeyFileLabel = new JLabel("SSH identity file");
-    sshKeyFile = new JTextField();
-    sshKeyFile.setName(Configuration.getParam("sshKeyFile").getName());
-    sshKeyFileBrowser = new JButton("Browse");
+    sshKeyFileInput = new JTextField();
+    sshKeyFileChooser = new JButton("Browse");
+    sshKeyFileChooser.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        JComponent c = ((JButton)e.getSource()).getRootPane();
+        File f = showChooser("Path to SSH key file", null, c);
+        if (f != null && f.exists() && f.isFile() && f.canRead())
+          sshKeyFileInput.setText(f.getAbsolutePath());
+      }
+    });
     JPanel sshArgsPanel = new JPanel(new GridBagLayout());
     JLabel sshArgsLabel = new JLabel("Arguments:");
-    sshArgsDefault =
-      new GroupedJRadioButton("Default", sshArgsGroup, sshArgsPanel);
-    sshArgsCustom =
-      new GroupedJRadioButton("Custom", sshArgsGroup, sshArgsPanel);
-    sshArguments = new JTextField();
+    sshArgsDefaultButton = new GroupedJRadioButton("Default", sshArgsGroup, sshArgsPanel);
+    sshArgsDefaultButton.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        sshArgsInput.setEnabled(sshArgsCustomButton.isSelected());
+      }
+    });
+    sshArgsCustomButton = new GroupedJRadioButton("Custom", sshArgsGroup, sshArgsPanel);
+    sshArgsCustomButton.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        sshArgsInput.setEnabled(sshArgsCustomButton.isSelected());
+      }
+    });
+    sshArgsInput = new JTextField();
 
     JPanel gatewayPanel = new JPanel(new GridBagLayout());
-    gatewayPanel.add(sshUseGateway,
+    gatewayPanel.add(viaCheckbox,
                     new GridBagConstraints(0, 0,
                                            REMAINDER, 1,
                                            LIGHT, LIGHT,
                                            LINE_START, NONE,
                                            new Insets(0, 0, 4, 0),
                                            NONE, NONE));
-    indent = getButtonLabelInset(sshUseGateway);
+    int indent = getButtonLabelInset(viaCheckbox);
     gatewayPanel.add(sshUserLabel,
                  new GridBagConstraints(0, 1,
                                         1, 1,
@@ -642,7 +1296,7 @@
                                         LINE_START, HORIZONTAL,
                                         new Insets(0, 5, 4, 0),
                                         NONE, NONE));
-    gatewayPanel.add(sshUser,
+    gatewayPanel.add(viaUserInput,
                  new GridBagConstraints(0, 2,
                                         1, 1,
                                         LIGHT, LIGHT,
@@ -656,14 +1310,14 @@
                                         LINE_START, HORIZONTAL,
                                         new Insets(0, 2, 0, 2),
                                         NONE, NONE));
-    gatewayPanel.add(sshHost,
+    gatewayPanel.add(viaHostInput,
                  new GridBagConstraints(2, 2,
                                         1, 1,
                                         HEAVY, LIGHT,
                                         LINE_START, HORIZONTAL,
                                         new Insets(0, 0, 0, 0),
                                         NONE, NONE));
-    gatewayPanel.add(sshPort,
+    gatewayPanel.add(viaPortInput,
                  new GridBagConstraints(3, 2,
                                         1, 1,
                                         LIGHT, LIGHT,
@@ -672,21 +1326,21 @@
                                         NONE, NONE));
 
     JPanel clientPanel = new JPanel(new GridBagLayout());
-    clientPanel.add(sshUseExt,
+    clientPanel.add(extSSHCheckbox,
                     new GridBagConstraints(0, 0,
                                            1, 1,
                                            LIGHT, LIGHT,
                                            LINE_START, NONE,
                                            new Insets(0, 0, 0, 0),
                                            NONE, NONE));
-    clientPanel.add(sshClient,
+    clientPanel.add(sshClientInput,
                     new GridBagConstraints(1, 0,
                                            1, 1,
                                            HEAVY, LIGHT,
                                            LINE_START, HORIZONTAL,
                                            new Insets(0, 5, 0, 0),
                                            NONE, NONE));
-    clientPanel.add(sshClientBrowser,
+    clientPanel.add(sshClientChooser,
                     new GridBagConstraints(2, 0,
                                            1, 1,
                                            LIGHT, LIGHT,
@@ -700,28 +1354,28 @@
                                            LINE_START, NONE,
                                            new Insets(0, 0, 0, 0),
                                            NONE, NONE));
-    sshArgsPanel.add(sshArgsDefault,
+    sshArgsPanel.add(sshArgsDefaultButton,
                     new GridBagConstraints(1, 1,
                                            1, 1,
                                            LIGHT, LIGHT,
                                            LINE_START, NONE,
                                            new Insets(0, 5, 0, 0),
                                            NONE, NONE));
-    sshArgsPanel.add(sshArgsCustom,
+    sshArgsPanel.add(sshArgsCustomButton,
                     new GridBagConstraints(2, 1,
                                            1, 1,
                                            LIGHT, LIGHT,
                                            LINE_START, NONE,
                                            new Insets(0, 5, 0, 0),
                                            NONE, NONE));
-    sshArgsPanel.add(sshArguments,
+    sshArgsPanel.add(sshArgsInput,
                     new GridBagConstraints(3, 1,
                                            1, 1,
                                            HEAVY, LIGHT,
                                            LINE_START, HORIZONTAL,
                                            new Insets(0, 5, 0, 0),
                                            NONE, NONE));
-    indent = getButtonLabelInset(sshUseExt);
+    indent = getButtonLabelInset(extSSHCheckbox);
     clientPanel.add(sshArgsPanel,
                     new GridBagConstraints(0, 1,
                                            REMAINDER, 1,
@@ -731,7 +1385,9 @@
                                            NONE, NONE));
 
     JPanel opensshPanel = new JPanel(new GridBagLayout());
-    opensshPanel.setBorder(BorderFactory.createTitledBorder("Embedded SSH client configuration"));
+    TitledBorder border =
+      BorderFactory.createTitledBorder("Embedded SSH client configuration");
+    opensshPanel.setBorder(border);
     opensshPanel.add(sshConfigLabel,
                      new GridBagConstraints(0, 0,
                                             1, 1,
@@ -739,14 +1395,14 @@
                                             LINE_START, NONE,
                                             new Insets(0, 0, 5, 0),
                                             NONE, NONE));
-    opensshPanel.add(sshConfig,
+    opensshPanel.add(sshConfigInput,
                      new GridBagConstraints(1, 0,
                                             1, 1,
                                             HEAVY, LIGHT,
                                             LINE_START, HORIZONTAL,
                                             new Insets(0, 5, 5, 0),
                                             NONE, NONE));
-    opensshPanel.add(sshConfigBrowser,
+    opensshPanel.add(sshConfigChooser,
                      new GridBagConstraints(2, 0,
                                             1, 1,
                                             LIGHT, LIGHT,
@@ -760,14 +1416,14 @@
                                             LINE_START, NONE,
                                             new Insets(0, 0, 0, 0),
                                             NONE, NONE));
-    opensshPanel.add(sshKeyFile,
+    opensshPanel.add(sshKeyFileInput,
                      new GridBagConstraints(1, 1,
                                             1, 1,
                                             HEAVY, LIGHT,
                                             LINE_START, HORIZONTAL,
                                             new Insets(0, 5, 0, 0),
                                             NONE, NONE));
-    opensshPanel.add(sshKeyFileBrowser,
+    opensshPanel.add(sshKeyFileChooser,
                      new GridBagConstraints(2, 1,
                                             1, 1,
                                             LIGHT, LIGHT,
@@ -796,14 +1452,14 @@
                                            new Insets(0, 0, 0, 0),
                                            NONE, NONE));
 
-    sshPanel.add(sshTunnel,
+    sshPanel.add(tunnelCheckbox,
                  new GridBagConstraints(0, 0,
                                         REMAINDER, 1,
                                         LIGHT, LIGHT,
                                         LINE_START, NONE,
                                         new Insets(0, 0, 4, 0),
                                         NONE, NONE));
-    indent = getButtonLabelInset(sshTunnel);
+    indent = getButtonLabelInset(tunnelCheckbox);
     sshPanel.add(tunnelPanel,
                  new GridBagConstraints(0, 2,
                                         REMAINDER, 1,
@@ -818,601 +1474,149 @@
                                         LINE_START, BOTH,
                                         new Insets(0, 0, 0, 0),
                                         NONE, NONE));
-
-    // load/save tab
-    JPanel loadSavePanel = new JPanel(new GridBagLayout());
-    loadSavePanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5));
-    JPanel configPanel = new JPanel(new GridBagLayout());
-    configPanel.
-      setBorder(BorderFactory.createTitledBorder("Configuration File"));
-    cfLoadButton = new JButton("Load");
-    cfSaveAsButton = new JButton("Save As...");
-    configPanel.add(cfLoadButton,
-                    new GridBagConstraints(0, 0,
-                                           1, 1,
-                                           HEAVY, LIGHT,
-                                           CENTER, HORIZONTAL,
-                                           new Insets(0, 0, 5, 0),
-                                           NONE, NONE));
-    configPanel.add(cfSaveAsButton,
-                    new GridBagConstraints(0, 1,
-                                           1, 1,
-                                           HEAVY, HEAVY,
-                                           CENTER, HORIZONTAL,
-                                           new Insets(0, 0, 0, 0),
-                                           NONE, NONE));
-
-    JPanel defaultsPanel = new JPanel(new GridBagLayout());
-    defaultsPanel.setBorder(BorderFactory.createTitledBorder("Defaults"));
-    defClearButton = new JButton("Clear");
-    defReloadButton = new JButton("Reload");
-    defSaveButton = new JButton("Save");
-    defaultsPanel.add(defClearButton,
-                      new GridBagConstraints(0, 0,
-                                             1, 1,
-                                             HEAVY, LIGHT,
-                                             CENTER, HORIZONTAL,
-                                             new Insets(0, 0, 5, 0),
-                                             NONE, NONE));
-    defaultsPanel.add(defReloadButton,
-                      new GridBagConstraints(0, 1,
-                                             1, 1,
-                                             HEAVY, LIGHT,
-                                             CENTER, HORIZONTAL,
-                                             new Insets(0, 0, 5, 0),
-                                             NONE, NONE));
-    defaultsPanel.add(defSaveButton,
-                      new GridBagConstraints(0, 2,
-                                             1, 1,
-                                             HEAVY, HEAVY,
-                                             CENTER, HORIZONTAL,
-                                             new Insets(0, 0, 0, 0),
-                                             NONE, NONE));
-
-    loadSavePanel.add(configPanel,
-                      new GridBagConstraints(0, 0,
-                                             1, 1,
-                                             HEAVY, LIGHT,
-                                             PAGE_START, HORIZONTAL,
-                                             new Insets(0, 0, 0, 0),
-                                             NONE, NONE));
-    loadSavePanel.add(Box.createRigidArea(new Dimension(5, 0)),
-                      new GridBagConstraints(1, 1,
-                                             1, 1,
-                                             LIGHT, LIGHT,
-                                             LINE_START, NONE,
-                                             new Insets(0, 0, 0, 0),
-                                             NONE, NONE));
-    loadSavePanel.add(defaultsPanel,
-                      new GridBagConstraints(2, 0,
-                                             1, 1,
-                                             HEAVY, LIGHT,
-                                             PAGE_START, HORIZONTAL,
-                                             new Insets(0, 0, 0, 0),
-                                             NONE, NONE));
-    loadSavePanel.add(Box.createRigidArea(new Dimension(5, 0)),
-                      new GridBagConstraints(0, 1,
-                                             REMAINDER, REMAINDER,
-                                             HEAVY, HEAVY,
-                                             LINE_START, BOTH,
-                                             new Insets(0, 0, 0, 0),
-                                             NONE, NONE));
-
-    // tabPane
-    tabPane.addTab("Compression", FormatPanel);
-    tabPane.addTab("Security", SecPanel);
-    tabPane.addTab("Input", inputPanel);
-    tabPane.addTab("Screen", ScreenPanel);
-    tabPane.addTab("Misc", MiscPanel);
-    tabPane.addTab("SSH", sshPanel);
-    tabPane.addTab("Load / Save", loadSavePanel);
-    tabPane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
-    // Resize the tabPane if necessary to prevent scrolling
-    Insets tpi =
-      (Insets)UIManager.get("TabbedPane:TabbedPaneTabArea.contentMargins");
-    int minWidth = tpi.left + tpi.right;
-    for (int i = 0; i < tabPane.getTabCount(); i++)
-      minWidth += tabPane.getBoundsAt(i).width;
-    int minHeight = tabPane.getPreferredSize().height;
-    if (tabPane.getPreferredSize().width < minWidth)
-      tabPane.setPreferredSize(new Dimension(minWidth, minHeight));
-
-    // button pane
-    okButton = new JButton("OK");
-    cancelButton = new JButton("Cancel");
-
-    JPanel buttonPane = new JPanel(new GridLayout(1, 5, 10, 10));
-    buttonPane.setBorder(BorderFactory.createEmptyBorder(10, 5, 5, 5));
-    buttonPane.add(Box.createRigidArea(new Dimension()));
-    buttonPane.add(Box.createRigidArea(new Dimension()));
-    buttonPane.add(Box.createRigidArea(new Dimension()));
-    buttonPane.add(okButton);
-    buttonPane.add(cancelButton);
-
-    this.add(tabPane);
-    this.add(buttonPane);
-    addListeners(this);
-    pack();
+    return sshPanel;
   }
 
-  public void initDialog() {
-    if (cc != null) cc.setOptions();
-    zrle.setEnabled(!autoSelect.isSelected());
-    hextile.setEnabled(!autoSelect.isSelected());
-    tight.setEnabled(!autoSelect.isSelected());
-    raw.setEnabled(!autoSelect.isSelected());
-    fullColour.setEnabled(!autoSelect.isSelected());
-    mediumColour.setEnabled(!autoSelect.isSelected());
-    lowColour.setEnabled(!autoSelect.isSelected());
-    veryLowColour.setEnabled(!autoSelect.isSelected());
-    compressLevel.setEnabled(customCompressLevel.isSelected());
-    qualityLevel.setEnabled(noJpeg.isSelected());
-    sendLocalUsername.setEnabled(secVeNCrypt.isEnabled() &&
-      (secPlain.isSelected() || secIdent.isSelected()));
-    sshArguments.setEnabled(sshTunnel.isSelected() &&
-      (sshUseExt.isSelected() && sshArgsCustom.isSelected()));
+  private void handleAutoselect()
+  {
+    ButtonGroup[] groups = { encodingGroup, colorlevelGroup };
+    for (ButtonGroup grp : groups) {
+      Enumeration<AbstractButton> elems = grp.getElements();
+      while (elems.hasMoreElements())
+        elems.nextElement().setEnabled(!autoselectCheckbox.isSelected());
+    }
+
+    // JPEG setting is also affected by autoselection
+    jpegCheckbox.setEnabled(!autoselectCheckbox.isSelected());
+    handleJpeg();
   }
 
-  private void updatePreferences() {
-    if (autoSelect.isSelected()) {
-      UserPreferences.set("global", "AutoSelect", true);
-    } else {
-      UserPreferences.set("global", "AutoSelect", false);
-      if (zrle.isSelected()) {
-        UserPreferences.set("global", "PreferredEncoding", "ZRLE");
-      } else if (hextile.isSelected()) {
-        UserPreferences.set("global", "PreferredEncoding", "hextile");
-      } else if (tight.isSelected()) {
-        UserPreferences.set("global", "PreferredEncoding", "Tight");
-      } else if (raw.isSelected()) {
-        UserPreferences.set("global", "PreferredEncoding", "raw");
-      }
-    }
-    if (fullColour.isSelected()) {
-      UserPreferences.set("global", "FullColour", true);
-    } else {
-      UserPreferences.set("global", "FullColour", false);
-      if (mediumColour.isSelected()) {
-        UserPreferences.set("global", "LowColorLevel", 2);
-      } else if (lowColour.isSelected()) {
-        UserPreferences.set("global", "LowColorLevel", 1);
-      } else if (veryLowColour.isSelected()) {
-        UserPreferences.set("global", "LowColorLevel", 0);
-      }
-    }
-    UserPreferences.set("global", "NoJPEG", !noJpeg.isSelected());
-    UserPreferences.set("global",
-            "QualityLevel", (Integer)qualityLevel.getSelectedItem());
-    UserPreferences.set("global",
-            "CustomCompressLevel", customCompressLevel.isSelected());
-    UserPreferences.set("global",
-            "CompressLevel", (Integer)compressLevel.getSelectedItem());
-    UserPreferences.set("global", "ViewOnly", viewOnly.isSelected());
-    UserPreferences.set("global",
-            "AcceptClipboard", acceptClipboard.isSelected());
-    UserPreferences.set("global", "SendClipboard", sendClipboard.isSelected());
-    String menuKeyStr =
-      MenuKey.getMenuKeySymbols()[menuKey.getSelectedIndex()].name;
-    UserPreferences.set("global", "MenuKey", menuKeyStr);
-    String desktopSizeString =
-      desktopSize.isSelected() ?
-        desktopWidth.getText() + "x" + desktopHeight.getText() : "";
-    UserPreferences.set("global", "DesktopSize", desktopSizeString);
-    UserPreferences.set("global", "FullScreen", fullScreen.isSelected());
-    UserPreferences.set("global",
-            "FullScreenAllMonitors", fullScreenAllMonitors.isSelected());
-    UserPreferences.set("global", "Shared", shared.isSelected());
-    UserPreferences.set("global",
-            "UseLocalCursor", useLocalCursor.isSelected());
-    UserPreferences.set("global", "AcceptBell", acceptBell.isSelected());
-    String scaleString = scalingFactor.getSelectedItem().toString();
-    if (scaleString.equalsIgnoreCase("Auto")) {
-      UserPreferences.set("global", "ScalingFactor", "Auto");
-    } else if(scaleString.equalsIgnoreCase("Fixed Aspect Ratio")) {
-      UserPreferences.set("global", "ScalingFactor", "FixedRatio");
-    } else {
-      scaleString=scaleString.substring(0, scaleString.length()-1);
-      UserPreferences.set("global", "ScalingFactor", scaleString);
-    }
-    UserPreferences.set("viewer", "secVeNCrypt", secVeNCrypt.isSelected());
-    UserPreferences.set("viewer", "encNone", encNone.isSelected());
-    UserPreferences.set("viewer", "encTLS", encTLS.isSelected());
-    UserPreferences.set("viewer", "encX509", encX509.isSelected());
-    UserPreferences.set("viewer", "secNone", secNone.isSelected());
-    UserPreferences.set("viewer", "secVnc", secVnc.isSelected());
-    UserPreferences.set("viewer", "secPlain", secPlain.isSelected());
-    UserPreferences.set("viewer", "secIdent", secIdent.isSelected());
-    UserPreferences.set("global",
-            "SendLocalUsername", sendLocalUsername.isSelected());
-    if (!CSecurityTLS.x509ca.getValueStr().equals(""))
-      UserPreferences.set("viewer", "x509ca",
-              CSecurityTLS.x509ca.getValueStr());
-    if (!CSecurityTLS.x509crl.getValueStr().equals(""))
-      UserPreferences.set("viewer", "x509crl",
-              CSecurityTLS.x509crl.getValueStr());
-    UserPreferences.set("global", "Tunnel", sshTunnel.isSelected());
-    if (sshUseGateway.isSelected()) {
-      String via = sshUser.getText()+"@"+sshHost.getText()+":"+sshPort.getText();
-      UserPreferences.set("global", "Via", via);
-    }
-    if (sshUseExt.isSelected()) {
-      UserPreferences.set("global", "extSSH", sshUseExt.isSelected());
-      UserPreferences.set("global", "extSSHClient", sshClient.getText());
-      if (!sshArguments.getText().isEmpty())
-        UserPreferences.set("global", "extSSHArgs", sshArguments.getText());
-    }
-    UserPreferences.set("global", "SSHConfig", sshConfig.getText());
-    UserPreferences.set("global", "SSHKeyFile", sshKeyFile.getText());
+  private void handleCompression()
+  {
+    compressionInput.setEnabled(compressionCheckbox.isSelected());
   }
 
-  private void restorePreferences() {
-    autoSelect.setSelected(UserPreferences.getBool("global", "AutoSelect"));
-    if (!autoSelect.isSelected()) {
-      if (UserPreferences.getBool("global", "FullColour")) {
-        fullColour.setSelected(true);
-      } else {
-        switch (UserPreferences.getInt("global", "LowColorLevel")) {
-        case 2:
-          mediumColour.setSelected(true);
-          break;
-        case 1:
-          lowColour.setSelected(true);
-          break;
-        case 0:
-          veryLowColour.setSelected(true);
-          break;
-        }
-      }
-      String encoding = UserPreferences.get("global", "PreferredEncoding");
-      if (encoding != null) {
-        switch (Encodings.encodingNum(encoding)) {
-        case Encodings.encodingZRLE:
-          zrle.setSelected(true);
-          break;
-        case Encodings.encodingHextile:
-          hextile.setSelected(true);
-          break;
-        case Encodings.encodingRaw:
-          raw.setSelected(true);
-          break;
-        default:
-          tight.setSelected(true);
-        }
-      }
-    }
-    noJpeg.setSelected(!UserPreferences.getBool("global", "NoJPEG"));
-    qualityLevel.setSelectedItem(UserPreferences.getInt("global",
-            "QualityLevel"));
-    customCompressLevel.setSelected(UserPreferences.getBool("global",
-            "CustomCompressLevel"));
-    compressLevel.setSelectedItem(UserPreferences.getInt("global",
-            "CompressLevel"));
-    viewOnly.setSelected(UserPreferences.getBool("global", "ViewOnly"));
-    acceptClipboard.setSelected(UserPreferences.getBool("global",
-            "AcceptClipboard"));
-    sendClipboard.setSelected(UserPreferences.getBool("global",
-            "SendClipboard"));
-    menuKey.setSelectedItem(UserPreferences.get("global", "MenuKey"));
-    desktopSize.setSelected(!UserPreferences.get("global", "DesktopSize").isEmpty());
-    if (desktopSize.isSelected()) {
-      String desktopSizeString = UserPreferences.get("global", "DesktopSize");
-      desktopWidth.setText(desktopSizeString.split("x")[0]);
-      desktopHeight.setText(desktopSizeString.split("x")[1]);
-    }
-    fullScreen.setSelected(UserPreferences.getBool("global", "FullScreen"));
-    fullScreenAllMonitors.setSelected(UserPreferences.getBool("global",
-            "FullScreenAllMonitors"));
-    if (shared.isEnabled())
-      shared.setSelected(UserPreferences.getBool("global", "Shared"));
-    useLocalCursor.setSelected(UserPreferences.getBool("global",
-            "UseLocalCursor"));
-    acceptBell.setSelected(UserPreferences.getBool("global", "AcceptBell"));
-    String scaleString = UserPreferences.get("global", "ScalingFactor");
-    if (scaleString != null) {
-      if (scaleString.equalsIgnoreCase("Auto")) {
-        scalingFactor.setSelectedItem("Auto");
-      } else if (scaleString.equalsIgnoreCase("FixedRatio")) {
-        scalingFactor.setSelectedItem("Fixed Aspect Ratio");
-      } else {
-        scalingFactor.setSelectedItem(scaleString+"%");
-      }
-    }
-    if (secVeNCrypt.isEnabled()) {
-      secVeNCrypt.setSelected(UserPreferences.getBool("viewer",
-              "secVeNCrypt", true));
-      if (secVeNCrypt.isSelected()) {
-        encNone.setSelected(UserPreferences.getBool("viewer", "encNone", true));
-        encTLS.setSelected(UserPreferences.getBool("viewer", "encTLS", true));
-        encX509.setSelected(UserPreferences.getBool("viewer", "encX509", true));
-        secPlain.setSelected(UserPreferences.getBool("viewer", "secPlain", true));
-        secIdent.setSelected(UserPreferences.getBool("viewer", "secIdent", true));
-        sendLocalUsername.setSelected(UserPreferences.getBool("global",
-                "SendLocalUsername"));
-      }
-    }
-    if (secNone.isEnabled())
-      secNone.setSelected(UserPreferences.getBool("viewer", "secNone", true));
-    if (secVnc.isEnabled())
-      secVnc.setSelected(UserPreferences.getBool("viewer", "secVnc", true));
-    sshTunnel.setSelected(UserPreferences.getBool("global", "Tunnel"));
-    sshUseGateway.setSelected(UserPreferences.get("global", "Via") != null);
-    if (sshUseGateway.isSelected())
-      cc.viewer.via.setParam(UserPreferences.get("global", "Via"));
-    sshUser.setText(Tunnel.getSshUser(cc));
-    sshHost.setText(Tunnel.getSshHost(cc));
-    sshPort.setText(Integer.toString(Tunnel.getSshPort(cc)));
-    sshUseExt.setSelected(UserPreferences.getBool("global", "extSSH"));
-    File f = new File(UserPreferences.get("global", "extSSHClient"));
-    if (f.exists() && f.canExecute())
-      sshClient.setText(f.getAbsolutePath());
-    sshArguments.setText(UserPreferences.get("global", "extSSHArgs"));
-    if (sshArguments.getText().isEmpty())
-      sshArgsDefault.setSelected(true);
+  private void handleJpeg()
+  {
+    if (jpegCheckbox.isSelected() &&
+        !autoselectCheckbox.isSelected())
+      jpegInput.setEnabled(true);
     else
-      sshArgsCustom.setSelected(true);
-    f = new File(UserPreferences.get("global", "SSHConfig"));
-    if (f.exists() && f.canRead())
-      sshConfig.setText(f.getAbsolutePath());
-    if (UserPreferences.get("global", "SSHKeyFile") != null) {
-      f = new File(UserPreferences.get("global", "SSHKeyFile"));
-      if (f.exists() && f.canRead())
-        sshKeyFile.setText(f.getAbsolutePath());
+      jpegInput.setEnabled(false);
+  }
+
+  private void handleX509()
+  {
+    caInput.setEnabled(encX509Checkbox.isSelected());
+    caChooser.setEnabled(encX509Checkbox.isSelected());
+    crlInput.setEnabled(encX509Checkbox.isSelected());
+    crlChooser.setEnabled(encX509Checkbox.isSelected());
+  }
+
+  private void handleSendLocalUsername()
+  {
+    boolean value = authIdentCheckbox.isSelected() ||
+                    authPlainCheckbox.isSelected();
+        sendLocalUsernameCheckbox.setEnabled(value);
+  }
+
+  private void handleDesktopSize()
+  {
+    desktopWidthInput.setEnabled(desktopSizeCheckbox.isSelected());
+    desktopHeightInput.setEnabled(desktopSizeCheckbox.isSelected());
+  }
+
+  private void handleRemoteResize()
+  {
+    scalingFactorInput.setEnabled(!remoteResizeButton.isSelected());
+  }
+
+  private void handleTunnel()
+  {
+    viaCheckbox.setEnabled(tunnelCheckbox.isSelected());
+    extSSHCheckbox.setEnabled(tunnelCheckbox.isSelected());
+    if (tunnelCheckbox.isSelected()) {
+      JComponent[] components = { viaUserInput, viaHostInput, viaPortInput };
+      for (JComponent c : components)
+        c.setEnabled(viaCheckbox.isSelected());
+      sshClientInput.setEnabled(extSSHCheckbox.isSelected());
+      sshClientChooser.setEnabled(extSSHCheckbox.isSelected());
+      sshArgsDefaultButton.setEnabled(extSSHCheckbox.isSelected());
+      sshArgsCustomButton.setEnabled(extSSHCheckbox.isSelected());
+      sshArgsInput.setEnabled(extSSHCheckbox.isSelected());
+      sshConfigInput.setEnabled(!extSSHCheckbox.isSelected());
+      sshConfigChooser.setEnabled(!extSSHCheckbox.isSelected());
+      sshKeyFileInput.setEnabled(!extSSHCheckbox.isSelected());
+      sshKeyFileChooser.setEnabled(!extSSHCheckbox.isSelected());
     } else {
-      sshKeyFile.setText(Tunnel.getSshKeyFile(cc));
-    }
-    sshUseGateway.setEnabled(sshTunnel.isSelected());
-    sshUser.setEnabled(sshTunnel.isSelected() &&
-                       sshUseGateway.isEnabled() &&
-                       sshUseGateway.isSelected());
-    sshHost.setEnabled(sshTunnel.isSelected() &&
-                       sshUseGateway.isEnabled() &&
-                       sshUseGateway.isSelected());
-    sshPort.setEnabled(sshTunnel.isSelected() &&
-                       sshUseGateway.isEnabled() &&
-                       sshUseGateway.isSelected());
-    sshUseExt.setEnabled(sshTunnel.isSelected());
-    sshClient.setEnabled(sshTunnel.isSelected() &&
-                         sshUseExt.isEnabled());
-    sshClientBrowser.setEnabled(sshTunnel.isSelected() &&
-                                sshUseExt.isEnabled() &&
-                                sshUseExt.isSelected());
-    sshArgsDefault.setEnabled(sshTunnel.isSelected() &&
-                              sshUseExt.isEnabled() &&
-                              sshUseExt.isSelected());
-    sshArgsCustom.setEnabled(sshTunnel.isSelected() &&
-                             sshUseExt.isEnabled() &&
-                             sshUseExt.isSelected());
-    sshArguments.setEnabled(sshTunnel.isSelected() &&
-                            sshUseExt.isEnabled() &&
-                            sshUseExt.isSelected() &&
-                            sshArgsCustom.isSelected());
-    sshConfig.setEnabled(sshTunnel.isSelected() &&
-                         sshUseExt.isEnabled() &&
-                         !sshUseExt.isSelected());
-    sshConfigBrowser.setEnabled(sshTunnel.isSelected() &&
-                                sshUseExt.isEnabled() &&
-                                !sshUseExt.isSelected());
-    sshKeyFile.setEnabled(sshTunnel.isSelected() &&
-                          sshUseExt.isEnabled() &&
-                          !sshUseExt.isSelected());
-    sshKeyFileBrowser.setEnabled(sshTunnel.isSelected() &&
-                                 sshUseExt.isEnabled() &&
-                                 !sshUseExt.isSelected());
-  }
-
-  public void endDialog() {
-    super.endDialog();
-    if (cc.viewport != null && cc.viewport.isVisible()) {
-      cc.viewport.toFront();
-      cc.viewport.requestFocus();
+      JComponent[] components = {
+        viaUserInput, viaHostInput, viaPortInput, sshClientInput,
+        sshClientChooser, sshArgsDefaultButton, sshArgsCustomButton,
+        sshArgsInput, sshConfigInput, sshConfigChooser, sshKeyFileInput,
+        sshKeyFileChooser, };
+      for (JComponent c : components)
+        c.setEnabled(false);
     }
   }
 
-  public void actionPerformed(ActionEvent e) {
-    Object s = e.getSource();
-    if (s instanceof JButton) {
-      JButton button = (JButton)s;
-      if (button == okButton) {
-        JTextField[] fields =
-          { x509ca, x509crl, sshClient, sshConfig, sshKeyFile };
-        for (JTextField field : fields) {
-          if (field.getText() != null && !field.getText().equals("")) {
-            File f = new File(field.getText());
-            if (!f.exists() || !f.canRead()) {
-              String msg = new String("The file "+f.getAbsolutePath()+
-                           " specified for option "+field.getName()+
-                           " does not exist or cannot be read.  Please"+
-                           " correct before proceeding.");
-              JOptionPane.showMessageDialog(this, msg, "WARNING",
-                                            JOptionPane.WARNING_MESSAGE);
-              return;
-            }
-          }
-        }
-        if (cc != null) cc.getOptions();
-        endDialog();
-      } else if (button == cancelButton) {
-        endDialog();
-      } else if (button == cfLoadButton) {
-        JFileChooser fc = new JFileChooser();
-        fc.setDialogTitle("Path to configuration file");
-        fc.setApproveButtonText("OK");
-        fc.setFileHidingEnabled(false);
-        int ret = fc.showOpenDialog(this);
-        if (ret == JFileChooser.APPROVE_OPTION) {
-          String filename = fc.getSelectedFile().toString();
-          if (filename != null)
-            Configuration.load(filename);
-          cc.setOptions();
-        }
-      } else if (button == cfSaveAsButton) {
-        JFileChooser fc = new JFileChooser();
-        fc.setDialogTitle("Save current configuration as:");
-        fc.setApproveButtonText("OK");
-        fc.setFileHidingEnabled(false);
-        int ret = fc.showOpenDialog(this);
-        if (ret == JFileChooser.APPROVE_OPTION) {
-          String filename = fc.getSelectedFile().toString();
-          if (filename != null)
-            Configuration.save(filename);
-        }
-      } else if (button == defSaveButton) {
-        updatePreferences();
-        UserPreferences.save();
-      } else if (button == defReloadButton) {
-        restorePreferences();
-      } else if (button == defClearButton) {
-        UserPreferences.clear();
-        cc.setOptions();
-      } else if (button == caButton) {
-        JFileChooser fc =
-          new JFileChooser(new File(CSecurityTLS.getDefaultCA()));
-        fc.setDialogTitle("Path to X509 CA certificate");
-        fc.setApproveButtonText("OK");
-        fc.setFileHidingEnabled(false);
-        int ret = fc.showOpenDialog(this);
-        if (ret == JFileChooser.APPROVE_OPTION)
-          x509ca.setText(fc.getSelectedFile().toString());
-      } else if (button == crlButton) {
-        JFileChooser fc =
-          new JFileChooser(new File(CSecurityTLS.getDefaultCRL()));
-        fc.setDialogTitle("Path to X509 CRL file");
-        fc.setApproveButtonText("OK");
-        fc.setFileHidingEnabled(false);
-        int ret = fc.showOpenDialog(this);
-        if (ret == JFileChooser.APPROVE_OPTION)
-          x509crl.setText(fc.getSelectedFile().toString());
-      } else if (button == sshClientBrowser) {
-        JFileChooser fc = new JFileChooser();
-        fc.setDialogTitle("Path to external SSH client");
-        fc.setApproveButtonText("OK");
-        fc.setFileHidingEnabled(false);
-        int ret = fc.showOpenDialog(this);
-        if (ret == JFileChooser.APPROVE_OPTION)
-          sshClient.setText(fc.getSelectedFile().toString());
-      } else if (button == sshConfigBrowser) {
-        JFileChooser fc = new JFileChooser();
-        fc.setDialogTitle("Path to OpenSSH client config file");
-        fc.setApproveButtonText("OK");
-        fc.setFileHidingEnabled(false);
-        int ret = fc.showOpenDialog(this);
-        if (ret == JFileChooser.APPROVE_OPTION)
-          sshConfig.setText(fc.getSelectedFile().toString());
-      } else if (button == sshKeyFileBrowser) {
-        JFileChooser fc = new JFileChooser();
-        fc.setDialogTitle("Path to SSH key file");
-        fc.setApproveButtonText("OK");
-        fc.setFileHidingEnabled(false);
-        int ret = fc.showOpenDialog(this);
-        if (ret == JFileChooser.APPROVE_OPTION)
-          sshKeyFile.setText(fc.getSelectedFile().toString());
-      }
-    } else if (s instanceof JRadioButton) {
-      JRadioButton button = (JRadioButton)s;
-      if (button == sshArgsCustom || button == sshArgsDefault) {
-        sshArguments.setEnabled(sshArgsCustom.isSelected());
-      }
+  private void handleVia()
+  {
+    if (tunnelCheckbox.isSelected()) {
+      viaUserInput.setEnabled(viaCheckbox.isSelected());
+      viaHostInput.setEnabled(viaCheckbox.isSelected());
+      viaPortInput.setEnabled(viaCheckbox.isSelected());
     }
   }
 
-  public void itemStateChanged(ItemEvent e) {
-    Object s = e.getSource();
-    if (s instanceof JCheckBox) {
-      JCheckBox item = (JCheckBox)s;
-      boolean enable = item.isSelected();
-      if (item == autoSelect) {
-        ButtonGroup[] groups = { encodingGroup, colourGroup };
-        for (ButtonGroup grp : groups) {
-          Enumeration<AbstractButton> elems = grp.getElements();
-          while (elems.hasMoreElements())
-            elems.nextElement().setEnabled(!enable);
-        }
-      } else if (item == customCompressLevel) {
-        compressLevel.setEnabled(enable);
-      } else if (item == desktopSize) {
-        desktopWidth.setEnabled(enable);
-        desktopHeight.setEnabled(enable);
-      } else if (item == noJpeg) {
-        qualityLevel.setEnabled(enable);
-      } else if (item == encX509) {
-        x509ca.setEnabled(enable);
-        caButton.setEnabled(enable);
-        x509crl.setEnabled(enable);
-        crlButton.setEnabled(enable);
-      } else if (item == secVeNCrypt) {
-        encNone.setEnabled(enable);
-        encTLS.setEnabled(enable);
-        encX509.setEnabled(enable);
-        x509ca.setEnabled(enable && encX509.isSelected());
-        caButton.setEnabled(enable && encX509.isSelected());
-        x509crl.setEnabled(enable && encX509.isSelected());
-        crlButton.setEnabled(enable && encX509.isSelected());
-        secIdent.setEnabled(enable);
-        secPlain.setEnabled(enable);
-        sendLocalUsername.setEnabled(enable);
-      } else if (item == encNone) {
-        secNone.setSelected(enable &&
-          UserPreferences.getBool("viewer", "secNone", true));
-        secVnc.setSelected(enable &&
-          UserPreferences.getBool("viewer", "secVnc", true));
-      } else if (item == secIdent || item == secPlain) {
-        sendLocalUsername.setEnabled(secIdent.isSelected() ||
-                                     secPlain.isSelected());
-      } else if (item == sshTunnel) {
-        sshUseGateway.setEnabled(enable);
-        sshUser.setEnabled(enable &&
-                           sshUseGateway.isEnabled() &&
-                           sshUseGateway.isSelected());
-        sshHost.setEnabled(enable &&
-                           sshUseGateway.isEnabled() &&
-                           sshUseGateway.isSelected());
-        sshPort.setEnabled(enable &&
-                           sshUseGateway.isEnabled() &&
-                           sshUseGateway.isSelected());
-        sshUseExt.setEnabled(enable);
-        sshClient.setEnabled(enable &&
-                             sshUseExt.isEnabled() &&
-                             sshUseExt.isSelected());
-        sshClientBrowser.setEnabled(enable &&
-                                    sshUseExt.isEnabled() &&
-                                    sshUseExt.isSelected());
-        sshArgsDefault.setEnabled(enable &&
-                                  sshUseExt.isEnabled() &&
-                                  sshUseExt.isSelected());
-        sshArgsCustom.setEnabled(enable &&
-                                 sshUseExt.isEnabled() &&
-                                 sshUseExt.isSelected());
-        sshArguments.setEnabled(enable &&
-                                sshUseExt.isEnabled() &&
-                                sshUseExt.isSelected() &&
-                                sshArgsCustom.isSelected());
-        sshConfig.setEnabled(enable &&
-                             sshUseExt.isEnabled() &&
-                             !sshUseExt.isSelected());
-        sshConfigBrowser.setEnabled(enable &&
-                                    sshUseExt.isEnabled() &&
-                                    !sshUseExt.isSelected());
-        sshKeyFile.setEnabled(enable &&
-                              sshUseExt.isEnabled() &&
-                              !sshUseExt.isSelected());
-        sshKeyFileBrowser.setEnabled(enable &&
-                                     sshUseExt.isEnabled() &&
-                                     !sshUseExt.isSelected());
-      } else if (item == sshUseExt) {
-        sshClient.setEnabled(enable);
-        sshClientBrowser.setEnabled(enable);
-        sshArgsDefault.setEnabled(enable);
-        sshArgsCustom.setEnabled(enable);
-        sshArguments.setEnabled(enable && sshArgsCustom.isSelected());
-        sshConfig.setEnabled(!enable);
-        sshConfigBrowser.setEnabled(!enable);
-        sshKeyFile.setEnabled(!enable);
-        sshKeyFileBrowser.setEnabled(!enable);
-      } else if (item == sshUseGateway) {
-        sshUser.setEnabled(enable);
-        sshHost.setEnabled(enable);
-        sshPort.setEnabled(enable);
-      }
+  private void handleExtSSH()
+  {
+    if (tunnelCheckbox.isSelected()) {
+      sshClientInput.setEnabled(extSSHCheckbox.isSelected());
+      sshClientChooser.setEnabled(extSSHCheckbox.isSelected());
+      sshArgsDefaultButton.setEnabled(extSSHCheckbox.isSelected());
+      sshArgsCustomButton.setEnabled(extSSHCheckbox.isSelected());
+      sshConfigInput.setEnabled(!extSSHCheckbox.isSelected());
+      sshConfigChooser.setEnabled(!extSSHCheckbox.isSelected());
+      sshKeyFileInput.setEnabled(!extSSHCheckbox.isSelected());
+      sshKeyFileChooser.setEnabled(!extSSHCheckbox.isSelected());
+      if (sshArgsCustomButton.isSelected())
+        sshArgsInput.setEnabled(extSSHCheckbox.isSelected());
+      else
+        sshArgsInput.setEnabled(false);
     }
   }
+
+  private void handleEmbed()
+  {
+    if (embed.getValue()) {
+      desktopSizeCheckbox.setEnabled(false);
+      desktopWidthInput.setEnabled(false);
+      desktopHeightInput.setEnabled(false);
+      remoteResizeButton.setEnabled(false);
+      remoteScaleButton.setEnabled(false);
+      fullScreenCheckbox.setEnabled(false);
+      fullScreenAllMonitorsCheckbox.setEnabled(false);
+      scalingFactorInput.setEnabled(false);
+    }
+  }
+
+  private void handleRfbState()
+  {
+    CConn cc = VncViewer.cc;
+    if (cc != null && cc.state() == CConnection.RFBSTATE_NORMAL) {
+      JComponent[] components = {
+          encNoneCheckbox, encTLSCheckbox, encX509Checkbox, authNoneCheckbox,
+          authVncCheckbox, authVncCheckbox, authIdentCheckbox, authPlainCheckbox,
+          sendLocalUsernameCheckbox, caInput, caChooser, crlInput, crlChooser,
+          sharedCheckbox, tunnelCheckbox, viaCheckbox, viaUserInput, viaHostInput,
+          viaPortInput, extSSHCheckbox, sshClientInput, sshClientChooser,
+          sshArgsDefaultButton, sshArgsCustomButton, sshArgsInput, sshConfigInput,
+          sshKeyFileInput, sshConfigChooser, sshKeyFileChooser,
+        };
+      for (JComponent c : components)
+        c.setEnabled(false);
+    }
+  }
+
+  static LogWriter vlog = new LogWriter("OptionsDialog");
 }
diff --git a/java/com/tigervnc/vncviewer/OptionsDialogCallback.java b/java/com/tigervnc/vncviewer/OptionsDialogCallback.java
deleted file mode 100644
index efb8c06..0000000
--- a/java/com/tigervnc/vncviewer/OptionsDialogCallback.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- *
- * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
- * USA.
- */
-
-package com.tigervnc.vncviewer;
-
-public interface OptionsDialogCallback {
-  public void setOptions();
-  public void getOptions();
-}
diff --git a/java/com/tigervnc/vncviewer/Parameters.java b/java/com/tigervnc/vncviewer/Parameters.java
new file mode 100644
index 0000000..50e26cb
--- /dev/null
+++ b/java/com/tigervnc/vncviewer/Parameters.java
@@ -0,0 +1,646 @@
+/* Copyright (C) 2016 Brian P. Hinz
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ * USA.
+ */
+
+package com.tigervnc.vncviewer;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.PrintWriter;
+import java.util.StringTokenizer;
+
+import com.tigervnc.rfb.*;
+import com.tigervnc.rfb.Exception;
+
+public class Parameters {
+
+  public static BoolParameter noLionFS
+  = new BoolParameter("NoLionFS",
+    "On Mac systems, setting this parameter will force the use of the old "+
+    "(pre-Lion) full-screen mode, even if the viewer is running on OS X 10.7 "+
+    "Lion or later.",
+    false);
+
+  public static BoolParameter embed
+  = new BoolParameter("Embed",
+    "If the viewer is being run as an applet, display its output to " +
+    "an embedded frame in the browser window rather than to a dedicated " +
+    "window. Embed=1 implies FullScreen=0 and Scale=100.",
+    false);
+
+  public static BoolParameter dotWhenNoCursor
+  = new BoolParameter("DotWhenNoCursor",
+    "Show the dot cursor when the server sends an invisible cursor",
+    false);
+
+  public static BoolParameter sendLocalUsername
+  = new BoolParameter("SendLocalUsername",
+    "Send the local username for SecurityTypes "+
+    "such as Plain rather than prompting",
+    true);
+
+  public static StringParameter passwordFile
+  = new StringParameter("PasswordFile",
+    "Password file for VNC authentication",
+    "");
+
+  public static AliasParameter passwd
+  = new AliasParameter("passwd",
+    "Alias for PasswordFile",
+    passwordFile);
+
+  public static BoolParameter autoSelect
+  = new BoolParameter("AutoSelect",
+    "Auto select pixel format and encoding",
+    true);
+
+  public static BoolParameter fullColor
+  = new BoolParameter("FullColor",
+    "Use full color - otherwise 6-bit colour is used "+
+    "until AutoSelect decides the link is fast enough",
+    true);
+
+  public static AliasParameter fullColorAlias
+  = new AliasParameter("FullColour",
+    "Alias for FullColor",
+    Parameters.fullColor);
+
+  public static IntParameter lowColorLevel
+  = new IntParameter("LowColorLevel",
+    "Color level to use on slow connections. "+
+    "0 = Very Low (8 colors), 1 = Low (64 colors), "+
+    "2 = Medium (256 colors)",
+    2);
+
+  public static AliasParameter lowColorLevelAlias
+  = new AliasParameter("LowColourLevel",
+    "Alias for LowColorLevel",
+    lowColorLevel);
+
+  public static StringParameter preferredEncoding
+  = new StringParameter("PreferredEncoding",
+    "Preferred encoding to use (Tight, ZRLE, "+
+    "hextile or raw) - implies AutoSelect=0",
+    "Tight");
+
+  public static BoolParameter remoteResize
+  = new BoolParameter("RemoteResize",
+    "Dynamically resize the remote desktop size as "+
+    "the size of the local client window changes. "+
+    "(Does not work with all servers)",
+    true);
+
+  public static BoolParameter viewOnly
+  = new BoolParameter("ViewOnly",
+    "Don't send any mouse or keyboard events to the server",
+    false);
+
+  public static BoolParameter shared
+  = new BoolParameter("Shared",
+    "Don't disconnect other viewers upon "+
+    "connection - share the desktop instead",
+    false);
+
+  public static BoolParameter maximize
+  = new BoolParameter("Maximize",
+    "Maximize viewer window",
+    false);
+
+  public static BoolParameter fullScreen
+  = new BoolParameter("FullScreen",
+    "Full Screen Mode",
+    false);
+
+  public static BoolParameter fullScreenAllMonitors
+  = new BoolParameter("FullScreenAllMonitors",
+    "Enable full screen over all monitors",
+    true);
+
+  public static BoolParameter acceptClipboard
+  = new BoolParameter("AcceptClipboard",
+    "Accept clipboard changes from the server",
+    true);
+
+  public static BoolParameter sendClipboard
+  = new BoolParameter("SendClipboard",
+    "Send clipboard changes to the server",
+    true);
+
+  public static IntParameter maxCutText
+  = new IntParameter("MaxCutText",
+    "Maximum permitted length of an outgoing clipboard update",
+    262144);
+
+  public static StringParameter menuKey
+  = new StringParameter("MenuKey",
+    "The key which brings up the popup menu",
+    "F8");
+
+  public static StringParameter desktopSize
+  = new StringParameter("DesktopSize",
+    "Reconfigure desktop size on the server on connect (if possible)",
+    "");
+
+  public static BoolParameter listenMode
+  = new BoolParameter("listen",
+    "Listen for connections from VNC servers",
+    false);
+
+  public static StringParameter scalingFactor
+  = new StringParameter("ScalingFactor",
+    "Reduce or enlarge the remote desktop image. "+
+    "The value is interpreted as a scaling factor "+
+    "in percent. If the parameter is set to "+
+    "\"Auto\", then automatic scaling is "+
+    "performed. Auto-scaling tries to choose a "+
+    "scaling factor in such a way that the whole "+
+    "remote desktop will fit on the local screen. "+
+    "If the parameter is set to \"FixedRatio\", "+
+    "then automatic scaling is performed, but the "+
+    "original aspect ratio is preserved.",
+    "100");
+
+  public static BoolParameter alwaysShowServerDialog
+  = new BoolParameter("AlwaysShowServerDialog",
+    "Always show the server dialog even if a server has been "+
+    "specified in an applet parameter or on the command line",
+    false);
+
+  public static StringParameter vncServerName
+  = new StringParameter("Server",
+    "The VNC server <host>[:<dpyNum>] or <host>::<port>",
+    "");
+
+  public static BoolParameter acceptBell
+  = new BoolParameter("AcceptBell",
+    "Produce a system beep when requested to by the server.",
+    true);
+
+  public static StringParameter via
+  = new StringParameter("Via",
+    "Automatically create an encrypted TCP tunnel to "+
+    "the gateway machine, then connect to the VNC host "+
+    "through that tunnel. By default, this option invokes "+
+    "SSH local port forwarding using the embedded JSch "+
+    "client, however an external SSH client may be specified "+
+    "using the \"-extSSH\" parameter. Note that when using "+
+    "the -via option, the VNC host machine name should be "+
+    "specified from the point of view of the gateway machine, "+
+    "e.g. \"localhost\" denotes the gateway, "+
+    "not the machine on which the viewer was launched. "+
+    "See the System Properties section below for "+
+    "information on configuring the -Via option.", "");
+
+  public static BoolParameter tunnel
+  = new BoolParameter("Tunnel",
+    "The -Tunnel command is basically a shorthand for the "+
+    "-via command when the VNC server and SSH gateway are "+
+    "one and the same. -Tunnel creates an SSH connection "+
+    "to the server and forwards the VNC through the tunnel "+
+    "without the need to specify anything else.", false);
+
+  public static BoolParameter extSSH
+  = new BoolParameter("extSSH",
+    "By default, SSH tunneling uses the embedded JSch client "+
+    "for tunnel creation. This option causes the client to "+
+    "invoke an external SSH client application for all tunneling "+
+    "operations. By default, \"/usr/bin/ssh\" is used, however "+
+    "the path to the external application may be specified using "+
+    "the -SSHClient option.", false);
+
+  public static StringParameter extSSHClient
+  = new StringParameter("extSSHClient",
+    "Specifies the path to an external SSH client application "+
+    "that is to be used for tunneling operations when the -extSSH "+
+    "option is in effect.", "/usr/bin/ssh");
+
+  public static StringParameter extSSHArgs
+  = new StringParameter("extSSHArgs",
+    "Specifies the arguments string or command template to be used "+
+    "by the external SSH client application when the -extSSH option "+
+    "is in effect. The string will be processed according to the same "+
+    "pattern substitution rules as the VNC_TUNNEL_CMD and VNC_VIA_CMD "+
+    "system properties, and can be used to override those in a more "+
+    "command-line friendly way. If not specified, then the appropriate "+
+    "VNC_TUNNEL_CMD or VNC_VIA_CMD command template will be used.", "");
+
+  public static StringParameter sshConfig
+  = new StringParameter("SSHConfig",
+    "Specifies the path to an OpenSSH configuration file that to "+
+    "be parsed by the embedded JSch SSH client during tunneling "+
+    "operations.", FileUtils.getHomeDir()+".ssh/config");
+
+  public static StringParameter sshKey
+  = new StringParameter("SSHKey",
+    "When using the Via or Tunnel options with the embedded SSH client, "+
+    "this parameter specifies the text of the SSH private key to use when "+
+    "authenticating with the SSH server. You can use \\n within the string "+
+    "to specify a new line.", "");
+
+  public static StringParameter sshKeyFile
+  = new StringParameter("SSHKeyFile",
+    "When using the Via or Tunnel options with the embedded SSH client, "+
+    "this parameter specifies a file that contains an SSH private key "+
+    "(or keys) to use when authenticating with the SSH server. If not "+
+    "specified, ~/.ssh/id_dsa or ~/.ssh/id_rsa will be used (if they exist). "+
+    "Otherwise, the client will fallback to prompting for an SSH password.",
+    "");
+
+  public static StringParameter sshKeyPass
+  = new StringParameter("SSHKeyPass",
+    "When using the Via or Tunnel options with the embedded SSH client, "+
+    "this parameter specifies the passphrase for the SSH key.", "");
+
+  public static BoolParameter customCompressLevel
+  = new BoolParameter("CustomCompressLevel",
+    "Use custom compression level. Default if CompressLevel is specified.",
+    false);
+
+  public static IntParameter compressLevel
+  = new IntParameter("CompressLevel",
+    "Use specified compression level. 0 = Low, 6 = High",
+    1);
+
+  public static BoolParameter noJpeg
+  = new BoolParameter("NoJPEG",
+    "Disable lossy JPEG compression in Tight encoding.",
+    false);
+
+  public static IntParameter qualityLevel
+  = new IntParameter("QualityLevel",
+    "JPEG quality level. 0 = Low, 9 = High",
+    8);
+
+  private static final String IDENTIFIER_STRING
+  = "TigerVNC Configuration file Version 1.0";
+
+  static VoidParameter[] parameterArray = {
+    CSecurityTLS.X509CA,
+    CSecurityTLS.X509CRL,
+    SecurityClient.secTypes,
+    autoSelect,
+    fullColor,
+    lowColorLevel,
+    preferredEncoding,
+    customCompressLevel,
+    compressLevel,
+    noJpeg,
+    qualityLevel,
+    maximize,
+    fullScreen,
+    fullScreenAllMonitors,
+    desktopSize,
+    remoteResize,
+    viewOnly,
+    shared,
+    acceptClipboard,
+    sendClipboard,
+    menuKey,
+    noLionFS,
+    sendLocalUsername,
+    maxCutText,
+    scalingFactor,
+    acceptBell,
+    via,
+    tunnel,
+    extSSH,
+    extSSHClient,
+    extSSHArgs,
+    sshConfig,
+    sshKeyFile,
+  };
+
+
+  static LogWriter vlog = new LogWriter("Parameters");
+
+	public static void saveViewerParameters(String filename, String servername) {
+
+	  // Write to the registry or a predefined file if no filename was specified.
+    String filepath;
+    if (filename == null || filename.isEmpty()) {
+      saveToReg(servername);
+      return;
+      /*
+      String homeDir = FileUtils.getVncHomeDir();
+      if (homeDir == null)
+        throw new Exception("Failed to read configuration file, "+
+                            "can't obtain home directory path.");
+      filepath = homeDir.concat("default.tigervnc");
+      */
+    } else {
+      filepath = filename;
+    }
+
+	  /* Write parameters to file */
+    File f = new File(filepath);
+    if (f.exists() && !f.canWrite())
+	    throw new Exception(String.format("Failed to write configuration file,"+
+                                        "can't open %s", filepath)); 
+
+    PrintWriter pw = null;
+    try {
+      pw = new PrintWriter(f, "UTF-8");
+    } catch (java.lang.Exception e)	{
+      throw new Exception(e.getMessage());
+    }
+
+    pw.println(IDENTIFIER_STRING);
+    pw.println("");
+
+	  if (servername != null && !servername.isEmpty()) {
+	    pw.println(String.format("ServerName=%s\n", servername));
+      updateConnHistory(servername);
+    }
+
+    for (int i = 0; i < parameterArray.length; i++) {
+      if (parameterArray[i] instanceof StringParameter) {
+        //if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName()))
+          pw.println(String.format("%s=%s",parameterArray[i].getName(),
+                                   parameterArray[i].getValueStr()));
+      } else if (parameterArray[i] instanceof IntParameter) {
+        //if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName()))
+          pw.println(String.format("%s=%s",parameterArray[i].getName(),
+                                   parameterArray[i].getValueStr()));
+      } else if (parameterArray[i] instanceof BoolParameter) {
+        //if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName()))
+          pw.println(String.format("%s=%s",parameterArray[i].getName(),
+                                   parameterArray[i].getValueStr()));
+      } else {
+        vlog.error(String.format("Unknown parameter type for parameter %s",
+                   parameterArray[i].getName()));
+      }
+    }
+
+    pw.flush();
+    pw.close();
+	}
+
+  public static String loadViewerParameters(String filename) throws Exception {
+    String servername = "";
+
+    String filepath;
+    if (filename == null) {
+
+      return loadFromReg(); 
+
+      /*
+      String homeDir = FileUtils.getVncHomeDir();
+      if (homeDir == null)
+        throw new Exception("Failed to read configuration file, "+
+                            "can't obtain home directory path.");
+      filepath = homeDir.concat("default.tigervnc");
+      */
+    } else {
+      filepath = filename;
+    }
+
+    /* Read parameters from file */
+    File f = new File(filepath);
+    if (!f.exists() || !f.canRead()) {
+      if (filename == null || filename.isEmpty())
+        return null;
+      throw new Exception(String.format("Failed to read configuration file, can't open %s",
+                          filepath)); 
+    }
+
+    String line = "";
+    LineNumberReader reader;
+    try {
+      reader = new LineNumberReader(new FileReader(f));
+    } catch (FileNotFoundException e) {
+      throw new Exception(e.getMessage());
+    }
+
+    int lineNr = 0;
+    while (line != null) {
+
+      // Read the next line
+      try {
+        line = reader.readLine();
+        lineNr = reader.getLineNumber();
+        if (line == null)
+          break;
+      } catch (IOException e) {
+        throw new Exception(String.format("Failed to read line %d in file %s: %s",
+          lineNr, filepath, e.getMessage()));
+      }
+
+      // Make sure that the first line of the file has the file identifier string
+      if(lineNr == 1) {
+        if(line.equals(IDENTIFIER_STRING))
+          continue;
+        else
+          throw new Exception(String.format("Configuration file %s is in an invalid format", filename));
+      }
+
+      // Skip empty lines and comments
+      if (line.trim().isEmpty() || line.trim().startsWith("#"))
+        continue;
+
+      // Find the parameter value
+      int idx = line.indexOf("=");
+      if (idx == -1) {
+        vlog.error(String.format("Failed to read line %d in file %s: %s",
+                   lineNr, filename, "Invalid format"));
+        continue;
+      }
+      String value = line.substring(idx+1).trim();
+      boolean invalidParameterName = true; // Will be set to false below if 
+                                           // the line contains a valid name.
+
+      if (line.substring(0,idx).trim().equalsIgnoreCase("ServerName")) {
+        if (value.length() > 256) {
+          vlog.error(String.format("Failed to read line %d in file %s: %s",
+                     lineNr, filepath, "Invalid format or too large value"));
+          continue;
+        }
+        servername = value;
+        invalidParameterName = false;
+      } else {
+        for (int i = 0; i < parameterArray.length; i++) {
+          if (parameterArray[i] instanceof StringParameter) {
+            if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName())) {
+              if (value.length() > 256) {
+                vlog.error(String.format("Failed to read line %d in file %s: %s",
+                           lineNr, filepath, "Invalid format or too large value"));
+                continue;
+              }
+              ((StringParameter)parameterArray[i]).setParam(value);
+              invalidParameterName = false;
+            }
+          } else if (parameterArray[i] instanceof IntParameter) {
+            if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName())) {
+              ((IntParameter)parameterArray[i]).setParam(value);
+              invalidParameterName = false;
+            }
+          } else if (parameterArray[i] instanceof BoolParameter) {
+            if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName())) {
+              ((BoolParameter)parameterArray[i]).setParam(value);
+              invalidParameterName = false;
+            }
+          } else {
+            vlog.error(String.format("Unknown parameter type for parameter %s",
+                       parameterArray[i].getName()));
+
+          }
+        }
+      }
+
+      if (invalidParameterName)
+        vlog.info(String.format("Unknown parameter %s on line %d in file %s",
+                  line, lineNr, filepath));
+    }
+    try {
+      reader.close();
+    } catch (IOException e) {
+      vlog.info(e.getMessage());
+    } finally {
+      try {
+        if (reader != null)
+          reader.close();
+        } catch (IOException e) { }
+    }
+
+    return servername;
+  }
+
+  public static void saveToReg(String servername) {
+    String hKey = "global";
+
+    if (servername != null && !servername.isEmpty()) {
+      UserPreferences.set(hKey, "ServerName", servername);
+      updateConnHistory(servername);
+    }
+
+    for (int i = 0; i < parameterArray.length; i++) {
+      if (parameterArray[i] instanceof StringParameter) {
+        UserPreferences.set(hKey, parameterArray[i].getName(),
+                                  parameterArray[i].getValueStr());
+      } else if (parameterArray[i] instanceof IntParameter) {
+        UserPreferences.set(hKey, parameterArray[i].getName(),
+                            ((IntParameter)parameterArray[i]).getValue());
+      } else if (parameterArray[i] instanceof BoolParameter) {
+        UserPreferences.set(hKey, parameterArray[i].getName(),
+                            ((BoolParameter)parameterArray[i]).getValue());
+      } else {
+        vlog.error(String.format("Unknown parameter type for parameter %s",
+                   parameterArray[i].getName()));
+      }
+    }
+
+    UserPreferences.save(hKey);
+  }
+
+  public static String loadFromReg() {
+
+    String hKey = "global";
+
+    String servername = UserPreferences.get(hKey, "ServerName");
+    if (servername == null)
+      servername = "";
+
+    for (int i = 0; i < parameterArray.length; i++) {
+      if (parameterArray[i] instanceof StringParameter) {
+        if (UserPreferences.get(hKey, parameterArray[i].getName()) != null) {
+          String stringValue =
+            UserPreferences.get(hKey, parameterArray[i].getName());
+          ((StringParameter)parameterArray[i]).setParam(stringValue);
+        }
+      } else if (parameterArray[i] instanceof IntParameter) {
+        if (UserPreferences.get(hKey, parameterArray[i].getName()) != null) {
+          int intValue =
+            UserPreferences.getInt(hKey, parameterArray[i].getName());
+          ((IntParameter)parameterArray[i]).setParam(intValue);
+        }
+      } else if (parameterArray[i] instanceof BoolParameter) {
+        if (UserPreferences.get(hKey, parameterArray[i].getName()) != null) {
+          boolean booleanValue =
+            UserPreferences.getBool(hKey, parameterArray[i].getName());
+          ((BoolParameter)parameterArray[i]).setParam(booleanValue);
+        }
+      } else {
+        vlog.error(String.format("Unknown parameter type for parameter %s",
+                   parameterArray[i].getName()));
+      }
+    }
+
+    return servername;
+  }
+
+  public static String loadAppletParameters(VncViewer applet) {
+    String servername = applet.getParameter("Server");
+    String serverport = applet.getParameter("Port");
+    String embedParam = applet.getParameter("Embed");
+
+    if (servername == null)
+      servername = applet.getCodeBase().getHost();
+
+    if (serverport != null)
+      servername = servername.concat("::"+serverport);
+    else
+      servername = servername.concat("::5900");
+
+    if (embedParam != null)
+      embed.setParam(embedParam);
+
+    for (int i = 0; i < parameterArray.length; i++) {
+      String value = applet.getParameter(parameterArray[i].getName());
+      if (value == null)
+        continue;
+      if (parameterArray[i] instanceof StringParameter) {
+        if (value.length() > 256) {
+          vlog.error(String.format("Failed to read applet parameter %s: %s",
+                     parameterArray[i].getName(),
+                     "Invalid format or too large value"));
+          continue;
+        }
+        ((StringParameter)parameterArray[i]).setParam(value);
+      } else if (parameterArray[i] instanceof IntParameter) {
+        ((IntParameter)parameterArray[i]).setParam(value);
+      } else if (parameterArray[i] instanceof BoolParameter) {
+        ((BoolParameter)parameterArray[i]).setParam(value);
+      } else {
+        vlog.error(String.format("Unknown parameter type for parameter %s",
+                   parameterArray[i].getName()));
+
+      }
+    }
+
+    return servername;
+  }
+
+  private static void updateConnHistory(String serverName) {
+    String hKey = "ServerDialog";
+    if (serverName != null && !serverName.isEmpty()) {
+      String valueStr = UserPreferences.get(hKey, "history");
+      String t = (valueStr == null) ? "" : valueStr;
+      StringTokenizer st = new StringTokenizer(t, ",");
+      StringBuffer sb = new StringBuffer().append(serverName);
+      while (st.hasMoreTokens()) {
+        String str = st.nextToken();
+        if (!str.equals(serverName) && !str.equals(""))
+          sb.append(',').append(str);
+      }
+      UserPreferences.set(hKey, "history", sb.toString());
+      UserPreferences.save(hKey);
+    }
+  }
+
+}
diff --git a/java/com/tigervnc/vncviewer/PasswdDialog.java b/java/com/tigervnc/vncviewer/PasswdDialog.java
index fbaf991..9d5cc7c 100644
--- a/java/com/tigervnc/vncviewer/PasswdDialog.java
+++ b/java/com/tigervnc/vncviewer/PasswdDialog.java
@@ -149,7 +149,6 @@
                                             String instruction,
                                             String[] prompt,
                                             boolean[] echo) {
-    vlog.info("OK");
     Container panel = new JPanel(new GridBagLayout());
     ((JPanel)panel).setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
 
diff --git a/java/com/tigervnc/vncviewer/PlatformPixelBuffer.java b/java/com/tigervnc/vncviewer/PlatformPixelBuffer.java
index e0e24f0..564eb8e 100644
--- a/java/com/tigervnc/vncviewer/PlatformPixelBuffer.java
+++ b/java/com/tigervnc/vncviewer/PlatformPixelBuffer.java
@@ -24,86 +24,38 @@
 import java.nio.ByteOrder;
 
 import com.tigervnc.rfb.*;
+import com.tigervnc.rfb.Point;
 
-abstract public class PlatformPixelBuffer extends PixelBuffer
+public class PlatformPixelBuffer extends FullFramePixelBuffer
 {
-  public PlatformPixelBuffer(int w, int h, CConn cc_, DesktopWindow desktop_) {
-    cc = cc_;
-    desktop = desktop_;
-    PixelFormat nativePF = getNativePF();
-    if (nativePF.depth > cc.serverPF.depth) {
-      setPF(cc.serverPF);
-    } else {
-      setPF(nativePF);
-    }
-    resize(w, h);
+  public PlatformPixelBuffer(PixelFormat pf,
+                             int w, int h,
+                             WritableRaster data)
+  {
+    super(pf, w, h, data);
+    damage = new Rect(0, 0, w, h);
   }
 
-  // resize() resizes the image, preserving the image data where possible.
-  abstract public void resize(int w, int h);
-
-  public PixelFormat getNativePF() {
-    PixelFormat pf;
-    cm = tk.getColorModel();
-    if (cm.getColorSpace().getType() == java.awt.color.ColorSpace.TYPE_RGB) {
-      int depth = ((cm.getPixelSize() > 24) ? 24 : cm.getPixelSize());
-      int bpp = (depth > 16 ? 32 : (depth > 8 ? 16 : 8));
-      ByteOrder byteOrder = ByteOrder.nativeOrder();
-      boolean bigEndian = (byteOrder == ByteOrder.BIG_ENDIAN ? true : false);
-      boolean trueColour = (depth > 8 ? true : false);
-      int redShift    = cm.getComponentSize()[0] + cm.getComponentSize()[1];
-      int greenShift  = cm.getComponentSize()[0];
-      int blueShift   = 0;
-      pf = new PixelFormat(bpp, depth, bigEndian, trueColour,
-        (depth > 8 ? 0xff : 0),
-        (depth > 8 ? 0xff : 0),
-        (depth > 8 ? 0xff : 0),
-        (depth > 8 ? redShift : 0),
-        (depth > 8 ? greenShift : 0),
-        (depth > 8 ? blueShift : 0));
-    } else {
-      pf = new PixelFormat(8, 8, false, false, 7, 7, 3, 0, 3, 6);
-    }
-    vlog.debug("Native pixel format is "+pf.print());
-    return pf;
-  }
-
-  abstract public void imageRect(int x, int y, int w, int h, Object pix);
-
-  // setColourMapEntries() changes some of the entries in the colourmap.
-  // However these settings won't take effect until updateColourMap() is
-  // called.  This is because getting java to recalculate its internal
-  // translation table and redraw the screen is expensive.
-
-  public void setColourMapEntries(int firstColour, int nColours_,
-                                               int[] rgbs) {
-    nColours = nColours_;
-    reds = new byte[nColours];
-    blues = new byte[nColours];
-    greens = new byte[nColours];
-    for (int i = 0; i < nColours; i++) {
-      reds[firstColour+i] = (byte)(rgbs[i*3]   >> 8);
-      greens[firstColour+i] = (byte)(rgbs[i*3+1] >> 8);
-      blues[firstColour+i] = (byte)(rgbs[i*3+2] >> 8);
+  public void commitBufferRW(Rect r)
+  {
+    super.commitBufferRW(r);
+    synchronized(damage) {
+      Rect n = damage.union_boundary(r);
+      damage.setXYWH(n.tl.x, n.tl.y, n.width(), n.height());
     }
   }
 
-  public void updateColourMap() {
-    cm = new IndexColorModel(8, nColours, reds, greens, blues);
+  public Rect getDamage() {
+    Rect r = new Rect();
+
+    synchronized(damage) {
+      r.setXYWH(damage.tl.x, damage.tl.y, damage.width(), damage.height());
+      damage.clear();
+    }
+
+    return r;
   }
 
-  protected static Toolkit tk = Toolkit.getDefaultToolkit();
+  protected Rect damage;
 
-  abstract public Image getImage();
-
-  protected Image image;
-
-  int nColours;
-  byte[] reds;
-  byte[] greens;
-  byte[] blues;
-
-  CConn cc;
-  DesktopWindow desktop;
-  static LogWriter vlog = new LogWriter("PlatformPixelBuffer");
 }
diff --git a/java/com/tigervnc/vncviewer/ServerDialog.java b/java/com/tigervnc/vncviewer/ServerDialog.java
index 172bde6..001e231 100644
--- a/java/com/tigervnc/vncviewer/ServerDialog.java
+++ b/java/com/tigervnc/vncviewer/ServerDialog.java
@@ -21,8 +21,11 @@
 
 import java.awt.*;
 import java.awt.event.*;
+import java.io.File;
+import java.nio.CharBuffer;
 import javax.swing.*;
 import javax.swing.border.*;
+import javax.swing.filechooser.*;
 import javax.swing.WindowConstants.*;
 import java.util.*;
 
@@ -30,51 +33,64 @@
 
 import static java.awt.GridBagConstraints.HORIZONTAL;
 import static java.awt.GridBagConstraints.LINE_START;
+import static java.awt.GridBagConstraints.LINE_END;
 import static java.awt.GridBagConstraints.NONE;
 import static java.awt.GridBagConstraints.REMAINDER;
 
-class ServerDialog extends Dialog {
+import static com.tigervnc.vncviewer.Parameters.*;
+
+class ServerDialog extends Dialog implements Runnable {
 
   @SuppressWarnings({"unchecked","rawtypes"})
-  public ServerDialog(OptionsDialog options_,
-                      String defaultServerName, CConn cc_) {
-
+  public ServerDialog(String defaultServerName,
+                      CharBuffer vncServerName) {
     super(true);
-    cc = cc_;
+    this.vncServerName = vncServerName;
     setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
     setTitle("VNC Viewer: Connection Details");
     setResizable(false);
     addWindowListener(new WindowAdapter() {
       public void windowClosing(WindowEvent e) {
-        if (VncViewer.nViewers == 1) {
-          cc.viewer.exit(1);
-        } else {
-          ret = false;
-          endDialog();
-        }
+        endDialog();
+        System.exit(1);
       }
     });
 
-    options = options_;
+    JLabel serverLabel = new JLabel("VNC server:", JLabel.RIGHT);
+    String valueStr = new String(defaultServerName);
+    ArrayList<String> servernames = new ArrayList<String>();
+    if (!valueStr.isEmpty())
+      servernames.add(valueStr);
+    String history = UserPreferences.get("ServerDialog", "history");
+    if (history != null) {
+      for (String s : history.split(",")) {
+        if (servernames.indexOf(s) < 0)
+          servernames.add(s);
+      }
+    }
+    serverName = new MyJComboBox(servernames.toArray());
+    serverName.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        JComboBox s = (JComboBox)e.getSource();
+        if (e.getActionCommand().equals("comboBoxEdited")) {
+          s.insertItemAt(editor.getItem(), 0);
+          s.setSelectedIndex(0);
+        }
+      }
+    });
+    if (servernames.size() == 0)
+      serverName.setPrototypeDisplayValue("255.255.255.255:5900");
 
-    JLabel serverLabel = new JLabel("VNC Server:", JLabel.RIGHT);
-    String valueStr = new String("");
-    if (UserPreferences.get("ServerDialog", "history") != null)
-      valueStr = UserPreferences.get("ServerDialog", "history");
-    server = new MyJComboBox(valueStr.split(","));
-    if (valueStr.equals(""))
-      server.setPrototypeDisplayValue("255.255.255.255:5900");
-
-    server.setEditable(true);
-    editor = server.getEditor();
+    serverName.setEditable(true);
+    editor = serverName.getEditor();
     editor.getEditorComponent().addKeyListener(new KeyListener() {
       public void keyTyped(KeyEvent e) {}
       public void keyReleased(KeyEvent e) {}
       public void keyPressed(KeyEvent e) {
         if (e.getKeyCode() == KeyEvent.VK_ENTER) {
-          server.insertItemAt(editor.getItem(), 0);
-          server.setSelectedIndex(0);
-          commit();
+          serverName.insertItemAt(editor.getItem(), 0);
+          serverName.setSelectedIndex(0);
+          handleConnect();
         }
       }
     });
@@ -84,9 +100,41 @@
 
     JLabel icon = new JLabel(VncViewer.logoIcon);
     optionsButton = new JButton("Options...");
+    optionsButton.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        handleOptions();
+      }
+    });
+    JButton loadButton = new JButton("Load...");
+    loadButton.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        handleLoad();
+      }
+    });
+    JButton saveAsButton = new JButton("Save As...");
+    saveAsButton.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        handleSaveAs();
+      }
+    });
     aboutButton = new JButton("About...");
-    okButton = new JButton("OK");
+    aboutButton.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        handleAbout();
+      }
+    });
     cancelButton = new JButton("Cancel");
+    cancelButton.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        handleCancel();
+      }
+    });
+    connectButton = new JButton("Connect   \u21B5");
+    connectButton.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        handleConnect();
+      }
+    });
 
     contentPane.add(icon,
                     new GridBagConstraints(0, 0,
@@ -102,89 +150,145 @@
                                            LINE_START, NONE,
                                            new Insets(5, 10, 5, 5),
                                            NONE, NONE));
-    contentPane.add(server,
+    contentPane.add(serverName,
                     new GridBagConstraints(2, 0,
                                            REMAINDER, 1,
                                            HEAVY, LIGHT,
                                            LINE_START, HORIZONTAL,
                                            new Insets(5, 0, 5, 5),
                                            NONE, NONE));
-    JPanel buttonPane = new JPanel();
-    buttonPane.setLayout(new GridLayout(1, 4, 5, 5));
-    buttonPane.add(aboutButton);
-    buttonPane.add(optionsButton);
-    buttonPane.add(okButton);
-    buttonPane.add(cancelButton);
-    contentPane.add(buttonPane,
+    JPanel buttonPane1 = new JPanel();
+    Box box = Box.createHorizontalBox();
+    JSeparator separator1 = new JSeparator();
+    JSeparator separator2 = new JSeparator();
+    GroupLayout layout = new GroupLayout(buttonPane1);
+    buttonPane1.setLayout(layout);
+    layout.setAutoCreateGaps(false);
+    layout.setAutoCreateContainerGaps(false);
+		layout.setHorizontalGroup(
+		  layout.createSequentialGroup()
+		    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
+		      .addGroup(layout.createSequentialGroup()
+		        .addGap(10)
+		        .addComponent(optionsButton))
+		      .addComponent(separator1)
+		      .addGroup(layout.createSequentialGroup()
+		        .addGap(10)
+		        .addComponent(aboutButton)))
+		    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
+		      .addGroup(layout.createSequentialGroup()
+		        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+		        .addComponent(loadButton)
+		        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+		        .addComponent(saveAsButton)
+		        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+		        .addComponent(box)
+		        .addGap(10))
+		      .addComponent(separator2)
+		      .addGroup(layout.createSequentialGroup()
+		        .addComponent(cancelButton)
+		        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+		        .addComponent(connectButton)
+		        .addGap(10)))
+		);
+		layout.setVerticalGroup(
+		  layout.createSequentialGroup()
+		    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
+		      .addComponent(optionsButton)
+		      .addComponent(loadButton)
+		      .addComponent(saveAsButton)
+		      .addComponent(box))
+		    .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+		    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
+		      .addComponent(separator1)
+		      .addComponent(separator2))
+		    .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+		    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
+		      .addComponent(aboutButton)
+		      .addComponent(cancelButton)
+		      .addComponent(connectButton))
+		    .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+		);
+		layout.linkSize(SwingConstants.HORIZONTAL,
+                    optionsButton, loadButton, saveAsButton,
+                    aboutButton, cancelButton, box);
+    contentPane.add(buttonPane1,
                     new GridBagConstraints(0, 1,
                                            REMAINDER, 1,
                                            LIGHT, LIGHT,
                                            LINE_START, HORIZONTAL,
-                                           new Insets(5, 5, 5, 5),
+                                           new Insets(5, 0, 10, 0),
                                            NONE, NONE));
-    addListeners(this);
     pack();
   }
 
-  @SuppressWarnings({"unchecked","rawtypes"})
-  public void actionPerformed(ActionEvent e) {
-    Object s = e.getSource();
-    if (s instanceof JButton && (JButton)s == okButton) {
-      commit();
-    } else if (s instanceof JButton && (JButton)s == cancelButton) {
-      if (VncViewer.nViewers == 1)
-        cc.viewer.exit(1);
-      ret = false;
-      endDialog();
-    } else if (s instanceof JButton && (JButton)s == optionsButton) {
-      options.showDialog(this);
-    } else if (s instanceof JButton && (JButton)s == aboutButton) {
-      cc.showAbout();
-    } else if (s instanceof JComboBox && (JComboBox)s == server) {
-      if (e.getActionCommand().equals("comboBoxEdited")) {
-        server.insertItemAt(editor.getItem(), 0);
-        server.setSelectedIndex(0);
-      }
-    }
+  public void run() {
+    this.showDialog();
   }
 
-  private void commit() {
-    String serverName = (String)server.getSelectedItem();
-    if (serverName == null || serverName.equals("")) {
-      vlog.error("Invalid servername specified");
-      if (VncViewer.nViewers == 1)
-        cc.viewer.exit(1);
-      ret = false;
-      endDialog();
+  private void handleOptions() {
+    OptionsDialog.showDialog(this);
+  }
+
+  private void handleLoad() {
+    String title = "Select a TigerVNC configuration file";
+    File dflt = new File(FileUtils.getVncHomeDir().concat("default.tigervnc"));
+    FileNameExtensionFilter filter =
+      new FileNameExtensionFilter("TigerVNC configuration (*.tigervnc)", "tigervnc");
+    File f = showChooser(title, dflt, filter);
+    if (f != null && f.exists() && f.canRead())
+      loadViewerParameters(f.getAbsolutePath());
+  }
+
+  private void handleSaveAs() {
+    String title = "Save the TigerVNC configuration to file";
+    File dflt = new File(FileUtils.getVncHomeDir().concat("default.tigervnc"));
+    if (!dflt.exists() || !dflt.isFile())
+      dflt = new File(FileUtils.getVncHomeDir());
+    FileNameExtensionFilter filter =
+      new FileNameExtensionFilter("TigerVNC configuration (*.tigervnc)", "tigervnc");
+    File f = showChooser(title, dflt, filter);
+    while (f != null && f.exists() && f.isFile()) {
+      String msg = f.getAbsolutePath();
+      msg = msg.concat(" already exists. Do you want to overwrite?");
+      Object[] options = {"Overwrite", "No  \u21B5"};
+      JOptionPane op =
+        new JOptionPane(msg, JOptionPane.QUESTION_MESSAGE,
+                        JOptionPane.OK_CANCEL_OPTION, null, options, options[1]);
+      JDialog dlg = op.createDialog(this, "TigerVNC Viewer");
+      dlg.setIconImage(VncViewer.frameIcon);
+      dlg.setAlwaysOnTop(true);
+      dlg.setVisible(true);
+      if (op.getValue() == options[0])
+        break;
+      else
+        f = showChooser(title, f, filter);
     }
-    // set params
-    Configuration.setParam("Server", Hostname.getHost(serverName));
-    Configuration.setParam("Port",
-                            Integer.toString(Hostname.getPort(serverName)));
-    // Update the history list
-    String valueStr = UserPreferences.get("ServerDialog", "history");
-    String t = (valueStr == null) ? "" : valueStr;
-    StringTokenizer st = new StringTokenizer(t, ",");
-    StringBuffer sb =
-        new StringBuffer().append((String)server.getSelectedItem());
-    while (st.hasMoreTokens()) {
-      String str = st.nextToken();
-      if (!str.equals((String)server.getSelectedItem()) && !str.equals("")) {
-        sb.append(',');
-        sb.append(str);
-      }
-    }
-    UserPreferences.set("ServerDialog", "history", sb.toString());
-    UserPreferences.save("ServerDialog");
+    if (f != null && (!f.exists() || f.canWrite()))
+      saveViewerParameters(f.getAbsolutePath(), (String)serverName.getSelectedItem());
+  }
+
+  private void handleAbout() {
+    VncViewer.showAbout(this);
+  }
+
+  private void handleCancel() {
     endDialog();
   }
 
-  CConn cc;
+  private void handleConnect() {
+    String servername = (String)serverName.getSelectedItem();
+    vncServerName.put(servername).flip();
+    saveViewerParameters(null, servername);
+    endDialog();
+  }
+
   @SuppressWarnings("rawtypes")
-  MyJComboBox server;
+  MyJComboBox serverName;
   ComboBoxEditor editor;
-  JButton aboutButton, optionsButton, okButton, cancelButton;
+  JButton aboutButton, optionsButton, connectButton, cancelButton;
   OptionsDialog options;
+  CharBuffer vncServerName;
   static LogWriter vlog = new LogWriter("ServerDialog");
 
 }
diff --git a/java/com/tigervnc/vncviewer/Tunnel.java b/java/com/tigervnc/vncviewer/Tunnel.java
index 90ab38e..bb98ec7 100644
--- a/java/com/tigervnc/vncviewer/Tunnel.java
+++ b/java/com/tigervnc/vncviewer/Tunnel.java
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2012-2016 All Rights Reserved.
+ *  Copyright (C) 2012-2016 Brian P. Hinz. All Rights Reserved.
  *  Copyright (C) 2000 Const Kaplinsky.  All Rights Reserved.
  *  Copyright (C) 1999 AT&T Laboratories Cambridge.  All Rights Reserved.
  *
@@ -43,6 +43,8 @@
 import com.jcraft.jsch.OpenSSHConfig;
 import com.jcraft.jsch.Session;
 
+import static com.tigervnc.vncviewer.Parameters.*;
+
 public class Tunnel {
 
   private final static String DEFAULT_TUNNEL_TEMPLATE
@@ -56,27 +58,27 @@
     String remoteHost;
 
     remotePort = cc.getServerPort();
-    if (cc.viewer.tunnel.getValue()) {
+    if (tunnel.getValue()) {
       gatewayHost = cc.getServerName();
       remoteHost = "localhost";
     } else {
-      gatewayHost = getSshHost(cc);
+      gatewayHost = getSshHost();
       remoteHost = cc.getServerName();
     }
 
-    String pattern = cc.viewer.extSSHArgs.getValue();
-    if (pattern == null) {
-      if (cc.viewer.tunnel.getValue())
+    String pattern = extSSHArgs.getValue();
+    if (pattern == null || pattern.isEmpty()) {
+      if (tunnel.getValue())
         pattern = System.getProperty("VNC_TUNNEL_CMD");
       else
         pattern = System.getProperty("VNC_VIA_CMD");
     }
 
-    if (cc.viewer.extSSH.getValue() || 
+    if (extSSH.getValue() || 
         (pattern != null && pattern.length() > 0)) {
-      createTunnelExt(gatewayHost, remoteHost, remotePort, localPort, pattern, cc);
+      createTunnelExt(gatewayHost, remoteHost, remotePort, localPort, pattern);
     } else {
-      createTunnelJSch(gatewayHost, remoteHost, remotePort, localPort, cc);
+      createTunnelJSch(gatewayHost, remoteHost, remotePort, localPort);
     }
   }
 
@@ -99,10 +101,10 @@
     }
   }
 
-  public static String getSshHost(CConn cc) {
-    String sshHost = cc.viewer.via.getValue();
-    if (sshHost == null)
-      return cc.getServerName();
+  public static String getSshHost() {
+    String sshHost = via.getValue();
+    if (sshHost.isEmpty())
+      return vncServerName.getValue();
     int end = sshHost.indexOf(":");
     if (end < 0)
       end = sshHost.length();
@@ -110,37 +112,42 @@
     return sshHost;
   }
 
-  public static String getSshUser(CConn cc) {
+  public static String getSshUser() {
     String sshUser = (String)System.getProperties().get("user.name");
-    String via = cc.viewer.via.getValue();
-    if (via != null && via.indexOf("@") > 0)
-      sshUser = via.substring(0, via.indexOf("@"));
+    String viaStr = via.getValue();
+    if (!viaStr.isEmpty() && viaStr.indexOf("@") > 0)
+      sshUser = viaStr.substring(0, viaStr.indexOf("@"));
     return sshUser;
   }
 
-  public static int getSshPort(CConn cc) {
+  public static int getSshPort() {
     String sshPort = "22";
-    String via = cc.viewer.via.getValue();
-    if (via != null && via.indexOf(":") > 0)
-      sshPort = via.substring(via.indexOf(":")+1, via.length());
+    String viaStr = via.getValue();
+    if (!viaStr.isEmpty() && viaStr.indexOf(":") > 0)
+      sshPort = viaStr.substring(viaStr.indexOf(":")+1, viaStr.length());
     return Integer.parseInt(sshPort);
   }
 
-  public static String getSshKeyFile(CConn cc) {
-    if (cc.viewer.sshKeyFile.getValue() != null)
-      return cc.viewer.sshKeyFile.getValue();
+  public static String getSshKeyFile() {
+    if (!sshKeyFile.getValue().isEmpty())
+      return sshKeyFile.getValue();
     String[] ids = { "id_dsa", "id_rsa" };
     for (String id : ids) {
       File f = new File(FileUtils.getHomeDir()+".ssh/"+id);
       if (f.exists() && f.canRead())
         return(f.getAbsolutePath());
     }
-    return null;
+    return "";
+  }
+
+  public static String getSshKey() {
+    if (!sshKey.getValue().isEmpty())
+      return sshKeyFile.getValue().replaceAll("\\\\n", "\n");
+    return "";
   }
 
   private static void createTunnelJSch(String gatewayHost, String remoteHost,
-                                       int remotePort, int localPort,
-                                       CConn cc) throws Exception {
+                                       int remotePort, int localPort) throws Exception {
     JSch.setLogger(new MyJSchLogger());
     JSch jsch=new JSch();
 
@@ -152,44 +159,39 @@
       if (knownHosts.exists() && knownHosts.canRead())
   	    jsch.setKnownHosts(knownHosts.getAbsolutePath());
       ArrayList<File> privateKeys = new ArrayList<File>();
-      String sshKeyFile = cc.options.sshKeyFile.getText();
-      String sshKey = cc.viewer.sshKey.getValue();
-      if (sshKey != null) {
-        String sshKeyPass = cc.viewer.sshKeyPass.getValue();
+      if (!getSshKey().isEmpty()) {
         byte[] keyPass = null, key;
-        if (sshKeyPass != null)
-          keyPass = sshKeyPass.getBytes();
-        sshKey = sshKey.replaceAll("\\\\n", "\n");
-        key = sshKey.getBytes();
-        jsch.addIdentity("TigerVNC", key, null, keyPass);
-      } else if (!sshKeyFile.equals("")) {
-        File f = new File(sshKeyFile);
+        if (!sshKeyPass.getValue().isEmpty())
+          keyPass = sshKeyPass.getValue().getBytes();
+        jsch.addIdentity("TigerVNC", getSshKey().getBytes(), null, keyPass);
+      } else if (!getSshKeyFile().isEmpty()) {
+        File f = new File(getSshKeyFile());
         if (!f.exists() || !f.canRead())
-          throw new Exception("Cannot access SSH key file "+ sshKeyFile);
+          throw new Exception("Cannot access SSH key file "+getSshKeyFile());
         privateKeys.add(f);
       }
       for (Iterator<File> i = privateKeys.iterator(); i.hasNext();) {
         File privateKey = (File)i.next();
         if (privateKey.exists() && privateKey.canRead())
-          if (cc.viewer.sshKeyPass.getValue() != null)
+          if (!sshKeyPass.getValue().isEmpty())
   	        jsch.addIdentity(privateKey.getAbsolutePath(),
-                             cc.viewer.sshKeyPass.getValue());
+                             sshKeyPass.getValue());
           else
   	        jsch.addIdentity(privateKey.getAbsolutePath());
       }
-  
-      String user = getSshUser(cc);
+
+      String user = getSshUser();
       String label = new String("SSH Authentication");
       PasswdDialog dlg =
         new PasswdDialog(label, (user == null ? false : true), false);
       dlg.userEntry.setText(user != null ? user : "");
-      File ssh_config = new File(cc.viewer.sshConfig.getValue());
+      File ssh_config = new File(sshConfig.getValue());
       if (ssh_config.exists() && ssh_config.canRead()) {
         ConfigRepository repo =
           OpenSSHConfig.parse(ssh_config.getAbsolutePath());
         jsch.setConfigRepository(repo);
       }
-      Session session=jsch.getSession(user, gatewayHost, getSshPort(cc));
+      Session session=jsch.getSession(user, gatewayHost, getSshPort());
       session.setUserInfo(dlg);
       // OpenSSHConfig doesn't recognize StrictHostKeyChecking
       if (session.getConfig("StrictHostKeyChecking") == null)
@@ -201,93 +203,19 @@
     }
   }
 
-  private static class MyExtProcess implements Runnable {
-
-    private String cmd = null;
-    private Process pid = null;
-
-    private static class MyProcessLogger extends Thread {
-      private final BufferedReader err;
-  
-      public MyProcessLogger(Process p) {
-        InputStreamReader reader = 
-          new InputStreamReader(p.getErrorStream());
-        err = new BufferedReader(reader);
-      }
-  
-      @Override
-      public void run() {
-        try {
-          while (true) {
-            String msg = err.readLine();
-            if (msg != null)
-              vlog.info(msg);
-          }
-        } catch(java.io.IOException e) {
-          vlog.info(e.getMessage());
-        } finally {
-          try {
-            if (err != null)
-              err.close();
-          } catch (java.io.IOException e ) { }
-        }
-      }
-    }
-
-    private static class MyShutdownHook extends Thread {
-
-      private Process proc = null;
-
-      public MyShutdownHook(Process p) {
-        proc = p;
-      }
-  
-      @Override
-      public void run() {
-        try {
-          proc.exitValue();
-        } catch (IllegalThreadStateException e) {
-          try {
-            // wait for CConn to shutdown the socket
-            Thread.sleep(500);
-          } catch(InterruptedException ie) { }
-          proc.destroy();
-        }
-      }
-    }
-
-    public MyExtProcess(String command) {
-      cmd = command;  
-    }
-
-    public void run() {
-      try {
-        Runtime runtime = Runtime.getRuntime();
-        pid = runtime.exec(cmd);
-        runtime.addShutdownHook(new MyShutdownHook(pid));
-        new MyProcessLogger(pid).start();
-        pid.waitFor();
-      } catch(InterruptedException e) {
-        vlog.info(e.getMessage());
-      } catch(java.io.IOException e) {
-        vlog.info(e.getMessage());
-      }
-    }
-  }
-
   private static void createTunnelExt(String gatewayHost, String remoteHost,
                                       int remotePort, int localPort,
-                                      String pattern, CConn cc) throws Exception {
+                                      String pattern) throws Exception {
     if (pattern == null || pattern.length() < 1) {
-      if (cc.viewer.tunnel.getValue())
+      if (tunnel.getValue())
         pattern = DEFAULT_TUNNEL_TEMPLATE;
       else
         pattern = DEFAULT_VIA_TEMPLATE;
     }
     String cmd = fillCmdPattern(pattern, gatewayHost, remoteHost,
-                                remotePort, localPort, cc);
+                                remotePort, localPort);
     try {
-      Thread t = new Thread(new MyExtProcess(cmd));
+      Thread t = new Thread(new ExtProcess(cmd, vlog, true));
       t.start();
       // wait for the ssh process to start
       Thread.sleep(1000);
@@ -298,21 +226,21 @@
 
   private static String fillCmdPattern(String pattern, String gatewayHost,
                                        String remoteHost, int remotePort,
-                                       int localPort, CConn cc) {
+                                       int localPort) {
     boolean H_found = false, G_found = false, R_found = false, L_found = false;
     boolean P_found = false;
-    String cmd = cc.options.sshClient.getText() + " ";
+    String cmd = extSSHClient.getValue() + " ";
     pattern.replaceAll("^\\s+", "");
 
-    String user = getSshUser(cc);
-    int sshPort = getSshPort(cc);
+    String user = getSshUser();
+    int sshPort = getSshPort();
     gatewayHost = user + "@" + gatewayHost;
 
     for (int i = 0; i < pattern.length(); i++) {
       if (pattern.charAt(i) == '%') {
         switch (pattern.charAt(++i)) {
         case 'H':
-          cmd += (cc.viewer.tunnel.getValue() ? gatewayHost : remoteHost);
+          cmd += (tunnel.getValue() ? gatewayHost : remoteHost);
   	      H_found = true;
           continue;
         case 'G':
@@ -342,7 +270,7 @@
     if (!H_found || !R_found || !L_found)
       throw new Exception("%H, %R or %L absent in tunneling command template.");
 
-    if (!cc.viewer.tunnel.getValue() && !G_found)
+    if (!tunnel.getValue() && !G_found)
       throw new Exception("%G pattern absent in tunneling command template.");
 
     vlog.info("SSH command line: "+cmd);
diff --git a/java/com/tigervnc/vncviewer/Viewport.java b/java/com/tigervnc/vncviewer/Viewport.java
index b55744b..bf07d2d 100644
--- a/java/com/tigervnc/vncviewer/Viewport.java
+++ b/java/com/tigervnc/vncviewer/Viewport.java
@@ -1,6 +1,8 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright (C) 2011-2015 Brian P. Hinz
- * Copyright (C) 2012-2013 D. R. Commander.  All Rights Reserved.
+ * Copyright (C) 2006 Constantin Kaplinsky.  All Rights Reserved.
+ * Copyright (C) 2009 Paul Donohue.  All Rights Reserved.
+ * Copyright (C) 2010, 2012-2013 D. R. Commander.  All Rights Reserved.
+ * Copyright (C) 2011-2014 Brian P. Hinz
  *
  * This is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -18,203 +20,443 @@
  * USA.
  */
 
-package com.tigervnc.vncviewer;
+//
+// DesktopWindow is an AWT Canvas representing a VNC desktop.
+//
+// Methods on DesktopWindow are called from both the GUI thread and the thread
+// which processes incoming RFB messages ("the RFB thread").  This means we
+// need to be careful with synchronization here.
+//
 
+package com.tigervnc.vncviewer;
+import java.awt.*;
 import java.awt.Color;
+import java.awt.color.ColorSpace;
 import java.awt.event.*;
-import java.awt.Dimension;
-import java.awt.Event;
-import java.awt.GraphicsConfiguration;
-import java.awt.GraphicsDevice;
-import java.awt.GraphicsEnvironment;
-import java.awt.Image;
-import java.awt.Insets;
-import java.awt.Window;
-import java.lang.reflect.*;
+import java.awt.geom.AffineTransform;
+import java.awt.image.*;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.Clipboard;
+import java.io.BufferedReader;
+import java.nio.*;
 import javax.swing.*;
 
+import javax.imageio.*;
+import java.io.*;
+
 import com.tigervnc.rfb.*;
-import java.lang.Exception;
-import java.awt.Rectangle;
+import com.tigervnc.rfb.Cursor;
+import com.tigervnc.rfb.Point;
 
-import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER;
-import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER;
-import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED;
-import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED;
+import static com.tigervnc.vncviewer.Parameters.*;
 
-public class Viewport extends JFrame
-{
-  public Viewport(String name, CConn cc_) {
+class Viewport extends JPanel implements MouseListener,
+  MouseMotionListener, MouseWheelListener, KeyListener {
+
+  static LogWriter vlog = new LogWriter("Viewport");
+
+  public Viewport(int w, int h, PixelFormat serverPF, CConn cc_)
+  {
     cc = cc_;
-    setTitle(name+" - TigerVNC");
-    setFocusable(false);
-    setFocusTraversalKeysEnabled(false);
-    if (!VncViewer.os.startsWith("mac os x"))
-      setIconImage(VncViewer.frameIcon);
-    UIManager.getDefaults().put("ScrollPane.ancestorInputMap",
-      new UIDefaults.LazyInputMap(new Object[]{}));
-    sp = new JScrollPane();
-    sp.getViewport().setBackground(Color.BLACK);
-    sp.setBorder(BorderFactory.createEmptyBorder(0,0,0,0));
-    getContentPane().add(sp);
-    if (VncViewer.os.startsWith("mac os x")) {
-      if (!VncViewer.noLionFS.getValue())
-        enableLionFS();
-    }
-    addWindowFocusListener(new WindowAdapter() {
-      public void windowGainedFocus(WindowEvent e) {
-        if (isVisible())
-          sp.getViewport().getView().requestFocusInWindow();
+    setScaledSize(cc.cp.width, cc.cp.height);
+    frameBuffer = createFramebuffer(serverPF, w, h);
+    assert(frameBuffer != null);
+    setBackground(Color.BLACK);
+
+    cc.setFramebuffer(frameBuffer);
+    OptionsDialog.addCallback("handleOptions", this);
+
+    addMouseListener(this);
+    addMouseWheelListener(this);
+    addMouseMotionListener(this);
+    addKeyListener(this);
+    addFocusListener(new FocusAdapter() {
+      public void focusGained(FocusEvent e) {
+        ClipboardDialog.clientCutText();
       }
-      public void windowLostFocus(WindowEvent e) {
+      public void focusLost(FocusEvent e) {
         cc.releaseDownKeys();
       }
     });
-    addWindowListener(new WindowAdapter() {
-      public void windowClosing(WindowEvent e) {
-        if (VncViewer.nViewers == 1) {
-          if (cc.closeListener != null) {
-            cc.close();
-          } else {
-            cc.viewer.exit(1);
-          }
-        } else {
-          cc.close();
-        }
-      }
-    });
-    addComponentListener(new ComponentAdapter() {
-      public void componentResized(ComponentEvent e) {
-        String scaleString = cc.viewer.scalingFactor.getValue();
-        if (scaleString.equalsIgnoreCase("Auto") ||
-            scaleString.equalsIgnoreCase("FixedRatio")) {
-          if ((sp.getSize().width != cc.desktop.scaledWidth) ||
-              (sp.getSize().height != cc.desktop.scaledHeight)) {
-            cc.desktop.setScaledSize();
-            sp.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER);
-            sp.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_NEVER);
-            sp.validate();
-            if (getExtendedState() != JFrame.MAXIMIZED_BOTH &&
-                !cc.fullScreen) {
-              sp.setSize(new Dimension(cc.desktop.scaledWidth,
-                                       cc.desktop.scaledHeight));
-              int w = cc.desktop.scaledWidth + getInsets().left +
-                      getInsets().right;
-              int h = cc.desktop.scaledHeight + getInsets().top +
-                      getInsets().bottom;
-              if (scaleString.equalsIgnoreCase("FixedRatio"))
-                setSize(w, h);
-            }
-          }
-        } else {
-          sp.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED);
-          sp.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED);
-          sp.validate();
-        }
-        if (cc.desktop.cursor != null) {
-          Cursor cursor = cc.desktop.cursor;
-          cc.setCursor(cursor.width(),cursor.height(),cursor.hotspot,
-                       cursor.data, cursor.mask);
-        }
-      }
-    });
+    setFocusTraversalKeysEnabled(false);
+    setFocusable(true);
+
+    // Send a fake pointer event so that the server will stop rendering
+    // a server-side cursor. Ideally we'd like to send the actual pointer
+    // position, but we can't really tell when the window manager is done
+    // placing us so we don't have a good time for that.
+    cc.writer().writePointerEvent(new Point(w/2, h/2), 0);
   }
 
-  boolean lionFSSupported() { return canDoLionFS; }
+  // Most efficient format (from Viewport's point of view)
+  public PixelFormat getPreferredPF()
+  {
+    return frameBuffer.getPF();
+  }
 
-  void enableLionFS() {
-    try {
-      String version = System.getProperty("os.version");
-      int firstDot = version.indexOf('.');
-      int lastDot = version.lastIndexOf('.');
-      if (lastDot > firstDot && lastDot >= 0) {
-        version = version.substring(0, version.indexOf('.', firstDot + 1));
+  // Copy the areas of the framebuffer that have been changed (damaged)
+  // to the displayed window.
+  public void updateWindow() {
+    Rect r = frameBuffer.getDamage();
+    if (!r.is_empty()) {
+      if (image == null)
+        image = (BufferedImage)createImage(frameBuffer.width(), frameBuffer.height());
+      image.getRaster().setDataElements(r.tl.x, r.tl.y, frameBuffer.getBuffer(r));
+      if (cc.cp.width != scaledWidth ||
+          cc.cp.height != scaledHeight) {
+        AffineTransform t = new AffineTransform(); 
+        t.scale((double)scaleRatioX, (double)scaleRatioY);
+        Rectangle s = new Rectangle(r.tl.x, r.tl.y, r.width(), r.height());
+        s = t.createTransformedShape(s).getBounds();
+        paintImmediately(s.x, s.y, s.width, s.height);
+      } else {
+        paintImmediately(r.tl.x, r.tl.y, r.width(), r.height());
       }
-      double v = Double.parseDouble(version);
-      if (v < 10.7)
-        throw new Exception("Operating system version is " + v);
-
-      Class fsuClass = Class.forName("com.apple.eawt.FullScreenUtilities");
-      Class argClasses[] = new Class[]{Window.class, Boolean.TYPE};
-      Method setWindowCanFullScreen =
-        fsuClass.getMethod("setWindowCanFullScreen", argClasses);
-      setWindowCanFullScreen.invoke(fsuClass, this, true);
-
-      canDoLionFS = true;
-    } catch (Exception e) {
-      vlog.debug("Could not enable OS X 10.7+ full-screen mode: " +
-                 e.getMessage());
     }
   }
 
-  public void toggleLionFS() {
-    try {
-      Class appClass = Class.forName("com.apple.eawt.Application");
-      Method getApplication = appClass.getMethod("getApplication",
-                                                 (Class[])null);
-      Object app = getApplication.invoke(appClass);
-      Method requestToggleFullScreen =
-        appClass.getMethod("requestToggleFullScreen", Window.class);
-      requestToggleFullScreen.invoke(app, this);
-    } catch (Exception e) {
-      vlog.debug("Could not toggle OS X 10.7+ full-screen mode: " +
-                 e.getMessage());
-    }
-  }
+  static final int[] dotcursor_xpm = {
+    0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+    0x00000000, 0xff000000, 0xff000000, 0xff000000, 0x00000000,
+    0x00000000, 0xff000000, 0xff000000, 0xff000000, 0x00000000,
+    0x00000000, 0xff000000, 0xff000000, 0xff000000, 0x00000000,
+    0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+  };
 
-  public JViewport getViewport() {
-    return sp.getViewport();
-  }
+  public void setCursor(int width, int height, Point hotspot,
+                        byte[] data, byte[] mask)
+  {
 
-  public void setGeometry(int x, int y, int w, int h) {
-    pack();
-    if (!cc.fullScreen)
-      setLocation(x, y);
-  }
+    int mask_len = ((width+7)/8) * height;
+    int i;
 
-  public Dimension getScreenSize() {
-    return getScreenBounds().getSize();
-  }
+    for (i = 0; i < mask_len; i++)
+      if ((mask[i] & 0xff) != 0) break;
 
-  public Rectangle getScreenBounds() {
-    GraphicsEnvironment ge =
-      GraphicsEnvironment.getLocalGraphicsEnvironment();
-    Rectangle r = new Rectangle();
-    setMaximizedBounds(null);
-    if (cc.viewer.fullScreenAllMonitors.getValue()) {
-      for (GraphicsDevice gd : ge.getScreenDevices())
-        for (GraphicsConfiguration gc : gd.getConfigurations())
-          r = r.union(gc.getBounds());
-      Rectangle mb = new Rectangle(r);
-      mb.grow(getInsets().left, getInsets().bottom);
-      setMaximizedBounds(mb);
+    if ((i == mask_len) && dotWhenNoCursor.getValue()) {
+      vlog.debug("cursor is empty - using dot");
+      cursor = new BufferedImage(5, 5, BufferedImage.TYPE_INT_ARGB_PRE);
+      cursor.setRGB(0, 0, 5, 5, dotcursor_xpm, 0, 5);
+      cursorHotspot.x = cursorHotspot.y = 3;
     } else {
-      GraphicsDevice gd = ge.getDefaultScreenDevice();
-      GraphicsConfiguration gc = gd.getDefaultConfiguration();
-      r = gc.getBounds();
+      if ((width == 0) || (height == 0)) {
+        cursor = new BufferedImage(tk.getBestCursorSize(0, 0).width,
+                                   tk.getBestCursorSize(0, 0).height,
+                                   BufferedImage.TYPE_INT_ARGB_PRE);
+        cursorHotspot.x = cursorHotspot.y = 0;
+      } else {
+        ByteBuffer buffer = ByteBuffer.allocate(width*height*4);
+        ByteBuffer in, o, m;
+        int m_width;
+
+        PixelFormat pf;
+
+        pf = cc.cp.pf();
+
+        in = (ByteBuffer)ByteBuffer.wrap(data).mark();
+        o = (ByteBuffer)buffer.duplicate().mark();
+        m = ByteBuffer.wrap(mask);
+        m_width = (width+7)/8;
+
+        for (int y = 0; y < height; y++) {
+          for (int x = 0; x < width; x++) {
+            // NOTE: BufferedImage needs ARGB, rather than RGBA
+            if ((m.get((m_width*y)+(x/8)) & 0x80>>(x%8)) != 0)
+              o.put((byte)255);
+            else
+              o.put((byte)0);
+
+            pf.rgbFromBuffer(o, in.duplicate(), 1);
+
+            o.position(o.reset().position() + 4).mark();
+            in.position(in.position() + pf.bpp/8);
+          }
+        }
+
+        IntBuffer rgb =
+          IntBuffer.allocate(width*height).put(buffer.asIntBuffer());
+        cursor = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
+        cursor.setRGB(0, 0, width, height, rgb.array(), 0, width);
+
+        cursorHotspot = hotspot;
+
+      }
     }
-    return r;
+
+    int cw = (int)Math.floor((float)cursor.getWidth() * scaleRatioX);
+    int ch = (int)Math.floor((float)cursor.getHeight() * scaleRatioY);
+
+    int x = (int)Math.floor((float)cursorHotspot.x * scaleRatioX);
+    int y = (int)Math.floor((float)cursorHotspot.y * scaleRatioY);
+
+    java.awt.Cursor softCursor;
+
+    Dimension cs = tk.getBestCursorSize(cw, ch);
+    if (cs.width != cw && cs.height != ch) {
+      cw = Math.min(cw, cs.width);
+      ch = Math.min(ch, cs.height);
+      x = (int)Math.min(x, Math.max(cs.width - 1, 0));
+      y = (int)Math.min(y, Math.max(cs.height - 1, 0));
+      BufferedImage scaledImage = 
+        new BufferedImage(cs.width, cs.height, BufferedImage.TYPE_INT_ARGB);
+      Graphics2D g2 = scaledImage.createGraphics();
+      g2.setRenderingHint(RenderingHints.KEY_RENDERING,
+                          RenderingHints.VALUE_RENDER_QUALITY);
+      g2.drawImage(cursor,
+                   0, 0, cw, ch,
+                   0, 0, cursor.getWidth(), cursor.getHeight(), null);
+      g2.dispose();
+      java.awt.Point hs = new java.awt.Point(x, y);
+      softCursor = tk.createCustomCursor(scaledImage, hs, "softCursor");
+      scaledImage.flush();
+    } else {
+      java.awt.Point hs = new java.awt.Point(x, y);
+      softCursor = tk.createCustomCursor(cursor, hs, "softCursor");
+    }
+
+    cursor.flush();
+
+    setCursor(softCursor);
+
   }
 
-  public static Window getFullScreenWindow() {
-    GraphicsEnvironment ge =
-      GraphicsEnvironment.getLocalGraphicsEnvironment();
-    GraphicsDevice gd = ge.getDefaultScreenDevice();
-    Window fullScreenWindow = gd.getFullScreenWindow();
-    return fullScreenWindow;
+  public void resize(int x, int y, int w, int h) {
+    if ((w != frameBuffer.width()) || (h != frameBuffer.height())) {
+      vlog.debug("Resizing framebuffer from "+frameBuffer.width()+"x"+
+                 frameBuffer.height()+" to "+w+"x"+h);
+      frameBuffer = createFramebuffer(frameBuffer.getPF(), w, h);
+      assert(frameBuffer != null);
+      cc.setFramebuffer(frameBuffer);
+      image = null;
+    }
+    setScaledSize(w, h);
   }
 
-  public static void setFullScreenWindow(Window fullScreenWindow) {
-    GraphicsEnvironment ge =
-      GraphicsEnvironment.getLocalGraphicsEnvironment();
-    GraphicsDevice gd = ge.getDefaultScreenDevice();
-    if (gd.isFullScreenSupported())
-      gd.setFullScreenWindow(fullScreenWindow);
+  private PlatformPixelBuffer createFramebuffer(PixelFormat pf, int w, int h)
+  {
+    PlatformPixelBuffer fb;
+
+    fb = new JavaPixelBuffer(w, h);
+
+    return fb;
   }
 
-  CConn cc;
-  JScrollPane sp;
-  boolean canDoLionFS;
-  static LogWriter vlog = new LogWriter("Viewport");
+  //
+  // Callback methods to determine geometry of our Component.
+  //
+
+  public Dimension getPreferredSize() {
+    return new Dimension(scaledWidth, scaledHeight);
+  }
+
+  public Dimension getMinimumSize() {
+    return new Dimension(scaledWidth, scaledHeight);
+  }
+
+  public Dimension getMaximumSize() {
+    return new Dimension(scaledWidth, scaledHeight);
+  }
+
+  public void paintComponent(Graphics g) {
+    Graphics2D g2 = (Graphics2D)g;
+    if (cc.cp.width != scaledWidth ||
+        cc.cp.height != scaledHeight) {
+      g2.setRenderingHint(RenderingHints.KEY_RENDERING,
+                          RenderingHints.VALUE_RENDER_QUALITY);
+      g2.drawImage(image, 0, 0, scaledWidth, scaledHeight, null);
+    } else {
+      g2.drawImage(image, 0, 0, null);
+    }
+    g2.dispose();
+  }
+
+  // Mouse-Motion callback function
+  private void mouseMotionCB(MouseEvent e) {
+    if (!viewOnly.getValue() &&
+        e.getX() >= 0 && e.getX() <= scaledWidth &&
+        e.getY() >= 0 && e.getY() <= scaledHeight)
+      cc.writePointerEvent(translateMouseEvent(e));
+  }
+  public void mouseDragged(MouseEvent e) { mouseMotionCB(e); }
+  public void mouseMoved(MouseEvent e) { mouseMotionCB(e); }
+
+  // Mouse callback function
+  private void mouseCB(MouseEvent e) {
+    if (!viewOnly.getValue())
+      if ((e.getID() == MouseEvent.MOUSE_RELEASED) ||
+          (e.getX() >= 0 && e.getX() <= scaledWidth &&
+           e.getY() >= 0 && e.getY() <= scaledHeight))
+        cc.writePointerEvent(translateMouseEvent(e));
+  }
+  public void mouseReleased(MouseEvent e) { mouseCB(e); }
+  public void mousePressed(MouseEvent e) { mouseCB(e); }
+  public void mouseClicked(MouseEvent e) {}
+  public void mouseEntered(MouseEvent e) {
+    if (embed.getValue())
+      requestFocus();
+  }
+  public void mouseExited(MouseEvent e) {}
+
+  // MouseWheel callback function
+  private void mouseWheelCB(MouseWheelEvent e) {
+    if (!viewOnly.getValue())
+      cc.writeWheelEvent(e);
+  }
+
+  public void mouseWheelMoved(MouseWheelEvent e) {
+    mouseWheelCB(e);
+  }
+
+  private static final Integer keyEventLock = 0; 
+
+  // Handle the key-typed event.
+  public void keyTyped(KeyEvent e) { }
+
+  // Handle the key-released event.
+  public void keyReleased(KeyEvent e) {
+    synchronized(keyEventLock) {
+      cc.writeKeyEvent(e);
+    }
+  }
+
+  // Handle the key-pressed event.
+  public void keyPressed(KeyEvent e)
+  {
+    if (e.getKeyCode() == MenuKey.getMenuKeyCode()) {
+      java.awt.Point pt = e.getComponent().getMousePosition();
+      if (pt != null) {
+        F8Menu menu = new F8Menu(cc);
+        menu.show(e.getComponent(), (int)pt.getX(), (int)pt.getY());
+      }
+      return;
+    }
+    int ctrlAltShiftMask = Event.SHIFT_MASK | Event.CTRL_MASK | Event.ALT_MASK;
+    if ((e.getModifiers() & ctrlAltShiftMask) == ctrlAltShiftMask) {
+      switch (e.getKeyCode()) {
+        case KeyEvent.VK_A:
+          VncViewer.showAbout(this);
+          return;
+        case KeyEvent.VK_F:
+          if (cc.desktop.fullscreen_active())
+            cc.desktop.fullscreen_on();
+          else
+            cc.desktop.fullscreen_off();
+          return;
+        case KeyEvent.VK_H:
+          cc.refresh();
+          return;
+        case KeyEvent.VK_I:
+          cc.showInfo();
+          return;
+        case KeyEvent.VK_O:
+            OptionsDialog.showDialog(this);
+          return;
+        case KeyEvent.VK_W:
+          VncViewer.newViewer();
+          return;
+        case KeyEvent.VK_LEFT:
+        case KeyEvent.VK_RIGHT:
+        case KeyEvent.VK_UP:
+        case KeyEvent.VK_DOWN:
+          return;
+      }
+    }
+    if ((e.getModifiers() & Event.META_MASK) == Event.META_MASK) {
+      switch (e.getKeyCode()) {
+        case KeyEvent.VK_COMMA:
+        case KeyEvent.VK_N:
+        case KeyEvent.VK_W:
+        case KeyEvent.VK_I:
+        case KeyEvent.VK_R:
+        case KeyEvent.VK_L:
+        case KeyEvent.VK_F:
+        case KeyEvent.VK_Z:
+        case KeyEvent.VK_T:
+          return;
+      }
+    }
+    synchronized(keyEventLock) {
+      cc.writeKeyEvent(e);
+    }
+  }
+
+  public void setScaledSize(int width, int height)
+  {
+    assert(width != 0 && height != 0);
+    String scaleString = scalingFactor.getValue();
+    if (remoteResize.getValue()) {
+      scaledWidth = width;
+      scaledHeight = height;
+      scaleRatioX = 1.00f;
+      scaleRatioY = 1.00f;
+    } else {
+      if (scaleString.matches("^[0-9]+$")) {
+        int scalingFactor = Integer.parseInt(scaleString);
+        scaledWidth =
+          (int)Math.floor((float)width * (float)scalingFactor/100.0);
+        scaledHeight =
+          (int)Math.floor((float)height * (float)scalingFactor/100.0);
+      } else if (scaleString.equalsIgnoreCase("Auto")) {
+        scaledWidth = width;
+        scaledHeight = height;
+      } else {
+        float widthRatio = (float)width / (float)cc.cp.width;
+        float heightRatio = (float)height / (float)cc.cp.height;
+        float ratio = Math.min(widthRatio, heightRatio);
+        scaledWidth = (int)Math.floor(cc.cp.width * ratio);
+        scaledHeight = (int)Math.floor(cc.cp.height * ratio);
+      }
+      scaleRatioX = (float)scaledWidth / (float)cc.cp.width;
+      scaleRatioY = (float)scaledHeight / (float)cc.cp.height;
+    }
+    if (scaledWidth != getWidth() || scaledHeight != getHeight())
+      setSize(new Dimension(scaledWidth, scaledHeight));
+  }
+
+  private MouseEvent translateMouseEvent(MouseEvent e)
+  {
+    if (cc.cp.width != scaledWidth ||
+        cc.cp.height != scaledHeight) {
+      int sx = (scaleRatioX == 1.00) ?
+        e.getX() : (int)Math.floor(e.getX() / scaleRatioX);
+      int sy = (scaleRatioY == 1.00) ?
+        e.getY() : (int)Math.floor(e.getY() / scaleRatioY);
+      e.translatePoint(sx - e.getX(), sy - e.getY());
+    }
+    return e;
+  }
+
+  public void handleOptions()
+  {
+    /*
+    setScaledSize(cc.cp.width, cc.cp.height);
+    if (!oldSize.equals(new Dimension(scaledWidth, scaledHeight))) {
+    // Re-layout the DesktopWindow when the scaled size changes.
+    // Ideally we'd do this with a ComponentListener, but unfortunately
+    // sometimes a spurious resize event is triggered on the viewport
+    // when the DesktopWindow is manually resized via the drag handles.
+    if (cc.desktop != null && cc.desktop.isVisible()) {
+      JScrollPane scroll = (JScrollPane)((JViewport)getParent()).getParent();
+      scroll.setViewportBorder(BorderFactory.createEmptyBorder(0,0,0,0));
+      cc.desktop.pack();
+    }
+    */
+  }
+
+  // access to cc by different threads is specified in CConn
+  private CConn cc;
+  private BufferedImage image;
+
+  // access to the following must be synchronized:
+  public PlatformPixelBuffer frameBuffer;
+
+  static Toolkit tk = Toolkit.getDefaultToolkit();
+
+  public int scaledWidth = 0, scaledHeight = 0;
+  float scaleRatioX, scaleRatioY;
+
+  BufferedImage cursor;
+  Point cursorHotspot = new Point();
+
 }
-
diff --git a/java/com/tigervnc/vncviewer/VncViewer.java b/java/com/tigervnc/vncviewer/VncViewer.java
index fc9c7b5..d690cc2 100644
--- a/java/com/tigervnc/vncviewer/VncViewer.java
+++ b/java/com/tigervnc/vncviewer/VncViewer.java
@@ -1,7 +1,7 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
  * Copyright 2011 Pierre Ossman <ossman@cendio.se> for Cendio AB
  * Copyright (C) 2011-2013 D. R. Commander.  All Rights Reserved.
- * Copyright (C) 2011-2015 Brian P. Hinz
+ * Copyright (C) 2011-2016 Brian P. Hinz
  *
  * This is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -35,29 +35,38 @@
 import java.awt.Color;
 import java.awt.Graphics;
 import java.awt.Image;
+import java.io.BufferedReader;
 import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.IOException;
 import java.io.File;
 import java.lang.Character;
 import java.lang.reflect.*;
+import java.net.URL;
+import java.nio.CharBuffer;
+import java.util.*;
 import java.util.jar.Attributes;
 import java.util.jar.Manifest;
-import java.util.*;
 import javax.swing.*;
+import javax.swing.border.*;
 import javax.swing.plaf.FontUIResource;
+import javax.swing.SwingUtilities;
 import javax.swing.UIManager.*;
 
 import com.tigervnc.rdr.*;
 import com.tigervnc.rfb.*;
 import com.tigervnc.network.*;
 
+import static com.tigervnc.vncviewer.Parameters.*;
+
 public class VncViewer extends javax.swing.JApplet 
   implements Runnable, ActionListener {
 
-  public static final String aboutText = new String("TigerVNC Java Viewer v%s (%s)%n"+
-                                                    "Built on %s at %s%n"+
-                                                    "Copyright (C) 1999-2016 TigerVNC Team and many others (see README.txt)%n"+
-                                                    "See http://www.tigervnc.org for information on TigerVNC.");
+  public static final String aboutText =
+    new String("TigerVNC Java Viewer v%s (%s)%n"+
+               "Built on %s at %s%n"+
+               "Copyright (C) 1999-2016 TigerVNC Team and many others (see README.txt)%n"+
+               "See http://www.tigervnc.org for information on TigerVNC.");
 
   public static String version = null;
   public static String build = null;
@@ -73,6 +82,11 @@
     VncViewer.class.getResourceAsStream("timestamp");
   public static final String os = 
     System.getProperty("os.name").toLowerCase();
+  private static VncViewer applet;
+
+  private String defaultServerName;
+  int VNCSERVERNAMELEN = 64;
+  CharBuffer vncServerName = CharBuffer.allocate(VNCSERVERNAMELEN);
 
   public static void setLookAndFeel() {
     try {
@@ -133,12 +147,12 @@
     viewer.start();
   }
 
+  public VncViewer() {
+    // Only called in applet mode
+    this(new String[0]);
+  }
 
   public VncViewer(String[] argv) {
-    embed.setParam(false);
-
-    // load user preferences
-    UserPreferences.load("global");
 
     SecurityClient.setDefaults();
 
@@ -150,14 +164,23 @@
 
     Configuration.enableViewerParams();
 
+    /* Load the default parameter settings */
+    try {
+      defaultServerName = loadViewerParameters(null);
+    } catch (com.tigervnc.rfb.Exception e) {
+      defaultServerName = "";
+      vlog.info(e.getMessage());
+    }
+
     // Override defaults with command-line options
     for (int i = 0; i < argv.length; i++) {
       if (argv[i].length() == 0)
         continue;
 
       if (argv[i].equalsIgnoreCase("-config")) {
-        if (++i >= argv.length) usage();
-          Configuration.load(argv[i]);
+        if (++i >= argv.length)
+          usage();
+        defaultServerName = loadViewerParameters(argv[i]);
         continue;
       }
 
@@ -181,28 +204,9 @@
         usage();
       }
 
-      if (vncServerName.getValue() != null)
-        usage();
-      vncServerName.setParam(argv[i]);
+      vncServerName.put(argv[i].toCharArray()).flip();
     }
 
-    if (!autoSelect.hasBeenSet()) {
-      // Default to AutoSelect=0 if -PreferredEncoding or -FullColor is used
-      autoSelect.setParam(!preferredEncoding.hasBeenSet() &&
-                          !fullColour.hasBeenSet() &&
-                          !fullColourAlias.hasBeenSet());
-    }
-    if (!fullColour.hasBeenSet() && !fullColourAlias.hasBeenSet()) {
-      // Default to FullColor=0 if AutoSelect=0 && LowColorLevel is set
-      if (!autoSelect.getValue() && (lowColourLevel.hasBeenSet() ||
-                          lowColourLevelAlias.hasBeenSet())) {
-        fullColour.setParam(false);
-      }
-    }
-    if (!customCompressLevel.hasBeenSet()) {
-      // Default to CustomCompressLevel=1 if CompressLevel is used.
-      customCompressLevel.setParam(compressLevel.hasBeenSet());
-    }
   }
 
   public static void usage() {
@@ -272,26 +276,27 @@
     System.exit(1);
   }
 
-  public VncViewer() {
-    UserPreferences.load("global");
-    embed.setParam(true);
-  }
-
-  public static void newViewer(VncViewer oldViewer, Socket sock, boolean close) {
-    VncViewer viewer = new VncViewer();
-    viewer.embed.setParam(oldViewer.embed.getValue());
-    viewer.sock = sock;
-    viewer.start();
-    if (close)
-      oldViewer.exit(0);
-  }
-
-  public static void newViewer(VncViewer oldViewer, Socket sock) {
-    newViewer(oldViewer, sock, false);
-  }
-
-  public static void newViewer(VncViewer oldViewer) {
-    newViewer(oldViewer, null);
+  public static void newViewer() {
+    String cmd = "java -jar ";
+    try {
+      URL url =
+        VncViewer.class.getProtectionDomain().getCodeSource().getLocation();
+      File f = new File(url.toURI());
+      if (!f.exists() || !f.canRead()) {
+        String msg = new String("The jar file "+f.getAbsolutePath()+
+                                " does not exist or cannot be read.");
+        JOptionPane.showMessageDialog(null, msg, "ERROR",
+                                      JOptionPane.ERROR_MESSAGE);
+        return;
+      }
+      cmd = cmd.concat(f.getAbsolutePath());
+      Thread t = new Thread(new ExtProcess(cmd, vlog));
+      t.start();
+    } catch (java.net.URISyntaxException e) {
+      vlog.info(e.getMessage());
+    } catch (java.lang.Exception e) {
+      vlog.info(e.getMessage());
+    }
   }
 
   public boolean isAppletDragStart(MouseEvent e) {
@@ -311,7 +316,7 @@
 
   public void appletDragStarted() {
     embed.setParam(false);
-    cc.recreateViewport();
+    //cc.recreateViewport();
     JFrame f = (JFrame)JOptionPane.getFrameForComponent(this);
     // The default JFrame created by the drag event will be
     // visible briefly between appletDragStarted and Finished.
@@ -320,7 +325,6 @@
   }
 
   public void appletDragFinished() {
-    cc.setEmbeddedFeatures(true);
     JFrame f = (JFrame)JOptionPane.getFrameForComponent(this);
     if (f != null)
       f.dispose();
@@ -331,24 +335,70 @@
   }
 
   public void appletRestored() {
-    cc.setEmbeddedFeatures(false);
     cc.setCloseListener(null);
   }
 
-  public void init() {
-    vlog.debug("init called");
-    Container parent = getParent();
-    while (!parent.isFocusCycleRoot()) {
-      parent = parent.getParent();
+  public static void setupEmbeddedFrame(JScrollPane sp) {
+    InputMap im = sp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
+    int ctrlAltShiftMask = Event.SHIFT_MASK | Event.CTRL_MASK | Event.ALT_MASK;
+    if (im != null) {
+      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, ctrlAltShiftMask),
+             "unitScrollUp");
+      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, ctrlAltShiftMask),
+             "unitScrollDown");
+      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, ctrlAltShiftMask),
+             "unitScrollLeft");
+      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, ctrlAltShiftMask),
+             "unitScrollRight");
+      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, ctrlAltShiftMask),
+             "scrollUp");
+      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, ctrlAltShiftMask),
+             "scrollDown");
+      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, ctrlAltShiftMask),
+             "scrollLeft");
+      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, ctrlAltShiftMask),
+             "scrollRight");
     }
-    ((Frame)parent).setModalExclusionType(null);
-    parent.setFocusable(false);
-    parent.setFocusTraversalKeysEnabled(false);
-    setLookAndFeel();
-    setBackground(Color.white);
+    applet.getContentPane().removeAll();
+    applet.getContentPane().add(sp);
+    applet.validate();
   }
 
-  private void getTimestamp() {
+  public void init() {
+    // Called right after zero-arg constructor in applet mode
+    setLookAndFeel();
+    setBackground(Color.white);
+    applet = this;
+    vncServerName.put(loadAppletParameters(applet).toCharArray()).flip();
+    if (embed.getValue()) {
+      fullScreen.setParam(false);
+      remoteResize.setParam(false);
+      maximize.setParam(false);
+      scalingFactor.setParam("100");
+    }
+    setFocusTraversalKeysEnabled(false);
+    addFocusListener(new FocusAdapter() {
+      public void focusGained(FocusEvent e) {
+        if (cc != null && cc.desktop != null)
+          cc.desktop.viewport.requestFocusInWindow();
+      }
+    });
+    Frame frame = (Frame)getFocusCycleRootAncestor();
+    frame.setFocusTraversalKeysEnabled(false);
+    frame.addWindowListener(new WindowAdapter() {
+      // Transfer focus to scrollpane when browser receives it
+      public void windowActivated(WindowEvent e) {
+        if (cc != null && cc.desktop != null)
+          cc.desktop.viewport.requestFocusInWindow();
+      }
+      public void windowDeactivated(WindowEvent e) {
+        if (cc != null)
+          cc.releaseDownKeys();
+      }
+    });
+  }
+
+  private static void getTimestamp() {
     if (version == null || build == null) {
       try {
         Manifest manifest = new Manifest(timestamp);
@@ -361,31 +411,40 @@
     }
   }
 
+  public static void showAbout(Container parent) {
+    String pkgDate = "";
+    String pkgTime = "";
+    try {
+      Manifest manifest = new Manifest(VncViewer.timestamp);
+      Attributes attributes = manifest.getMainAttributes();
+      pkgDate = attributes.getValue("Package-Date");
+      pkgTime = attributes.getValue("Package-Time");
+    } catch (java.lang.Exception e) { }
+
+    Window fullScreenWindow = DesktopWindow.getFullScreenWindow();
+    if (fullScreenWindow != null)
+      DesktopWindow.setFullScreenWindow(null);
+    String msg =
+      String.format(VncViewer.aboutText, VncViewer.version, VncViewer.build,
+                    VncViewer.buildDate, VncViewer.buildTime);
+    Object[] options = {"Close  \u21B5"};
+    JOptionPane op =
+      new JOptionPane(msg, JOptionPane.INFORMATION_MESSAGE,
+                      JOptionPane.DEFAULT_OPTION, VncViewer.logoIcon, options);
+    JDialog dlg = op.createDialog(parent, "About TigerVNC Viewer for Java");
+    dlg.setIconImage(VncViewer.frameIcon);
+    dlg.setAlwaysOnTop(true);
+    dlg.setVisible(true);
+    if (fullScreenWindow != null)
+      DesktopWindow.setFullScreenWindow(fullScreenWindow);
+  }
+
   public void start() {
-    vlog.debug("start called");
-    getTimestamp();
-    if (embed.getValue() && nViewers == 0) {
-      alwaysShowServerDialog.setParam(false);
-      Configuration.global().readAppletParams(this);
-      fullScreen.setParam(false);
-      scalingFactor.setParam("100");
-      String host = getCodeBase().getHost();
-      if (vncServerName.getValue() == null && vncServerPort.getValue() != 0) {
-        int port = vncServerPort.getValue();
-        vncServerName.setParam(host + ((port >= 5900 && port <= 5999)
-                                       ? (":"+(port-5900))
-                                       : ("::"+port)));
-      }
-    }
-    nViewers++;
     thread = new Thread(this);
     thread.start();
   }
 
   public void exit(int n) {
-    nViewers--;
-    if (nViewers > 0)
-      return;
     if (embed.getValue())
       destroy();
     else
@@ -425,16 +484,26 @@
     }
   }
 
-  CConn cc;
   public void run() {
     cc = null;
+    Socket sock = null;
+
+    /* Specifying -via and -listen together is nonsense */
+    if (listenMode.getValue() && !via.getValueStr().isEmpty()) {
+      vlog.error("Parameters -listen and -via are incompatible");
+      String msg =
+        new String("Parameters -listen and -via are incompatible");
+      JOptionPane.showMessageDialog(null, msg, "ERROR",
+                                    JOptionPane.ERROR_MESSAGE);
+      exit(1);
+    }
 
     if (listenMode.getValue()) {
       int port = 5500;
 
-      if (vncServerName.getValue() != null &&
-          Character.isDigit(vncServerName.getValue().charAt(0)))
-        port = Integer.parseInt(vncServerName.getValue());
+      if (vncServerName.charAt(0) != 0 &&
+          Character.isDigit(vncServerName.charAt(0)))
+        port = Integer.parseInt(vncServerName.toString());
 
       TcpListener listener = null;
       try {
@@ -446,15 +515,25 @@
 
       vlog.info("Listening on port "+port);
 
-      while (true) {
-        Socket new_sock = listener.accept();
-        if (new_sock != null)
-          newViewer(this, new_sock, true);
+      while (sock == null)
+        sock = listener.accept();
+    } else {
+      if (vncServerName.charAt(0) == 0) {
+        try {
+          SwingUtilities.invokeAndWait(
+            new ServerDialog(defaultServerName, vncServerName));
+        } catch (InvocationTargetException e) {
+          reportException(e);
+        } catch (InterruptedException e) {
+          reportException(e);
+        }
+        if (vncServerName.charAt(0) == 0)
+          exit(0);
       }
     }
 
     try {
-      cc = new CConn(this, sock, vncServerName.getValue());
+      cc = new CConn(vncServerName.toString(), sock);
       while (!cc.shuttingDown)
         cc.processMsg();
       exit(0);
@@ -462,7 +541,7 @@
       if (cc == null || !cc.shuttingDown) {
         reportException(e);
         if (cc != null)
-          cc.deleteWindow();
+          cc.close();
       } else if (embed.getValue()) {
         reportException(new java.lang.Exception("Connection closed"));
         exit(0);
@@ -471,243 +550,11 @@
     }
   }
 
-  static BoolParameter noLionFS
-  = new BoolParameter("NoLionFS",
-  "On Mac systems, setting this parameter will force the use of the old "+
-  "(pre-Lion) full-screen mode, even if the viewer is running on OS X 10.7 "+
-  "Lion or later.",
-  false);
-
-  BoolParameter embed
-  = new BoolParameter("Embed",
-  "If the viewer is being run as an applet, display its output to " +
-  "an embedded frame in the browser window rather than to a dedicated " +
-  "window. Embed=1 implies FullScreen=0 and Scale=100.",
-  false);
-
-  BoolParameter useLocalCursor
-  = new BoolParameter("UseLocalCursor",
-                      "Render the mouse cursor locally",
-                      true);
-  BoolParameter sendLocalUsername
-  = new BoolParameter("SendLocalUsername",
-                      "Send the local username for SecurityTypes "+
-                      "such as Plain rather than prompting",
-                      true);
-  StringParameter passwordFile
-  = new StringParameter("PasswordFile",
-                        "Password file for VNC authentication",
-                        "");
-  AliasParameter passwd
-  = new AliasParameter("passwd",
-                       "Alias for PasswordFile",
-                       passwordFile);
-  BoolParameter autoSelect
-  = new BoolParameter("AutoSelect",
-                      "Auto select pixel format and encoding",
-                      true);
-  BoolParameter fullColour
-  = new BoolParameter("FullColour",
-                      "Use full colour - otherwise 6-bit colour is "+
-                      "used until AutoSelect decides the link is "+
-                      "fast enough",
-                      true);
-  AliasParameter fullColourAlias
-  = new AliasParameter("FullColor",
-                       "Alias for FullColour",
-                       fullColour);
-  IntParameter lowColourLevel
-  = new IntParameter("LowColorLevel",
-                     "Color level to use on slow connections. "+
-                     "0 = Very Low (8 colors), 1 = Low (64 colors), "+
-                     "2 = Medium (256 colors)",
-                     2);
-  AliasParameter lowColourLevelAlias
-  = new AliasParameter("LowColourLevel",
-                       "Alias for LowColorLevel",
-                       lowColourLevel);
-  StringParameter preferredEncoding
-  = new StringParameter("PreferredEncoding",
-                        "Preferred encoding to use (Tight, ZRLE, "+
-                        "hextile or raw) - implies AutoSelect=0",
-                        "Tight");
-  BoolParameter viewOnly
-  = new BoolParameter("ViewOnly",
-                      "Don't send any mouse or keyboard events to "+
-                      "the server",
-                      false);
-  BoolParameter shared
-  = new BoolParameter("Shared",
-                      "Don't disconnect other viewers upon "+
-                      "connection - share the desktop instead",
-                      false);
-  BoolParameter fullScreen
-  = new BoolParameter("FullScreen",
-                      "Full Screen Mode",
-                      false);
-  BoolParameter fullScreenAllMonitors
-  = new BoolParameter("FullScreenAllMonitors",
-                      "Enable full screen over all monitors",
-                      true);
-  BoolParameter acceptClipboard
-  = new BoolParameter("AcceptClipboard",
-                      "Accept clipboard changes from the server",
-                      true);
-  BoolParameter sendClipboard
-  = new BoolParameter("SendClipboard",
-                      "Send clipboard changes to the server",
-                      true);
-  static IntParameter maxCutText
-  = new IntParameter("MaxCutText",
-                     "Maximum permitted length of an outgoing clipboard update",
-                     262144);
-  StringParameter menuKey
-  = new StringParameter("MenuKey",
-                        "The key which brings up the popup menu",
-                        "F8");
-  StringParameter desktopSize
-  = new StringParameter("DesktopSize",
-                        "Reconfigure desktop size on the server on "+
-                        "connect (if possible)", "");
-  BoolParameter listenMode
-  = new BoolParameter("listen",
-                      "Listen for connections from VNC servers",
-                      false);
-  StringParameter scalingFactor
-  = new StringParameter("ScalingFactor",
-                        "Reduce or enlarge the remote desktop image. "+
-                        "The value is interpreted as a scaling factor "+
-                        "in percent. If the parameter is set to "+
-                        "\"Auto\", then automatic scaling is "+
-                        "performed. Auto-scaling tries to choose a "+
-                        "scaling factor in such a way that the whole "+
-                        "remote desktop will fit on the local screen. "+
-                        "If the parameter is set to \"FixedRatio\", "+
-                        "then automatic scaling is performed, but the "+
-                        "original aspect ratio is preserved.",
-                        "100");
-  BoolParameter alwaysShowServerDialog
-  = new BoolParameter("AlwaysShowServerDialog",
-                      "Always show the server dialog even if a server "+
-                      "has been specified in an applet parameter or on "+
-                      "the command line",
-                      false);
-  StringParameter vncServerName
-  = new StringParameter("Server",
-                        "The VNC server <host>[:<dpyNum>] or "+
-                        "<host>::<port>",
-                        null);
-  IntParameter vncServerPort
-  = new IntParameter("Port",
-                     "The VNC server's port number, assuming it is on "+
-                     "the host from which the applet was downloaded",
-                     0);
-  BoolParameter acceptBell
-  = new BoolParameter("AcceptBell",
-                      "Produce a system beep when requested to by the server.",
-                      true);
-  StringParameter via
-  = new StringParameter("Via",
-    "Automatically create an encrypted TCP tunnel to "+
-    "the gateway machine, then connect to the VNC host "+
-    "through that tunnel. By default, this option invokes "+
-    "SSH local port forwarding using the embedded JSch "+
-    "client, however an external SSH client may be specified "+
-    "using the \"-extSSH\" parameter. Note that when using "+
-    "the -via option, the VNC host machine name should be "+
-    "specified from the point of view of the gateway machine, "+
-    "e.g. \"localhost\" denotes the gateway, "+
-    "not the machine on which the viewer was launched. "+
-    "See the System Properties section below for "+
-    "information on configuring the -Via option.", null);
-  BoolParameter tunnel
-  = new BoolParameter("Tunnel",
-    "The -Tunnel command is basically a shorthand for the "+
-    "-via command when the VNC server and SSH gateway are "+
-    "one and the same. -Tunnel creates an SSH connection "+
-    "to the server and forwards the VNC through the tunnel "+
-    "without the need to specify anything else.", false);
-  BoolParameter extSSH
-  = new BoolParameter("extSSH",
-    "By default, SSH tunneling uses the embedded JSch client "+
-    "for tunnel creation. This option causes the client to "+
-    "invoke an external SSH client application for all tunneling "+
-    "operations. By default, \"/usr/bin/ssh\" is used, however "+
-    "the path to the external application may be specified using "+
-    "the -SSHClient option.", false);
-  StringParameter extSSHClient
-  = new StringParameter("extSSHClient",
-    "Specifies the path to an external SSH client application "+
-    "that is to be used for tunneling operations when the -extSSH "+
-    "option is in effect.", "/usr/bin/ssh");
-  StringParameter extSSHArgs
-  = new StringParameter("extSSHArgs",
-    "Specifies the arguments string or command template to be used "+
-    "by the external SSH client application when the -extSSH option "+
-    "is in effect. The string will be processed according to the same "+
-    "pattern substitution rules as the VNC_TUNNEL_CMD and VNC_VIA_CMD "+
-    "system properties, and can be used to override those in a more "+
-    "command-line friendly way. If not specified, then the appropriate "+
-    "VNC_TUNNEL_CMD or VNC_VIA_CMD command template will be used.", null);
-  StringParameter sshConfig
-  = new StringParameter("SSHConfig",
-    "Specifies the path to an OpenSSH configuration file that to "+
-    "be parsed by the embedded JSch SSH client during tunneling "+
-    "operations.", FileUtils.getHomeDir()+".ssh/config");
-  StringParameter sshKey
-  = new StringParameter("SSHKey",
-    "When using the Via or Tunnel options with the embedded SSH client, "+
-    "this parameter specifies the text of the SSH private key to use when "+
-    "authenticating with the SSH server. You can use \\n within the string "+
-    "to specify a new line.", null);
-  StringParameter sshKeyFile
-  = new StringParameter("SSHKeyFile",
-    "When using the Via or Tunnel options with the embedded SSH client, "+
-    "this parameter specifies a file that contains an SSH private key "+
-    "(or keys) to use when authenticating with the SSH server. If not "+
-    "specified, ~/.ssh/id_dsa or ~/.ssh/id_rsa will be used (if they exist). "+
-    "Otherwise, the client will fallback to prompting for an SSH password.",
-    null);
-  StringParameter sshKeyPass
-  = new StringParameter("SSHKeyPass",
-    "When using the Via or Tunnel options with the embedded SSH client, "+
-    "this parameter specifies the passphrase for the SSH key.", null);
-  BoolParameter customCompressLevel
-  = new BoolParameter("CustomCompressLevel",
-                      "Use custom compression level. "+
-                      "Default if CompressLevel is specified.",
-                      false);
-  IntParameter compressLevel
-  = new IntParameter("CompressLevel",
-                     "Use specified compression level "+
-                     "0 = Low, 6 = High",
-                     1);
-  BoolParameter noJpeg
-  = new BoolParameter("NoJPEG",
-                      "Disable lossy JPEG compression in Tight encoding.",
-                      false);
-  IntParameter qualityLevel
-  = new IntParameter("QualityLevel",
-                     "JPEG quality level. "+
-                     "0 = Low, 9 = High",
-                     8);
-  StringParameter x509ca
-  = new StringParameter("X509CA",
-    "Path to CA certificate to use when authenticating remote servers "+
-    "using any of the X509 security schemes (X509None, X509Vnc, etc.). "+
-    "Must be in PEM format.",
-    FileUtils.getHomeDir()+".vnc/x509_ca.pem");
-  StringParameter x509crl
-  = new StringParameter("X509CRL",
-    "Path to certificate revocation list to use in conjunction with "+
-    "-X509CA. Must also be in PEM format.",
-    FileUtils.getHomeDir()+".vnc/x509_crl.pem");
-  StringParameter config
-  = new StringParameter("config",
+  public static CConn cc;
+  public static StringParameter config
+  = new StringParameter("Config",
   "Specifies a configuration file to load.", null);
 
   Thread thread;
-  Socket sock;
-  static int nViewers;
   static LogWriter vlog = new LogWriter("VncViewer");
 }
diff --git a/po/fr.po b/po/fr.po
index 260e9e9..db708e3 100644
--- a/po/fr.po
+++ b/po/fr.po
@@ -3,127 +3,119 @@
 # This file is distributed under the same license as the tigervnc package.
 #
 # Alain Portal <alain.portal@free.fr>, 2010
-# Stéphane Aulery <lkppo@free.fr>, 2015.
+# Stéphane Aulery <lkppo@free.fr>, 2015-2016.
 #
 # Traduction complète et relecture, S. Aulery, 25-04-2015.
+# Mise à jour, S. Aulery, 23-12-2016.
 #
 msgid ""
 msgstr ""
-"Project-Id-Version: tigervnc 1.5.90\n"
+"Project-Id-Version: tigervnc 1.6.90\n"
 "Report-Msgid-Bugs-To: tigervnc-devel@googlegroups.com\n"
-"POT-Creation-Date: 2015-11-26 11:33+0000\n"
-"PO-Revision-Date: 2015-12-23 01:30+0100\n"
+"POT-Creation-Date: 2016-07-01 10:15+0000\n"
+"PO-Revision-Date: 2016-12-23 20:56+0100\n"
 "Last-Translator: Stéphane Aulery <lkppo@free.fr>\n"
 "Language-Team: French <traduc@traduc.org>\n"
 "Language: fr\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
+"X-Bugs: Report translation errors to the Language-Team address.\n"
 "X-Generator: Lokalize 1.0\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
-#: vncviewer/CConn.cxx:111
+#: vncviewer/CConn.cxx:110
 #, c-format
 msgid "connected to host %s port %d"
 msgstr "connecté à l’hôte %s par le port %d"
 
-#: vncviewer/CConn.cxx:173
+#: vncviewer/CConn.cxx:169
 #, c-format
 msgid "Desktop name: %.80s"
 msgstr "Nom du bureau : %.80s"
 
-#: vncviewer/CConn.cxx:178
+#: vncviewer/CConn.cxx:174
 #, c-format
 msgid "Host: %.80s port: %d"
 msgstr "Hôte : %.80s port : %d"
 
-#: vncviewer/CConn.cxx:183
+#: vncviewer/CConn.cxx:179
 #, c-format
 msgid "Size: %d x %d"
 msgstr "Taille : %d x %d"
 
-#: vncviewer/CConn.cxx:191
+#: vncviewer/CConn.cxx:187
 #, c-format
 msgid "Pixel format: %s"
 msgstr "Format de pixel : %s"
 
-#: vncviewer/CConn.cxx:198
+#: vncviewer/CConn.cxx:194
 #, c-format
 msgid "(server default %s)"
 msgstr "(serveur par défaut %s)"
 
-#: vncviewer/CConn.cxx:203
+#: vncviewer/CConn.cxx:199
 #, c-format
 msgid "Requested encoding: %s"
 msgstr "Encodage demandé : %s"
 
-#: vncviewer/CConn.cxx:208
+#: vncviewer/CConn.cxx:204
 #, c-format
 msgid "Last used encoding: %s"
 msgstr "Dernier encodage utilisé : %s"
 
-#: vncviewer/CConn.cxx:213
+#: vncviewer/CConn.cxx:209
 #, c-format
 msgid "Line speed estimate: %d kbit/s"
 msgstr "Vitesse estimée de la connexion : %d kbit/s"
 
-#: vncviewer/CConn.cxx:218
+#: vncviewer/CConn.cxx:214
 #, c-format
 msgid "Protocol version: %d.%d"
 msgstr "Version du protocol : %d.%d"
 
-#: vncviewer/CConn.cxx:223
+#: vncviewer/CConn.cxx:219
 #, c-format
 msgid "Security method: %s"
 msgstr "Méthode de sécurité : %s"
 
-#: vncviewer/CConn.cxx:329
+#: vncviewer/CConn.cxx:319
 #, c-format
 msgid "SetDesktopSize failed: %d"
 msgstr "SetDesktopSize échoué : %d"
 
-#: vncviewer/CConn.cxx:398
+#: vncviewer/CConn.cxx:411
 msgid "Invalid SetColourMapEntries from server!"
 msgstr "Opération SetColourMapEntries du serveur invalide !"
 
-#. TRANSLATORS: Refers to a VNC protocol encoding type
-#: vncviewer/CConn.cxx:444 vncviewer/CConn.cxx:451
-#, c-format
-msgid "Unknown encoding %d"
-msgstr "Encodage inconnu %d"
-
-#: vncviewer/CConn.cxx:445 vncviewer/CConn.cxx:452
-msgid "Unknown encoding"
-msgstr "Encodage inconnu"
-
-#: vncviewer/CConn.cxx:484
+#: vncviewer/CConn.cxx:485
 msgid "Enabling continuous updates"
 msgstr "Autorisation de mises à jour en continu"
 
-#: vncviewer/CConn.cxx:554
+#: vncviewer/CConn.cxx:555
 #, c-format
 msgid "Throughput %d kbit/s - changing to quality %d"
 msgstr "Débit %d kbit/s - %d pour une meilleure qualité"
 
-#: vncviewer/CConn.cxx:576
+#: vncviewer/CConn.cxx:577
 #, c-format
 msgid "Throughput %d kbit/s - full color is now %s"
 msgstr "Débit %d kbit/s - pleine couleur est à présent %s"
 
-#: vncviewer/CConn.cxx:578
+#: vncviewer/CConn.cxx:579
 msgid "disabled"
 msgstr "désactivé"
 
-#: vncviewer/CConn.cxx:578
+#: vncviewer/CConn.cxx:579
 msgid "enabled"
 msgstr "activé"
 
-#: vncviewer/CConn.cxx:588
+#: vncviewer/CConn.cxx:589
 #, c-format
 msgid "Using %s encoding"
 msgstr "Utilise l’encodage %s"
 
-#: vncviewer/CConn.cxx:635
+#: vncviewer/CConn.cxx:636
 #, c-format
 msgid "Using pixel format %s"
 msgstr "Utilisation du format de pixel %s"
@@ -132,25 +124,25 @@
 msgid "Invalid geometry specified!"
 msgstr "Géométrie spécifiée invalide !"
 
-#: vncviewer/DesktopWindow.cxx:309
+#: vncviewer/DesktopWindow.cxx:303
 msgid "Adjusting window size to avoid accidental full screen request"
 msgstr "Ajustement de la taille de la fenêtre pour éviter une demande accidentelle de plein écran"
 
-#: vncviewer/DesktopWindow.cxx:491 vncviewer/DesktopWindow.cxx:497
-#: vncviewer/DesktopWindow.cxx:510
+#: vncviewer/DesktopWindow.cxx:485 vncviewer/DesktopWindow.cxx:491
+#: vncviewer/DesktopWindow.cxx:504
 msgid "Failure grabbing keyboard"
 msgstr "Échec de saisie clavier"
 
-#: vncviewer/DesktopWindow.cxx:522
+#: vncviewer/DesktopWindow.cxx:516
 msgid "Failure grabbing mouse"
 msgstr "Échec de saisie souris"
 
-#: vncviewer/DesktopWindow.cxx:752
+#: vncviewer/DesktopWindow.cxx:746
 msgid "Invalid screen layout computed for resize request!"
 msgstr "Calcul de sortie d’écran invalide pour la demande de redimensionnement !"
 
 #: vncviewer/FLTKPixelBuffer.cxx:33 vncviewer/OSXPixelBuffer.cxx:48
-#: vncviewer/X11PixelBuffer.cxx:113
+#: vncviewer/X11PixelBuffer.cxx:117
 msgid "Not enough memory for framebuffer"
 msgstr "Pas assez de mémoire pour le framebuffer"
 
@@ -167,160 +159,164 @@
 msgstr "Visionneuse VNC : options de la connexion"
 
 #: vncviewer/OptionsDialog.cxx:83 vncviewer/ServerDialog.cxx:91
-#: vncviewer/vncviewer.cxx:268
+#: vncviewer/vncviewer.cxx:282
 msgid "Cancel"
 msgstr "Annuler"
 
-#: vncviewer/OptionsDialog.cxx:88 vncviewer/vncviewer.cxx:267
+#: vncviewer/OptionsDialog.cxx:88 vncviewer/vncviewer.cxx:281
 msgid "OK"
 msgstr "Ok"
 
-#: vncviewer/OptionsDialog.cxx:413
+#: vncviewer/OptionsDialog.cxx:423
 msgid "Compression"
 msgstr "Compression"
 
-#: vncviewer/OptionsDialog.cxx:429
+#: vncviewer/OptionsDialog.cxx:439
 msgid "Auto select"
 msgstr "Sélection automatique"
 
-#: vncviewer/OptionsDialog.cxx:441
+#: vncviewer/OptionsDialog.cxx:451
 msgid "Preferred encoding"
 msgstr "Encodage préféré"
 
-#: vncviewer/OptionsDialog.cxx:489
+#: vncviewer/OptionsDialog.cxx:499
 msgid "Color level"
 msgstr "Niveau de couleur"
 
-#: vncviewer/OptionsDialog.cxx:500
+#: vncviewer/OptionsDialog.cxx:510
 msgid "Full (all available colors)"
 msgstr "Complet (toutes les couleurs disponibles)"
 
-#: vncviewer/OptionsDialog.cxx:507
+#: vncviewer/OptionsDialog.cxx:517
 msgid "Medium (256 colors)"
 msgstr "Moyen (256 couleurs)"
 
-#: vncviewer/OptionsDialog.cxx:514
+#: vncviewer/OptionsDialog.cxx:524
 msgid "Low (64 colors)"
 msgstr "Faible (64 couleurs)"
 
-#: vncviewer/OptionsDialog.cxx:521
+#: vncviewer/OptionsDialog.cxx:531
 msgid "Very low (8 colors)"
 msgstr "Très faible (8 couleurs)"
 
-#: vncviewer/OptionsDialog.cxx:538
+#: vncviewer/OptionsDialog.cxx:548
 msgid "Custom compression level:"
 msgstr "Niveau de compression personnalisé :"
 
-#: vncviewer/OptionsDialog.cxx:544
+#: vncviewer/OptionsDialog.cxx:554
 msgid "level (1=fast, 6=best [4-6 are rarely useful])"
 msgstr "niveau (1=rapide, 6=optimal [4-6 sont rarement utiles])"
 
-#: vncviewer/OptionsDialog.cxx:551
+#: vncviewer/OptionsDialog.cxx:561
 msgid "Allow JPEG compression:"
 msgstr "Autoriser la compression JPEG :"
 
-#: vncviewer/OptionsDialog.cxx:557
+#: vncviewer/OptionsDialog.cxx:567
 msgid "quality (0=poor, 9=best)"
 msgstr "qualité (0=faible, 9=optimale)"
 
-#: vncviewer/OptionsDialog.cxx:568
+#: vncviewer/OptionsDialog.cxx:578
 msgid "Security"
 msgstr "Sécurité"
 
-#: vncviewer/OptionsDialog.cxx:583
+#: vncviewer/OptionsDialog.cxx:593
 msgid "Encryption"
 msgstr "Cryptage"
 
-#: vncviewer/OptionsDialog.cxx:594 vncviewer/OptionsDialog.cxx:647
-#: vncviewer/OptionsDialog.cxx:715
+#: vncviewer/OptionsDialog.cxx:604 vncviewer/OptionsDialog.cxx:657
+#: vncviewer/OptionsDialog.cxx:735
 msgid "None"
 msgstr "Aucun"
 
-#: vncviewer/OptionsDialog.cxx:600
+#: vncviewer/OptionsDialog.cxx:610
 msgid "TLS with anonymous certificates"
 msgstr "TLS avec certificats anonymes"
 
-#: vncviewer/OptionsDialog.cxx:606
+#: vncviewer/OptionsDialog.cxx:616
 msgid "TLS with X509 certificates"
 msgstr "TLS avec certificat X509"
 
-#: vncviewer/OptionsDialog.cxx:613
+#: vncviewer/OptionsDialog.cxx:623
 msgid "Path to X509 CA certificate"
 msgstr "Certificat du chemin pour X509 CA"
 
-#: vncviewer/OptionsDialog.cxx:620
+#: vncviewer/OptionsDialog.cxx:630
 msgid "Path to X509 CRL file"
 msgstr "Fichier du chemin pour X509 CRL"
 
-#: vncviewer/OptionsDialog.cxx:636
+#: vncviewer/OptionsDialog.cxx:646
 msgid "Authentication"
 msgstr "Authentification"
 
-#: vncviewer/OptionsDialog.cxx:653
+#: vncviewer/OptionsDialog.cxx:663
 msgid "Standard VNC (insecure without encryption)"
 msgstr "VNC standard (non sécurisé et sans cryptage)"
 
-#: vncviewer/OptionsDialog.cxx:659
+#: vncviewer/OptionsDialog.cxx:669
 msgid "Username and password (insecure without encryption)"
 msgstr "Nom d’utilisateur et mot de passe (non sécurisé et sans cryptage)"
 
-#: vncviewer/OptionsDialog.cxx:678
+#: vncviewer/OptionsDialog.cxx:688
 msgid "Input"
 msgstr "Entrée"
 
-#: vncviewer/OptionsDialog.cxx:686
+#: vncviewer/OptionsDialog.cxx:696
 msgid "View only (ignore mouse and keyboard)"
 msgstr "Vue passive (ignorer la souris et le clavier)"
 
-#: vncviewer/OptionsDialog.cxx:692
+#: vncviewer/OptionsDialog.cxx:702
 msgid "Accept clipboard from server"
 msgstr "Accepter le presse-papier du serveur"
 
-#: vncviewer/OptionsDialog.cxx:698
+#: vncviewer/OptionsDialog.cxx:709
+msgid "Also set primary selection"
+msgstr "Définir également la sélection principale"
+
+#: vncviewer/OptionsDialog.cxx:716
 msgid "Send clipboard to server"
 msgstr "Envoyer le presse-papier au serveur"
 
-#: vncviewer/OptionsDialog.cxx:704
-msgid "Send primary selection and cut buffer as clipboard"
-msgstr "Envoyer la sélection principale et couper les données vers le presse-papiers"
+#: vncviewer/OptionsDialog.cxx:723
+msgid "Send primary selection as clipboard"
+msgstr "Envoyer la sélection principale vers le presse-papiers"
 
-#: vncviewer/OptionsDialog.cxx:710
+#: vncviewer/OptionsDialog.cxx:730
 msgid "Pass system keys directly to server (full screen)"
 msgstr "Donner directement les clefs système au serveur (plein écran)"
 
-#: vncviewer/OptionsDialog.cxx:713
+#: vncviewer/OptionsDialog.cxx:733
 msgid "Menu key"
 msgstr "Menu clef"
 
-#: vncviewer/OptionsDialog.cxx:729
+#: vncviewer/OptionsDialog.cxx:749
 msgid "Screen"
 msgstr "Écran"
 
-#: vncviewer/OptionsDialog.cxx:737
+#: vncviewer/OptionsDialog.cxx:757
 msgid "Resize remote session on connect"
 msgstr "Redimensionner la session distance à la connexion"
 
-#: vncviewer/OptionsDialog.cxx:750
+#: vncviewer/OptionsDialog.cxx:770
 msgid "Resize remote session to the local window"
 msgstr "Redimensionner la session distante pour la fenêtre locale"
 
-#: vncviewer/OptionsDialog.cxx:756
+#: vncviewer/OptionsDialog.cxx:776
 msgid "Full-screen mode"
 msgstr "Mode plein écran"
 
-#: vncviewer/OptionsDialog.cxx:762
+#: vncviewer/OptionsDialog.cxx:782
 msgid "Enable full-screen mode over all monitors"
 msgstr "Activer le mode plein écran pour tous les écrans"
 
-#: vncviewer/OptionsDialog.cxx:771
+#: vncviewer/OptionsDialog.cxx:791
 msgid "Misc."
 msgstr "Divers"
 
-#: vncviewer/OptionsDialog.cxx:779
+#: vncviewer/OptionsDialog.cxx:799
 msgid "Shared (don't disconnect other viewers)"
 msgstr "Partagé (ne pas déconnecter les autres visionneuses)"
 
-#: vncviewer/OptionsDialog.cxx:785
+#: vncviewer/OptionsDialog.cxx:805
 msgid "Show dot when no cursor"
 msgstr "Afficher un point s’il n’y a pas de curseur"
 
@@ -372,112 +368,112 @@
 msgid "Username:"
 msgstr "Nom d’utilisateur :"
 
-#: vncviewer/Viewport.cxx:433
+#: vncviewer/Viewport.cxx:391
 #, c-format
 msgid "Unable to create platform specific framebuffer: %s"
 msgstr "Impossible de créer un framebuffer adapté à la plateforme : %s"
 
-#: vncviewer/Viewport.cxx:434
+#: vncviewer/Viewport.cxx:392
 msgid "Using platform independent framebuffer"
 msgstr "Utilisation d’un framebuffer indépendant de la plateforme"
 
-#: vncviewer/Viewport.cxx:668
+#: vncviewer/Viewport.cxx:628
 #, c-format
 msgid "No scan code for extended virtual key 0x%02x"
 msgstr "Pas de scan code pour la clef virtuelle étendue 0x%02x"
 
-#: vncviewer/Viewport.cxx:670
+#: vncviewer/Viewport.cxx:630
 #, c-format
 msgid "No scan code for virtual key 0x%02x"
 msgstr "Pas de scan code pour la clef virtuelle 0x%02x"
 
-#: vncviewer/Viewport.cxx:687
+#: vncviewer/Viewport.cxx:647
 #, c-format
 msgid "No symbol for extended virtual key 0x%02x"
 msgstr "Pas de symbole pour la clef virtuelle étendue 0x%02x"
 
-#: vncviewer/Viewport.cxx:689
+#: vncviewer/Viewport.cxx:649
 #, c-format
 msgid "No symbol for virtual key 0x%02x"
 msgstr "Pas de symbole pour la clef virtuelle 0x%02x"
 
-#: vncviewer/Viewport.cxx:727
+#: vncviewer/Viewport.cxx:687
 #, c-format
 msgid "No symbol for key code 0x%02x (in the current state)"
 msgstr "Pas de scan code pour le code clef 0x%02x (en l'état actuel)"
 
-#: vncviewer/Viewport.cxx:753
+#: vncviewer/Viewport.cxx:713
 #, c-format
 msgid "No symbol for key code %d (in the current state)"
 msgstr "Pas de scan code pour le code clef %d (en l'état actuel)"
 
-#: vncviewer/Viewport.cxx:790
+#: vncviewer/Viewport.cxx:750
 msgctxt "ContextMenu|"
 msgid "E&xit viewer"
 msgstr "&Quitte la visionneuse"
 
-#: vncviewer/Viewport.cxx:793
+#: vncviewer/Viewport.cxx:753
 msgctxt "ContextMenu|"
 msgid "&Full screen"
 msgstr "&Plein écran"
 
-#: vncviewer/Viewport.cxx:796
+#: vncviewer/Viewport.cxx:756
 msgctxt "ContextMenu|"
 msgid "Minimi&ze"
 msgstr "&Réduire"
 
-#: vncviewer/Viewport.cxx:798
+#: vncviewer/Viewport.cxx:758
 msgctxt "ContextMenu|"
 msgid "Resize &window to session"
 msgstr "Redimensionner la &fenêtre pour la session"
 
-#: vncviewer/Viewport.cxx:803
+#: vncviewer/Viewport.cxx:763
 msgctxt "ContextMenu|"
 msgid "&Ctrl"
 msgstr "&Ctrl"
 
-#: vncviewer/Viewport.cxx:806
+#: vncviewer/Viewport.cxx:766
 msgctxt "ContextMenu|"
 msgid "&Alt"
 msgstr "&Alt"
 
-#: vncviewer/Viewport.cxx:812
+#: vncviewer/Viewport.cxx:772
 #, c-format
 msgctxt "ContextMenu|"
 msgid "Send %s"
 msgstr "Envoyer %s"
 
-#: vncviewer/Viewport.cxx:818
+#: vncviewer/Viewport.cxx:778
 msgctxt "ContextMenu|"
 msgid "Send Ctrl-Alt-&Del"
 msgstr "Envoyer Ctrl-Alt-&Sup"
 
-#: vncviewer/Viewport.cxx:821
+#: vncviewer/Viewport.cxx:781
 msgctxt "ContextMenu|"
 msgid "&Refresh screen"
 msgstr "R&afraîchir l’écran"
 
-#: vncviewer/Viewport.cxx:824
+#: vncviewer/Viewport.cxx:784
 msgctxt "ContextMenu|"
 msgid "&Options..."
 msgstr "&Options…"
 
-#: vncviewer/Viewport.cxx:826
+#: vncviewer/Viewport.cxx:786
 msgctxt "ContextMenu|"
 msgid "Connection &info..."
 msgstr "&Informations de connexion…"
 
-#: vncviewer/Viewport.cxx:828
+#: vncviewer/Viewport.cxx:788
 msgctxt "ContextMenu|"
 msgid "About &TigerVNC viewer..."
 msgstr "À propos de la visionneuse &TigerVNC…"
 
-#: vncviewer/Viewport.cxx:831
+#: vncviewer/Viewport.cxx:791
 msgctxt "ContextMenu|"
 msgid "Dismiss &menu"
 msgstr "Masquer le &menu"
 
-#: vncviewer/Viewport.cxx:915
+#: vncviewer/Viewport.cxx:875
 msgid "VNC connection info"
 msgstr "Informations de la connexion VNC"
 
@@ -499,123 +495,123 @@
 
 #. TRANSLATORS: "pixmap" is an X11 concept and may not be suitable
 #. to translate.
-#: vncviewer/X11PixelBuffer.cxx:61
+#: vncviewer/X11PixelBuffer.cxx:65
 msgid "Display lacks pixmap format for default depth"
 msgstr "Afficher les formats de pixmap manquants pour la profondeur par défaut"
 
 #. TRANSLATORS: "pixmap" is an X11 concept and may not be suitable
 #. to translate.
-#: vncviewer/X11PixelBuffer.cxx:72
+#: vncviewer/X11PixelBuffer.cxx:76
 msgid "Couldn't find suitable pixmap format"
 msgstr "Impossible de trouver un format de pixmap utilisable"
 
-#: vncviewer/X11PixelBuffer.cxx:81
+#: vncviewer/X11PixelBuffer.cxx:85
 msgid "Only true colour displays supported"
 msgstr "Ne prend en charge que l’affiche en vrais couleurs"
 
-#: vncviewer/X11PixelBuffer.cxx:83
+#: vncviewer/X11PixelBuffer.cxx:87
 #, c-format
 msgid "Using default colormap and visual, TrueColor, depth %d."
 msgstr "Utilisation de la carte de couleur et du visuel par défaut, TrueColor, profondeur %d."
 
-#: vncviewer/X11PixelBuffer.cxx:109
+#: vncviewer/X11PixelBuffer.cxx:113
 msgid "Could not create framebuffer image"
 msgstr "Impossible de créer l’image framebuffer"
 
-#: vncviewer/parameters.cxx:279 vncviewer/parameters.cxx:313
+#: vncviewer/parameters.cxx:286 vncviewer/parameters.cxx:320
 #, c-format
 msgid "The name of the parameter %s was too large to write to the registry"
 msgstr "Le nom du paramètre %s était trop long pour l’écrire dans le registre."
 
-#: vncviewer/parameters.cxx:285 vncviewer/parameters.cxx:292
+#: vncviewer/parameters.cxx:292 vncviewer/parameters.cxx:299
 #, c-format
 msgid "The parameter %s was too large to write to the registry"
 msgstr "Le paramètre %s était trop long pour l’écrire dans le registre."
 
-#: vncviewer/parameters.cxx:298 vncviewer/parameters.cxx:319
+#: vncviewer/parameters.cxx:305 vncviewer/parameters.cxx:326
 #, c-format
 msgid "Failed to write parameter %s of type %s to the registry: %ld"
 msgstr "Impossible d’écrire le paramètre %s de type %s dans le registre : %ld"
 
-#: vncviewer/parameters.cxx:334 vncviewer/parameters.cxx:373
+#: vncviewer/parameters.cxx:341 vncviewer/parameters.cxx:380
 #, c-format
 msgid "The name of the parameter %s was too large to read from the registry"
 msgstr "Le nom du paramètre %s était trop long pour lire le registre"
 
-#: vncviewer/parameters.cxx:343 vncviewer/parameters.cxx:382
+#: vncviewer/parameters.cxx:350 vncviewer/parameters.cxx:389
 #, c-format
 msgid "Failed to read parameter %s from the registry: %ld"
 msgstr "Impossible de lire le paramètre %s depuis le registre : %ld"
 
-#: vncviewer/parameters.cxx:352
+#: vncviewer/parameters.cxx:359
 #, c-format
 msgid "The parameter %s was too large to read from the registry"
 msgstr "Le paramètre %s était trop long pour le lire depuis le registre"
 
-#: vncviewer/parameters.cxx:402
+#: vncviewer/parameters.cxx:409
 #, c-format
 msgid "Failed to create registry key: %ld"
 msgstr "Impossible de créer la clef de regitre : %ld"
 
-#: vncviewer/parameters.cxx:416 vncviewer/parameters.cxx:465
-#: vncviewer/parameters.cxx:527 vncviewer/parameters.cxx:658
+#: vncviewer/parameters.cxx:423 vncviewer/parameters.cxx:472
+#: vncviewer/parameters.cxx:534 vncviewer/parameters.cxx:665
 #, c-format
 msgid "Unknown parameter type for parameter %s"
 msgstr "Type de paramètre inconnu pour le paramètre %s"
 
-#: vncviewer/parameters.cxx:423 vncviewer/parameters.cxx:472
+#: vncviewer/parameters.cxx:430 vncviewer/parameters.cxx:479
 #, c-format
 msgid "Failed to close registry key: %ld"
 msgstr "Impossible de fermer la clef de registre : %ld"
 
-#: vncviewer/parameters.cxx:439
+#: vncviewer/parameters.cxx:446
 #, c-format
 msgid "Failed to open registry key: %ld"
 msgstr "Impossible d’ouvrir la clef de registre : %ld"
 
-#: vncviewer/parameters.cxx:496
+#: vncviewer/parameters.cxx:503
 msgid "Failed to write configuration file, can't obtain home directory path."
 msgstr "Impossible d'écrire le fichier de configuration, impossible d'obtenir le chemin du répertoire personnel."
 
-#: vncviewer/parameters.cxx:509
+#: vncviewer/parameters.cxx:516
 #, c-format
 msgid "Failed to write configuration file, can't open %s: %s"
 msgstr "Impossible d'écrire le fichier de configuration, impossible d’ouvrir %s : %s"
 
-#: vncviewer/parameters.cxx:552
+#: vncviewer/parameters.cxx:559
 msgid "Failed to read configuration file, can't obtain home directory path."
 msgstr "Impossible de lire le fichier de configuration, impossible d’obtenir le chemin du répertoire personnel."
 
-#: vncviewer/parameters.cxx:565
+#: vncviewer/parameters.cxx:572
 #, c-format
 msgid "Failed to read configuration file, can't open %s: %s"
 msgstr "Impossible de lire le fichier de configuration, impossible d’ouvrir %s : %s"
 
-#: vncviewer/parameters.cxx:578 vncviewer/parameters.cxx:583
-#: vncviewer/parameters.cxx:608 vncviewer/parameters.cxx:621
-#: vncviewer/parameters.cxx:637
+#: vncviewer/parameters.cxx:585 vncviewer/parameters.cxx:590
+#: vncviewer/parameters.cxx:615 vncviewer/parameters.cxx:628
+#: vncviewer/parameters.cxx:644
 #, c-format
 msgid "Failed to read line %d in file %s: %s"
 msgstr "Impossible de lire la ligne %d du fichier %s : %s"
 
-#: vncviewer/parameters.cxx:584
+#: vncviewer/parameters.cxx:591
 msgid "Line too long"
 msgstr "Ligne trop longue"
 
-#: vncviewer/parameters.cxx:591
+#: vncviewer/parameters.cxx:598
 #, c-format
 msgid "Configuration file %s is in an invalid format"
 msgstr "Fichier de configuration %s dans un format invalide"
 
-#: vncviewer/parameters.cxx:609
+#: vncviewer/parameters.cxx:616
 msgid "Invalid format"
 msgstr "Format invalide"
 
-#: vncviewer/parameters.cxx:622 vncviewer/parameters.cxx:638
+#: vncviewer/parameters.cxx:629 vncviewer/parameters.cxx:645
 msgid "Invalid format or too large value"
 msgstr "Format invalide ou valeur trop grande"
 
-#: vncviewer/parameters.cxx:665
+#: vncviewer/parameters.cxx:672
 #, c-format
 msgid "Unknown parameter %s on line %d in file %s"
 msgstr "Paramètre %s inconnu à la ligne %d du fichier %s"
@@ -637,86 +633,92 @@
 msgid "About TigerVNC Viewer"
 msgstr "À propos de la visionneuse TigerVNC"
 
-#: vncviewer/vncviewer.cxx:144 vncviewer/vncviewer.cxx:156
+#: vncviewer/vncviewer.cxx:140
+msgid "Internal FLTK error. Exiting."
+msgstr "Erreur FLTK interne. Procédure de sortie."
+
+#: vncviewer/vncviewer.cxx:158 vncviewer/vncviewer.cxx:170
 #, c-format
 msgid "Error starting new TigerVNC Viewer: %s"
 msgstr "Une erreur à démarrer une nouvelle visionneuse TigerVNC : %s"
 
-#: vncviewer/vncviewer.cxx:165
+#: vncviewer/vncviewer.cxx:179
 #, c-format
 msgid "Termination signal %d has been received. TigerVNC Viewer will now exit."
 msgstr "Signal de fin %d reçu. La visionneuse TigerVNC va se fermer."
 
-#: vncviewer/vncviewer.cxx:257
+#: vncviewer/vncviewer.cxx:271
 msgid "TigerVNC Viewer"
 msgstr "Visionneuse TigerVNC"
 
-#: vncviewer/vncviewer.cxx:265
+#: vncviewer/vncviewer.cxx:279
 msgid "No"
 msgstr "Non"
 
-#: vncviewer/vncviewer.cxx:266
+#: vncviewer/vncviewer.cxx:280
 msgid "Yes"
 msgstr "Oui"
 
-#: vncviewer/vncviewer.cxx:269
+#: vncviewer/vncviewer.cxx:283
 msgid "Close"
 msgstr "Fermer"
 
-#: vncviewer/vncviewer.cxx:274
+#: vncviewer/vncviewer.cxx:288
 msgid "About"
 msgstr "À propos"
 
-#: vncviewer/vncviewer.cxx:277
+#: vncviewer/vncviewer.cxx:291
 msgid "Hide"
 msgstr "Cacher"
 
-#: vncviewer/vncviewer.cxx:280
+#: vncviewer/vncviewer.cxx:294
 msgid "Quit"
 msgstr "Quitter"
 
-#: vncviewer/vncviewer.cxx:284
+#: vncviewer/vncviewer.cxx:298
 msgid "Services"
 msgstr "Services"
 
-#: vncviewer/vncviewer.cxx:285
+#: vncviewer/vncviewer.cxx:299
 msgid "Hide Others"
 msgstr "Cacher les autres"
 
-#: vncviewer/vncviewer.cxx:286
+#: vncviewer/vncviewer.cxx:300
 msgid "Show All"
 msgstr "Afficher tout"
 
-#: vncviewer/vncviewer.cxx:295
+#: vncviewer/vncviewer.cxx:309
 msgctxt "SysMenu|"
 msgid "&File"
 msgstr "&Fichier"
 
-#: vncviewer/vncviewer.cxx:298
+#: vncviewer/vncviewer.cxx:312
 msgctxt "SysMenu|File|"
 msgid "&New Connection"
 msgstr "&Nouvelle connexion"
 
-#: vncviewer/vncviewer.cxx:310
+#: vncviewer/vncviewer.cxx:324
 msgid "Could not create VNC home directory: can't obtain home directory path."
 msgstr "Impossible de créer le répertoire VNC de départ : impossible d’obtenir le chemin du répertoire de départ."
 
-#: vncviewer/vncviewer.cxx:315
+#: vncviewer/vncviewer.cxx:329
 #, c-format
 msgid "Could not create VNC home directory: %s."
 msgstr "Impossible de créer le répertoire VNC de départ : %s."
 
 #. TRANSLATORS: "Parameters" are command line arguments, or settings
 #. from a file or the Windows registry.
-#: vncviewer/vncviewer.cxx:520 vncviewer/vncviewer.cxx:521
+#: vncviewer/vncviewer.cxx:534 vncviewer/vncviewer.cxx:535
 msgid "Parameters -listen and -via are incompatible"
 msgstr "Les paramètres -listen et -via sont incompatibles"
 
-#: vncviewer/vncviewer.cxx:536
+#: vncviewer/vncviewer.cxx:550
 #, c-format
 msgid "Listening on port %d"
 msgstr "Écoute du port %d"
 
-#: vncviewer/vncviewer.cxx:601
-msgid "Internal FLTK error. Exiting."
-msgstr "Erreur FLTK interne. Procédure de sortie."
+#~ msgid "Unknown encoding %d"
+#~ msgstr "Encodage inconnu %d"
+
+#~ msgid "Unknown encoding"
+#~ msgstr "Encodage inconnu"
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index bfd69dc..8ea9925 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,3 +1,6 @@
+include_directories(${FLTK_INCLUDE_DIR})
+include_directories(${GETTEXT_INCLUDE_DIR})
+
 include_directories(${CMAKE_SOURCE_DIR}/common)
 
 add_library(test_util STATIC util.cxx)
@@ -16,3 +19,23 @@
 
 add_executable(hostport hostport.cxx)
 target_link_libraries(hostport rfb)
+
+set(FBPERF_SOURCES
+  fbperf.cxx
+  ../vncviewer/PlatformPixelBuffer.cxx
+  ../vncviewer/Surface.cxx)
+if(WIN32)
+  set(FBPERF_SOURCES ${FBPERF_SOURCES} ../vncviewer/Surface_Win32.cxx)
+elseif(APPLE)
+  set(FBPERF_SOURCES ${FBPERF_SOURCES} ../vncviewer/Surface_OSX.cxx)
+else()
+  set(FBPERF_SOURCES ${FBPERF_SOURCES} ../vncviewer/Surface_X11.cxx)
+endif()
+add_executable(fbperf ${FBPERF_SOURCES})
+target_link_libraries(fbperf test_util rfb ${FLTK_LIBRARIES} ${GETTEXT_LIBRARIES})
+if(WIN32)
+  target_link_libraries(fbperf msimg32)
+endif()
+if(APPLE)
+  target_link_libraries(fbperf "-framework Cocoa" "-framework Carbon")
+endif()
diff --git a/tests/decperf.cxx b/tests/decperf.cxx
index 1fd763e..3b929a4 100644
--- a/tests/decperf.cxx
+++ b/tests/decperf.cxx
@@ -49,7 +49,7 @@
 
   virtual void setDesktopSize(int w, int h);
   virtual void setPixelFormat(const rfb::PixelFormat& pf);
-  virtual void setCursor(int, int, const rfb::Point&, void*, void*);
+  virtual void setCursor(int, int, const rfb::Point&, const rdr::U8*);
   virtual void framebufferUpdateStart();
   virtual void framebufferUpdateEnd();
   virtual void setColourMapEntries(int, int, rdr::U16*);
@@ -94,7 +94,7 @@
   CConnection::setPixelFormat(filePF);
 }
 
-void CConn::setCursor(int, int, const rfb::Point&, void*, void*)
+void CConn::setCursor(int, int, const rfb::Point&, const rdr::U8*)
 {
 }
 
diff --git a/tests/encperf.cxx b/tests/encperf.cxx
index d58d82e..7b9ff81 100644
--- a/tests/encperf.cxx
+++ b/tests/encperf.cxx
@@ -90,7 +90,7 @@
                 unsigned long long& rawEquivalent);
 
   virtual void setDesktopSize(int w, int h);
-  virtual void setCursor(int, int, const rfb::Point&, void*, void*);
+  virtual void setCursor(int, int, const rfb::Point&, const rdr::U8*);
   virtual void framebufferUpdateStart();
   virtual void framebufferUpdateEnd();
   virtual void dataRect(const rfb::Rect&, int);
@@ -207,7 +207,7 @@
   setFramebuffer(pb);
 }
 
-void CConn::setCursor(int, int, const rfb::Point&, void*, void*)
+void CConn::setCursor(int, int, const rfb::Point&, const rdr::U8*)
 {
 }
 
diff --git a/tests/fbperf.cxx b/tests/fbperf.cxx
new file mode 100644
index 0000000..a19ee47
--- /dev/null
+++ b/tests/fbperf.cxx
@@ -0,0 +1,399 @@
+/* Copyright 2016 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.
+ */
+
+#include <math.h>
+#include <sys/time.h>
+
+#include <FL/Fl.H>
+#include <FL/Fl_Window.H>
+#include <FL/fl_draw.H>
+
+#include <rdr/Exception.h>
+#include <rfb/util.h>
+
+#include "../vncviewer/PlatformPixelBuffer.h"
+
+#include "util.h"
+
+class TestWindow: public Fl_Window {
+public:
+  TestWindow();
+  ~TestWindow();
+
+  virtual void start(int width, int height);
+  virtual void stop();
+
+  virtual void draw();
+
+protected:
+  virtual void flush();
+
+  void update();
+  virtual void changefb();
+
+  static void timer(void* data);
+
+public:
+  unsigned long long pixels, frames;
+  double time;
+
+protected:
+  PlatformPixelBuffer* fb;
+};
+
+class PartialTestWindow: public TestWindow {
+protected:
+  virtual void changefb();
+};
+
+class OverlayTestWindow: public PartialTestWindow {
+public:
+  OverlayTestWindow();
+
+  virtual void start(int width, int height);
+  virtual void stop();
+
+  virtual void draw();
+
+protected:
+  Surface* overlay;
+  Surface* offscreen;
+};
+
+TestWindow::TestWindow() :
+  Fl_Window(0, 0, "Framebuffer Performance Test"),
+  fb(NULL)
+{
+}
+
+TestWindow::~TestWindow()
+{
+  stop();
+}
+
+void TestWindow::start(int width, int height)
+{
+  rdr::U32 pixel;
+
+  stop();
+
+  resize(x(), y(), width, height);
+
+  pixels = 0;
+  frames = 0;
+  time = 0;
+
+  fb = new PlatformPixelBuffer(w(), h());
+
+  pixel = 0;
+  fb->fillRect(fb->getRect(), &pixel);
+
+  show();
+}
+
+void TestWindow::stop()
+{
+  hide();
+
+  delete fb;
+  fb = NULL;
+
+  Fl::remove_idle(timer, this);
+}
+
+void TestWindow::draw()
+{
+  int X, Y, W, H;
+
+  // We cannot update the damage region from inside the draw function,
+  // so delegate this to an idle function
+  Fl::add_idle(timer, this);
+
+  // Check what actually needs updating
+  fl_clip_box(0, 0, w(), h(), X, Y, W, H);
+  if ((W == 0) || (H == 0))
+    return;
+
+  fb->draw(X, Y, X, Y, W, H);
+
+  pixels += W*H;
+  frames++;
+}
+
+void TestWindow::flush()
+{
+  startTimeCounter();
+  Fl_Window::flush();
+#if !defined(WIN32) && !defined(__APPLE__)
+  // Make sure we measure any work we queue up
+  XSync(fl_display, False);
+#endif
+  endTimeCounter();
+
+  time += getTimeCounter();
+}
+
+void TestWindow::update()
+{
+  rfb::Rect r;
+
+  startTimeCounter();
+
+  changefb();
+
+  r = fb->getDamage();
+  damage(FL_DAMAGE_USER1, r.tl.x, r.tl.y, r.width(), r.height());
+
+#if !defined(WIN32) && !defined(__APPLE__)
+  // Make sure we measure any work we queue up
+  XSync(fl_display, False);
+#endif
+
+  endTimeCounter();
+
+  time += getTimeCounter();
+}
+
+void TestWindow::changefb()
+{
+  rdr::U32 pixel;
+
+  pixel = rand();
+  fb->fillRect(fb->getRect(), &pixel);
+}
+
+void TestWindow::timer(void* data)
+{
+  TestWindow* self;
+
+  Fl::remove_idle(timer, data);
+
+  self = (TestWindow*)data;
+  self->update();
+}
+
+void PartialTestWindow::changefb()
+{
+  rfb::Rect r;
+  rdr::U32 pixel;
+
+  r = fb->getRect();
+  r.tl.x += w() / 4;
+  r.tl.y += h() / 4;
+  r.br.x -= w() / 4;
+  r.br.y -= h() / 4;
+
+  pixel = rand();
+  fb->fillRect(r, &pixel);
+}
+
+OverlayTestWindow::OverlayTestWindow() :
+  overlay(NULL), offscreen(NULL)
+{
+}
+
+void OverlayTestWindow::start(int width, int height)
+{
+  PartialTestWindow::start(width, height);
+
+  overlay = new Surface(400, 200);
+  overlay->clear(0xff, 0x80, 0x00, 0xcc);
+
+  // X11 needs an off screen buffer for compositing to avoid flicker,
+  // and alpha blending doesn't work for windows on Win32
+#if !defined(__APPLE__)
+  offscreen = new Surface(w(), h());
+#else
+  offscreen = NULL;
+#endif
+}
+
+void OverlayTestWindow::stop()
+{
+  PartialTestWindow::stop();
+
+  delete offscreen;
+  offscreen = NULL;
+  delete overlay;
+  overlay = NULL;
+}
+
+void OverlayTestWindow::draw()
+{
+  int ox, oy, ow, oh;
+  int X, Y, W, H;
+
+  // We cannot update the damage region from inside the draw function,
+  // so delegate this to an idle function
+  Fl::add_idle(timer, this);
+
+  // Check what actually needs updating
+  fl_clip_box(0, 0, w(), h(), X, Y, W, H);
+  if ((W == 0) || (H == 0))
+    return;
+
+  // We might get a redraw before we are fully ready
+  if (!overlay)
+    return;
+
+  // Simplify the clip region to a simple rectangle in order to
+  // properly draw all the layers even if they only partially overlap
+  fl_push_no_clip();
+  fl_push_clip(X, Y, W, H);
+
+  if (offscreen)
+    fb->draw(offscreen, X, Y, X, Y, W, H);
+  else
+    fb->draw(X, Y, X, Y, W, H);
+
+  pixels += W*H;
+  frames++;
+
+  ox = (w() - overlay->width()) / 2;
+  oy = h() / 4 - overlay->height() / 2;
+  ow = overlay->width();
+  oh = overlay->height();
+  fl_clip_box(ox, oy, ow, oh, X, Y, W, H);
+  if ((W != 0) && (H != 0)) {
+    if (offscreen)
+      overlay->blend(offscreen, X - ox, Y - oy, X, Y, W, H);
+    else
+      overlay->blend(X - ox, Y - oy, X, Y, W, H);
+  }
+
+  fl_pop_clip();
+  fl_pop_clip();
+
+  if (offscreen) {
+    fl_clip_box(0, 0, w(), h(), X, Y, W, H);
+    offscreen->draw(X, Y, X, Y, W, H);
+  }
+}
+
+static void dosubtest(TestWindow* win, int width, int height,
+                      unsigned long long* pixels,
+		      unsigned long long* frames,
+		      double* time)
+{
+  struct timeval start;
+
+  win->start(width, height);
+
+  gettimeofday(&start, NULL);
+  while (rfb::msSince(&start) < 3000)
+    Fl::wait();
+
+  win->stop();
+
+  *pixels = win->pixels;
+  *frames = win->frames;
+  *time = win->time;
+}
+
+static bool is_constant(double a, double b)
+{
+    return (fabs(a - b) / a) < 0.1;
+}
+
+static void dotest(TestWindow* win)
+{
+  unsigned long long pixels[3];
+  unsigned long long frames[3];
+  double time[3];
+
+  double delay, rate;
+  char s[1024];
+
+  // Run the test several times at different resolutions...
+  dosubtest(win, 800, 600, &pixels[0], &frames[0], &time[0]);
+  dosubtest(win, 1024, 768, &pixels[1], &frames[1], &time[1]);
+  dosubtest(win, 1280, 960, &pixels[2], &frames[2], &time[2]);
+
+  // ...in order to compute how much of the rendering time is static,
+  // and how much depends on the number of pixels
+  // (i.e. solve: time = delay * frames + rate * pixels)
+  delay = (((time[0] - (double)pixels[0] / pixels[1] * time[1]) /
+            (frames[0] - (double)pixels[0] / pixels[1] * frames[1])) +
+           ((time[1] - (double)pixels[1] / pixels[2] * time[2]) /
+            (frames[1] - (double)pixels[1] / pixels[2] * frames[2]))) / 2.0;
+  rate = (((time[0] - (double)frames[0] / frames[1] * time[1]) /
+           (pixels[0] - (double)frames[0] / frames[1] * pixels[1])) +
+          ((time[1] - (double)frames[1] / frames[2] * time[2]) /
+           (pixels[1] - (double)frames[1] / frames[2] * pixels[2]))) / 2.0;
+
+  // However, we have some corner cases:
+
+  // We are restricted by some delay, e.g. refresh rate
+  if (is_constant(frames[0]/time[0], frames[2]/time[2])) {
+    fprintf(stderr, "WARNING: Fixed delay dominating updates.\n\n");
+    delay = time[2]/frames[2];
+    rate = 0.0;
+  }
+
+  // There isn't any fixed delay, we are only restricted by pixel
+  // throughput
+  if (fabs(delay) < 0.001) {
+    delay = 0.0;
+    rate = time[2]/pixels[2];
+  }
+
+  // We can hit cache limits that causes performance to drop
+  // with increasing update size, screwing up our calculations
+  if ((pixels[2] / time[2]) < (pixels[0] / time[0] * 0.9)) {
+    fprintf(stderr, "WARNING: Unexpected behaviour. Measurement unreliable.\n\n");
+
+    // We can't determine the proportions between these, so divide the
+    // time spent evenly
+    delay = time[2] / 2.0 / frames[2];
+    rate = time[2] / 2.0 / pixels[2];
+  }
+
+  fprintf(stderr, "Rendering delay: %g ms/frame\n", delay * 1000.0);
+  if (rate == 0.0)
+    strcpy(s, "N/A pixels/s");
+  else
+    rfb::siPrefix(1.0 / rate, "pixels/s", s, sizeof(s));
+  fprintf(stderr, "Rendering rate: %s\n", s);
+  fprintf(stderr, "Maximum FPS: %g fps @ 1920x1080\n",
+          1.0 / (delay + rate * 1920 * 1080));
+}
+
+int main(int argc, char** argv)
+{
+  TestWindow* win;
+
+  fprintf(stderr, "Full window update:\n\n");
+  win = new TestWindow();
+  dotest(win);
+  delete win;
+  fprintf(stderr, "\n");
+
+  fprintf(stderr, "Partial window update:\n\n");
+  win = new PartialTestWindow();
+  dotest(win);
+  delete win;
+  fprintf(stderr, "\n");
+
+  fprintf(stderr, "Partial window update with overlay:\n\n");
+  win = new OverlayTestWindow();
+  dotest(win);
+  delete win;
+  fprintf(stderr, "\n");
+
+  return 0;
+}
diff --git a/tests/results/multicore/README b/tests/results/multicore/README
index ffeca4e..c93b2d7 100644
--- a/tests/results/multicore/README
+++ b/tests/results/multicore/README
@@ -34,7 +34,7 @@
 point and tracing doesn't reveal anything obvious. It may be because it
 is not a true quad core system, but rather uses HyperThreading.
 
-So in summary, the new code can do a noticable improvement on decoding
+So in summary, the new code can do a noticeable improvement on decoding
 time. However it does so at a cost of efficiency. Four times the CPUs
 only gives you about twice the performance. More improvements may be
 possible.
diff --git a/tests/util.cxx b/tests/util.cxx
index 4683d35..17a8369 100644
--- a/tests/util.cxx
+++ b/tests/util.cxx
@@ -24,6 +24,7 @@
 #include <windows.h>
 #else
 #include <sys/resource.h>
+#include <sys/time.h>
 #endif
 
 #include "util.h"
@@ -132,3 +133,46 @@
 
   return sysSeconds + userSeconds;
 }
+
+#ifdef WIN32
+static LARGE_INTEGER timeStart, timeEnd;
+#else
+static struct timeval timeStart, timeEnd;
+#endif
+
+void startTimeCounter(void)
+{
+#ifdef WIN32
+  QueryPerformanceCounter(&timeStart);
+#else
+  gettimeofday(&timeStart, NULL);
+#endif
+}
+
+void endTimeCounter(void)
+{
+#ifdef WIN32
+  QueryPerformanceCounter(&timeEnd);
+#else
+  gettimeofday(&timeEnd, NULL);
+#endif
+}
+
+double getTimeCounter(void)
+{
+  double time;
+
+#ifdef WIN32
+  LARGE_INTEGER freq;
+
+  QueryPerformanceFrequency(&freq);
+
+  time = timeEnd.QuadPart - timeStart.QuadPart;
+  time = time / freq.QuadPart;
+#else
+  time = (double)timeEnd.tv_sec - timeStart.tv_sec;
+  time += (double)(timeEnd.tv_usec - timeStart.tv_usec) / 1000000.0;
+#endif
+
+  return time;
+}
diff --git a/tests/util.h b/tests/util.h
index 16f2ba2..2b8ab4a 100644
--- a/tests/util.h
+++ b/tests/util.h
@@ -34,4 +34,9 @@
 
 double getCpuCounter(cpucounter_t c);
 
+void startTimeCounter(void);
+void endTimeCounter(void);
+
+double getTimeCounter(void);
+
 #endif
diff --git a/unix/tx/TXDialog.h b/unix/tx/TXDialog.h
index c8d601c..7a52f92 100644
--- a/unix/tx/TXDialog.h
+++ b/unix/tx/TXDialog.h
@@ -78,7 +78,7 @@
   // to make sure that checkboxes have the right state, etc.
   virtual void initDialog() {}
 
-  // resize() is overidden here to re-center the dialog
+  // resize() is overridden here to re-center the dialog
   void resize(int w, int h) {
     TXWindow::resize(w,h);
     int dpyWidth = WidthOfScreen(DefaultScreenOfDisplay(dpy));
diff --git a/unix/x0vncserver/TimeMillis.h b/unix/x0vncserver/TimeMillis.h
index e79db12..115ce08 100644
--- a/unix/x0vncserver/TimeMillis.h
+++ b/unix/x0vncserver/TimeMillis.h
@@ -31,7 +31,7 @@
 
   TimeMillis();
 
-  // Set this object to current time, returns true on sucess.
+  // Set this object to current time, returns true on success.
   bool update();
 
   // Return difference in milliseconds between two time points.
diff --git a/unix/xserver/hw/vnc/Input.c b/unix/xserver/hw/vnc/Input.c
index 64305cb..33e8604 100644
--- a/unix/xserver/hw/vnc/Input.c
+++ b/unix/xserver/hw/vnc/Input.c
@@ -74,7 +74,7 @@
 /*
  * Init input device.
  * This has to be called after core pointer/keyboard
- * initialization which unfortunately is after extesions
+ * initialization which unfortunately is after extensions
  * initialization (which means we cannot call it in
  * vncExtensionInit(). Check InitExtensions(),
  * InitCoreDevices() and InitInput() calls in dix/main.c.
diff --git a/unix/xserver/hw/vnc/XserverDesktop.cc b/unix/xserver/hw/vnc/XserverDesktop.cc
index b681ae7..4836782 100644
--- a/unix/xserver/hw/vnc/XserverDesktop.cc
+++ b/unix/xserver/hw/vnc/XserverDesktop.cc
@@ -1,5 +1,5 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright 2009-2015 Pierre Ossman for Cendio AB
+ * Copyright 2009-2017 Pierre Ossman for Cendio AB
  * Copyright 2014 Brian P. Hinz
  * 
  * This is free software; you can redistribute it and/or modify
@@ -344,46 +344,37 @@
                                const unsigned char *rgbaData)
 {
   rdr::U8* cursorData;
-  rdr::U8* cursorMask;
-  int rfbMaskBytesPerRow;
 
   rdr::U8 *out;
   const unsigned char *in;
-  rdr::U8 rgb[3];
 
-  cursorData = new rdr::U8[width * height * (getPF().bpp / 8)];
+  cursorData = new rdr::U8[width * height * 4];
 
-  rfbMaskBytesPerRow = (width + 7) / 8;
-
-  cursorMask = new rdr::U8[rfbMaskBytesPerRow * height];
-
-  memset(cursorMask, 0, rfbMaskBytesPerRow * height);
-
+  // Un-premultiply alpha
   in = rgbaData;
   out = cursorData;
   for (int y = 0; y < height; y++) {
     for (int x = 0; x < width; x++) {
-      rgb[0] = *in++;
-      rgb[1] = *in++;
-      rgb[2] = *in++;
+      rdr::U8 alpha;
 
-      getPF().bufferFromRGB(out, rgb, 1);
+      alpha = in[3];
+      if (alpha == 0)
+        alpha = 1; // Avoid division by zero
 
-      if (*in++ > 127)
-        cursorMask[y * rfbMaskBytesPerRow + x/8] |= 0x80>>(x%8);
-
-      out += getPF().bpp/8;
+      *out++ = (unsigned)*in++ * 255/alpha;
+      *out++ = (unsigned)*in++ * 255/alpha;
+      *out++ = (unsigned)*in++ * 255/alpha;
+      *out++ = *in++;
     }
   }
 
   try {
-    server->setCursor(width, height, Point(hotX, hotY), cursorData, cursorMask);
+    server->setCursor(width, height, Point(hotX, hotY), cursorData);
   } catch (rdr::Exception& e) {
     vlog.error("XserverDesktop::setCursor: %s",e.str());
   }
 
   delete [] cursorData;
-  delete [] cursorMask;
 }
 
 void XserverDesktop::add_changed(const rfb::Region &region)
@@ -443,6 +434,7 @@
   sock->outStream().setBlocking(false);
   vlog.debug("new client, sock %d", sock->getFd());
   sockserv->addSocket(sock);
+  vncSetNotifyFd(sock->getFd(), screenIndex, true, false);
 
   return true;
 }
@@ -536,6 +528,7 @@
 void XserverDesktop::addClient(Socket* sock, bool reverse)
 {
   vlog.debug("new client, sock %d reverse %d",sock->getFd(),reverse);
+  sock->outStream().setBlocking(false);
   server->addSocket(sock, reverse);
   vncSetNotifyFd(sock->getFd(), screenIndex, true, false);
 }
diff --git a/unix/xserver/hw/vnc/vncBlockHandler.c b/unix/xserver/hw/vnc/vncBlockHandler.c
index 390a9b3..24c2013 100644
--- a/unix/xserver/hw/vnc/vncBlockHandler.c
+++ b/unix/xserver/hw/vnc/vncBlockHandler.c
@@ -66,9 +66,9 @@
 {
 #if XORG >= 119
   int mask = (read ? X_NOTIFY_READ : 0) | (write ? X_NOTIFY_WRITE : 0);
-  SetNotifyFd(fd, vncSocketNotify, mask, (void*)scrIdx);
+  SetNotifyFd(fd, vncSocketNotify, mask, (void*)(intptr_t)scrIdx);
 #else
-  static struct vncFdEntry* entry;
+  struct vncFdEntry* entry;
 
   entry = fdsHead;
   while (entry) {
@@ -99,8 +99,8 @@
 #if XORG >= 119
   RemoveNotifyFd(fd);
 #else
-  static struct vncFdEntry** prev;
-  static struct vncFdEntry* entry;
+  struct vncFdEntry** prev;
+  struct vncFdEntry* entry;
 
   prev = &fdsHead;
   entry = fdsHead;
@@ -122,7 +122,7 @@
 {
   int scrIdx;
 
-  scrIdx = (int)data;
+  scrIdx = (intptr_t)data;
   vncHandleSocketEvent(fd, scrIdx,
                        xevents & X_NOTIFY_READ,
                        xevents & X_NOTIFY_WRITE);
diff --git a/unix/xserver/hw/vnc/vncHooks.c b/unix/xserver/hw/vnc/vncHooks.c
index 29f3f8b..ef340b3 100644
--- a/unix/xserver/hw/vnc/vncHooks.c
+++ b/unix/xserver/hw/vnc/vncHooks.c
@@ -1,5 +1,5 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright 2009-2015 Pierre Ossman for Cendio AB
+ * Copyright 2009-2017 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
@@ -73,22 +73,31 @@
   GlyphsProcPtr                Glyphs;
 #endif
 #ifdef RANDR
-  RRSetConfigProcPtr           RandRSetConfig;
-  RRScreenSetSizeProcPtr       RandRScreenSetSize;
-  RRCrtcSetProcPtr             RandRCrtcSet;
+  RRSetConfigProcPtr           rrSetConfig;
+  RRScreenSetSizeProcPtr       rrScreenSetSize;
+  RRCrtcSetProcPtr             rrCrtcSet;
 #endif
 } vncHooksScreenRec, *vncHooksScreenPtr;
 
 typedef struct _vncHooksGCRec {
 #if XORG >= 116
-    const GCFuncs *wrappedFuncs;
-    const GCOps *wrappedOps;
+    const GCFuncs *funcs;
+    const GCOps *ops;
 #else
-    GCFuncs *wrappedFuncs;
-    GCOps *wrappedOps;
+    GCFuncs *funcs;
+    GCOps *ops;
 #endif
 } vncHooksGCRec, *vncHooksGCPtr;
 
+#define wrap(priv, real, mem, func) {\
+    priv->mem = real->mem; \
+    real->mem = func; \
+}
+
+#define unwrap(priv, real, mem) {\
+    real->mem = priv->mem; \
+}
+
 #if XORG < 19
 static int vncHooksScreenPrivateKeyIndex;
 static int vncHooksGCPrivateKeyIndex;
@@ -284,55 +293,28 @@
 
   vncHooksScreen->ignoreHooks = 0;
 
-  vncHooksScreen->CloseScreen = pScreen->CloseScreen;
-  vncHooksScreen->CreateGC = pScreen->CreateGC;
-  vncHooksScreen->CopyWindow = pScreen->CopyWindow;
-  vncHooksScreen->ClearToBackground = pScreen->ClearToBackground;
+  wrap(vncHooksScreen, pScreen, CloseScreen, vncHooksCloseScreen);
+  wrap(vncHooksScreen, pScreen, CreateGC, vncHooksCreateGC);
+  wrap(vncHooksScreen, pScreen, CopyWindow, vncHooksCopyWindow);
+  wrap(vncHooksScreen, pScreen, ClearToBackground, vncHooksClearToBackground);
 #if XORG < 110
-  vncHooksScreen->RestoreAreas = pScreen->RestoreAreas;
+  wrap(vncHooksScreen, pScreen, RestoreAreas, vncHooksRestoreAreas);
 #endif
-  vncHooksScreen->DisplayCursor = pScreen->DisplayCursor;
-  vncHooksScreen->BlockHandler = pScreen->BlockHandler;
+  wrap(vncHooksScreen, pScreen, DisplayCursor, vncHooksDisplayCursor);
+  wrap(vncHooksScreen, pScreen, BlockHandler, vncHooksBlockHandler);
 #ifdef RENDER
   ps = GetPictureScreenIfSet(pScreen);
   if (ps) {
-    vncHooksScreen->Composite = ps->Composite;
-    vncHooksScreen->Glyphs = ps->Glyphs;
+    wrap(vncHooksScreen, ps, Composite, vncHooksComposite);
+    wrap(vncHooksScreen, ps, Glyphs, vncHooksGlyphs);
   }
 #endif
 #ifdef RANDR
   rp = rrGetScrPriv(pScreen);
   if (rp) {
-    vncHooksScreen->RandRSetConfig = rp->rrSetConfig;
-    vncHooksScreen->RandRScreenSetSize = rp->rrScreenSetSize;
-    vncHooksScreen->RandRCrtcSet = rp->rrCrtcSet;
-  }
-#endif
-
-  pScreen->CloseScreen = vncHooksCloseScreen;
-  pScreen->CreateGC = vncHooksCreateGC;
-  pScreen->CopyWindow = vncHooksCopyWindow;
-  pScreen->ClearToBackground = vncHooksClearToBackground;
-#if XORG < 110
-  pScreen->RestoreAreas = vncHooksRestoreAreas;
-#endif
-  pScreen->DisplayCursor = vncHooksDisplayCursor;
-  pScreen->BlockHandler = vncHooksBlockHandler;
-#ifdef RENDER
-  if (ps) {
-    ps->Composite = vncHooksComposite;
-    ps->Glyphs = vncHooksGlyphs;
-  }
-#endif
-#ifdef RANDR
-  if (rp) {
-    /* Some RandR callbacks are optional */
-    if (rp->rrSetConfig)
-      rp->rrSetConfig = vncHooksRandRSetConfig;
-    if (rp->rrScreenSetSize)
-      rp->rrScreenSetSize = vncHooksRandRScreenSetSize;
-    if (rp->rrCrtcSet)
-      rp->rrCrtcSet = vncHooksRandRCrtcSet;
+    wrap(vncHooksScreen, rp, rrSetConfig, vncHooksRandRSetConfig);
+    wrap(vncHooksScreen, rp, rrScreenSetSize, vncHooksRandRScreenSetSize);
+    wrap(vncHooksScreen, rp, rrCrtcSet, vncHooksRandRCrtcSet);
   }
 #endif
 
@@ -433,15 +415,16 @@
 // screen functions
 //
 
-// SCREEN_UNWRAP and SCREEN_REWRAP unwrap and rewrap the given screen function.
+// Unwrap and rewrap helpers
 
-#define SCREEN_UNWRAP(scrn,field)                                         \
+#define SCREEN_PROLOGUE(scrn,field)                                       \
   ScreenPtr pScreen = scrn;                                               \
   vncHooksScreenPtr vncHooksScreen = vncHooksScreenPrivate(pScreen);      \
-  pScreen->field = vncHooksScreen->field;                                 \
+  unwrap(vncHooksScreen, pScreen, field);                                 \
   DBGPRINT((stderr,"vncHooks" #field " called\n"));
 
-#define SCREEN_REWRAP(field) pScreen->field = vncHooks##field;
+#define SCREEN_EPILOGUE(field)                                            \
+  wrap(vncHooksScreen, pScreen, field, vncHooks##field);                  \
 
 
 // CloseScreen - unwrap the screen functions and call the original CloseScreen
@@ -460,29 +443,29 @@
   rrScrPrivPtr rp;
 #endif
 
-  SCREEN_UNWRAP(pScreen_, CloseScreen);
+  SCREEN_PROLOGUE(pScreen_, CloseScreen);
 
-  pScreen->CreateGC = vncHooksScreen->CreateGC;
-  pScreen->CopyWindow = vncHooksScreen->CopyWindow;
-  pScreen->ClearToBackground = vncHooksScreen->ClearToBackground;
+  unwrap(vncHooksScreen, pScreen, CreateGC);
+  unwrap(vncHooksScreen, pScreen, CopyWindow);
+  unwrap(vncHooksScreen, pScreen, ClearToBackground);
 #if XORG < 110
-  pScreen->RestoreAreas = vncHooksScreen->RestoreAreas;
+  unwrap(vncHooksScreen, pScreen, RestoreAreas);
 #endif
-  pScreen->DisplayCursor = vncHooksScreen->DisplayCursor;
-  pScreen->BlockHandler = vncHooksScreen->BlockHandler;
+  unwrap(vncHooksScreen, pScreen, DisplayCursor);
+  unwrap(vncHooksScreen, pScreen, BlockHandler);
 #ifdef RENDER
   ps = GetPictureScreenIfSet(pScreen);
   if (ps) {
-    ps->Composite = vncHooksScreen->Composite;
-    ps->Glyphs = vncHooksScreen->Glyphs;
+    unwrap(vncHooksScreen, ps, Composite);
+    unwrap(vncHooksScreen, ps, Glyphs);
   }
 #endif
 #ifdef RANDR
   rp = rrGetScrPriv(pScreen);
   if (rp) {
-    rp->rrSetConfig = vncHooksScreen->RandRSetConfig;
-    rp->rrScreenSetSize = vncHooksScreen->RandRScreenSetSize;
-    rp->rrCrtcSet = vncHooksScreen->RandRCrtcSet;
+    unwrap(vncHooksScreen, rp, rrSetConfig);
+    unwrap(vncHooksScreen, rp, rrScreenSetSize);
+    unwrap(vncHooksScreen, rp, rrCrtcSet);
   }
 #endif
 
@@ -502,15 +485,15 @@
   vncHooksGCPtr vncHooksGC = vncHooksGCPrivate(pGC);
   Bool ret;
 
-  SCREEN_UNWRAP(pGC->pScreen, CreateGC);
+  SCREEN_PROLOGUE(pGC->pScreen, CreateGC);
 
   ret = (*pScreen->CreateGC) (pGC);
 
-  vncHooksGC->wrappedOps = 0;
-  vncHooksGC->wrappedFuncs = pGC->funcs;
+  vncHooksGC->ops = NULL;
+  vncHooksGC->funcs = pGC->funcs;
   pGC->funcs = &vncHooksGCFuncs;
 
-  SCREEN_REWRAP(CreateGC);
+  SCREEN_EPILOGUE(CreateGC);
 
   return ret;
 }
@@ -526,7 +509,7 @@
   BoxRec screen_box;
   RegionRec copied, screen_rgn;
 
-  SCREEN_UNWRAP(pWin->drawable.pScreen, CopyWindow);
+  SCREEN_PROLOGUE(pWin->drawable.pScreen, CopyWindow);
 
   REGION_NULL(pScreen, &copied);
   REGION_COPY(pScreen, &copied, pOldRegion);
@@ -558,7 +541,7 @@
   REGION_UNINIT(pScreen, &copied);
   REGION_UNINIT(pScreen, &screen_rgn);
 
-  SCREEN_REWRAP(CopyWindow);
+  SCREEN_EPILOGUE(CopyWindow);
 }
 
 // ClearToBackground - changed region is the given rectangle, clipped by
@@ -570,7 +553,7 @@
   BoxRec box;
   RegionRec reg;
 
-  SCREEN_UNWRAP(pWin->drawable.pScreen, ClearToBackground);
+  SCREEN_PROLOGUE(pWin->drawable.pScreen, ClearToBackground);
 
   box.x1 = x + pWin->drawable.x;
   box.y1 = y + pWin->drawable.y;
@@ -588,7 +571,7 @@
 
   REGION_UNINIT(pScreen, &reg);
 
-  SCREEN_REWRAP(ClearToBackground);
+  SCREEN_EPILOGUE(ClearToBackground);
 }
 
 #if XORG < 110
@@ -598,7 +581,7 @@
 {
   RegionRec reg;
 
-  SCREEN_UNWRAP(pWin->drawable.pScreen, RestoreAreas);
+  SCREEN_PROLOGUE(pWin->drawable.pScreen, RestoreAreas);
 
   REGION_NULL(pScreen, &reg);
   REGION_COPY(pScreen, &reg, pRegion);
@@ -609,7 +592,7 @@
 
   REGION_UNINIT(pScreen, &reg);
 
-  SCREEN_REWRAP(RestoreAreas);
+  SCREEN_EPILOGUE(RestoreAreas);
 
   return result;
 }
@@ -622,7 +605,7 @@
 {
   Bool ret;
 
-  SCREEN_UNWRAP(pScreen_, DisplayCursor);
+  SCREEN_PROLOGUE(pScreen_, DisplayCursor);
 
   ret = (*pScreen->DisplayCursor) (pDev, pScreen, cursor);
 
@@ -707,7 +690,7 @@
   }
 
 out:
-  SCREEN_REWRAP(DisplayCursor);
+  SCREEN_EPILOGUE(DisplayCursor);
 
   return ret;
 }
@@ -726,9 +709,9 @@
 #endif
 {
 #if XORG <= 112
-  SCREEN_UNWRAP(screenInfo.screens[i], BlockHandler);
+  SCREEN_PROLOGUE(screenInfo.screens[i], BlockHandler);
 #else
-  SCREEN_UNWRAP(pScreen_, BlockHandler);
+  SCREEN_PROLOGUE(pScreen_, BlockHandler);
 #endif
 
   vncHooksScreen->ignoreHooks++;
@@ -743,23 +726,33 @@
 
   vncHooksScreen->ignoreHooks--;
 
-  SCREEN_REWRAP(BlockHandler);
+  SCREEN_EPILOGUE(BlockHandler);
 }
 
 #ifdef RENDER
 
+// Unwrap and rewrap helpers
+
+#define RENDER_PROLOGUE(scrn,field)                                       \
+  ScreenPtr pScreen = scrn;                                               \
+  PictureScreenPtr ps = GetPictureScreen(pScreen);                        \
+  vncHooksScreenPtr vncHooksScreen = vncHooksScreenPrivate(pScreen);      \
+  unwrap(vncHooksScreen, ps, field);                                      \
+  DBGPRINT((stderr,"vncHooks" #field " called\n"));
+
+#define RENDER_EPILOGUE(field)                                            \
+  wrap(vncHooksScreen, ps, field, vncHooks##field);                       \
+
 // Composite - The core of XRENDER
 
 static void vncHooksComposite(CARD8 op, PicturePtr pSrc, PicturePtr pMask,
 		       PicturePtr pDst, INT16 xSrc, INT16 ySrc, INT16 xMask, 
 		       INT16 yMask, INT16 xDst, INT16 yDst, CARD16 width, CARD16 height)
 {
-  ScreenPtr pScreen = pDst->pDrawable->pScreen;
-  vncHooksScreenPtr vncHooksScreen = vncHooksScreenPrivate(pScreen);
-  PictureScreenPtr ps = GetPictureScreen(pScreen);
-
   RegionRec changed;
 
+  RENDER_PROLOGUE(pDst->pDrawable->pScreen, Composite);
+
   if (is_visible(pDst->pDrawable)) {
     BoxRec box;
     RegionRec fbreg;
@@ -784,15 +777,15 @@
   }
 
 
-  ps->Composite = vncHooksScreen->Composite;
   (*ps->Composite)(op, pSrc, pMask, pDst, xSrc, ySrc,
 		   xMask, yMask, xDst, yDst, width, height);
-  ps->Composite = vncHooksComposite;
 
   if (REGION_NOTEMPTY(pScreen, &changed))
     add_changed(pScreen, &changed);
 
   REGION_UNINIT(pScreen, &changed);
+
+  RENDER_EPILOGUE(Composite);
 }
 
 static int
@@ -849,12 +842,10 @@
            PictFormatPtr maskFormat, INT16 xSrc, INT16 ySrc, int nlists, 
            GlyphListPtr lists, GlyphPtr * glyphs)
 {
-  ScreenPtr pScreen = pDst->pDrawable->pScreen;
-  vncHooksScreenPtr vncHooksScreen = vncHooksScreenPrivate(pScreen);
-  PictureScreenPtr ps = GetPictureScreen(pScreen);
-
   RegionPtr changed;
 
+  RENDER_PROLOGUE(pDst->pDrawable->pScreen, Glyphs);
+
   if (is_visible(pDst->pDrawable)) {
     BoxRec fbbox;
     RegionRec fbreg;
@@ -876,37 +867,46 @@
     changed = REGION_CREATE(pScreen, NullBox, 0);
   }
 
-  ps->Glyphs = vncHooksScreen->Glyphs;
   (*ps->Glyphs)(op, pSrc, pDst, maskFormat, xSrc, ySrc, nlists, lists, glyphs);
-  ps->Glyphs = vncHooksGlyphs;
 
   if (REGION_NOTEMPTY(pScreen, changed))
     add_changed(pScreen, changed);
 
   REGION_DESTROY(pScreen, changed);
+
+  RENDER_EPILOGUE(Glyphs);
 }
 
 #endif /* RENDER */
 
-// RandRSetConfig - follow any framebuffer changes
-
 #ifdef RANDR
 
+// Unwrap and rewrap helpers
+
+#define RANDR_PROLOGUE(field)                                             \
+  rrScrPrivPtr rp = rrGetScrPriv(pScreen);                                \
+  vncHooksScreenPtr vncHooksScreen = vncHooksScreenPrivate(pScreen);      \
+  unwrap(vncHooksScreen, rp, rr##field);                                  \
+  DBGPRINT((stderr,"vncHooksRandR" #field " called\n"));
+
+#define RANDR_EPILOGUE(field)                                             \
+  wrap(vncHooksScreen, rp, rr##field, vncHooksRandR##field);              \
+
+// RandRSetConfig - follow any framebuffer changes
+
 static Bool vncHooksRandRSetConfig(ScreenPtr pScreen, Rotation rotation,
                                    int rate, RRScreenSizePtr pSize)
 {
-  vncHooksScreenPtr vncHooksScreen = vncHooksScreenPrivate(pScreen);
-  rrScrPrivPtr rp = rrGetScrPriv(pScreen);
   Bool ret;
 
+  RANDR_PROLOGUE(SetConfig);
+
   vncPreScreenResize(pScreen->myNum);
-
-  rp->rrSetConfig = vncHooksScreen->RandRSetConfig;
   ret = (*rp->rrSetConfig)(pScreen, rotation, rate, pSize);
-  rp->rrSetConfig = vncHooksRandRSetConfig;
-
   vncPostScreenResize(pScreen->myNum, ret, pScreen->width, pScreen->height);
 
+  RANDR_EPILOGUE(SetConfig);
+
   if (!ret)
     return FALSE;
 
@@ -917,18 +917,16 @@
                                        CARD16 width, CARD16 height,
                                        CARD32 mmWidth, CARD32 mmHeight)
 {
-  vncHooksScreenPtr vncHooksScreen = vncHooksScreenPrivate(pScreen);
-  rrScrPrivPtr rp = rrGetScrPriv(pScreen);
   Bool ret;
 
+  RANDR_PROLOGUE(ScreenSetSize);
+
   vncPreScreenResize(pScreen->myNum);
-
-  rp->rrScreenSetSize = vncHooksScreen->RandRScreenSetSize;
   ret = (*rp->rrScreenSetSize)(pScreen, width, height, mmWidth, mmHeight);
-  rp->rrScreenSetSize = vncHooksRandRScreenSetSize;
-
   vncPostScreenResize(pScreen->myNum, ret, pScreen->width, pScreen->height);
 
+  RANDR_EPILOGUE(ScreenSetSize);
+
   if (!ret)
     return FALSE;
 
@@ -940,14 +938,14 @@
                                  Rotation rotation, int num_outputs,
                                  RROutputPtr *outputs)
 {
-  vncHooksScreenPtr vncHooksScreen = vncHooksScreenPrivate(pScreen);
-  rrScrPrivPtr rp = rrGetScrPriv(pScreen);
   Bool ret;
 
-  rp->rrCrtcSet = vncHooksScreen->RandRCrtcSet;
+  RANDR_PROLOGUE(CrtcSet);
+
   ret = (*rp->rrCrtcSet)(pScreen, crtc, mode, x, y, rotation,
                          num_outputs, outputs);
-  rp->rrCrtcSet = vncHooksRandRCrtcSet;
+
+  RANDR_EPILOGUE(CrtcSet);
 
   if (!ret)
     return FALSE;
@@ -968,18 +966,13 @@
 
 #define GC_FUNC_PROLOGUE(pGC, name)\
     vncHooksGCPtr pGCPriv = vncHooksGCPrivate(pGC);\
-    (pGC)->funcs = pGCPriv->wrappedFuncs;\
-    if(pGCPriv->wrappedOps)\
-        (pGC)->ops = pGCPriv->wrappedOps; \
+    unwrap(pGCPriv, pGC, funcs);\
+    if (pGCPriv->ops) unwrap(pGCPriv, pGC, ops)\
     DBGPRINT((stderr,"vncHooks" #name " called\n"))
 
 #define GC_FUNC_EPILOGUE(pGC)\
-    pGCPriv->wrappedFuncs = (pGC)->funcs;\
-    (pGC)->funcs = &vncHooksGCFuncs;\
-    if(pGCPriv->wrappedOps) {\
-        pGCPriv->wrappedOps = (pGC)->ops;\
-        (pGC)->ops = &vncHooksGCOps;\
-    }
+    wrap(pGCPriv, pGC, funcs, &vncHooksGCFuncs);\
+    if (pGCPriv->ops) wrap(pGCPriv, pGC, ops, &vncHooksGCOps)
 
 // ValidateGC - wrap the "ops" if the drawable is on screen
 
@@ -989,10 +982,10 @@
   GC_FUNC_PROLOGUE(pGC, ValidateGC);
   (*pGC->funcs->ValidateGC) (pGC, changes, pDrawable);
   if (is_visible(pDrawable)) {
-    pGCPriv->wrappedOps = pGC->ops;
+    pGCPriv->ops = pGC->ops;
     DBGPRINT((stderr,"vncHooksValidateGC: wrapped GC ops\n"));
   } else {
-    pGCPriv->wrappedOps = NULL;
+    pGCPriv->ops = NULL;
   }
   GC_FUNC_EPILOGUE(pGC);
 }
@@ -1043,22 +1036,21 @@
 #define GC_OP_PROLOGUE(pGC, name)\
     vncHooksGCPtr pGCPriv = vncHooksGCPrivate(pGC);\
     const GCFuncs *oldFuncs = pGC->funcs;\
-    pGC->funcs = pGCPriv->wrappedFuncs;\
-    pGC->ops = pGCPriv->wrappedOps; \
+    unwrap(pGCPriv, pGC, funcs);\
+    unwrap(pGCPriv, pGC, ops);\
     DBGPRINT((stderr,"vncHooks" #name " called\n"))
 #else
 #define GC_OP_PROLOGUE(pGC, name)\
     vncHooksGCPtr pGCPriv = vncHooksGCPrivate(pGC);\
     GCFuncs *oldFuncs = pGC->funcs;\
-    pGC->funcs = pGCPriv->wrappedFuncs;\
-    pGC->ops = pGCPriv->wrappedOps; \
+    unwrap(pGCPriv, pGC, funcs);\
+    unwrap(pGCPriv, pGC, ops);\
     DBGPRINT((stderr,"vncHooks" #name " called\n"))
 #endif
 
 #define GC_OP_EPILOGUE(pGC)\
-    pGCPriv->wrappedOps = pGC->ops;\
-    pGC->funcs = oldFuncs;\
-    pGC->ops   = &vncHooksGCOps
+    wrap(pGCPriv, pGC, funcs, oldFuncs); \
+    wrap(pGCPriv, pGC, ops, &vncHooksGCOps)
 
 // FillSpans - assume the entire clip region is damaged. This is pessimistic,
 // but I believe this function is rarely used so it doesn't matter.
diff --git a/unix/xserver/hw/vnc/vncSelection.c b/unix/xserver/hw/vnc/vncSelection.c
index e50548a..6a8fdc3 100644
--- a/unix/xserver/hw/vnc/vncSelection.c
+++ b/unix/xserver/hw/vnc/vncSelection.c
@@ -230,23 +230,24 @@
     Atom targets[] = { xaTARGETS, xaTIMESTAMP,
                        xaSTRING, xaTEXT, xaUTF8_STRING };
 
-    rc = ChangeWindowProperty(pWin, realProperty, XA_ATOM, 32,
-                              PropModeReplace,
-                              sizeof(targets)/sizeof(targets[0]),
-                              targets, TRUE);
+    rc = dixChangeWindowProperty(serverClient, pWin, realProperty,
+                                 XA_ATOM, 32, PropModeReplace,
+                                 sizeof(targets)/sizeof(targets[0]),
+                                 targets, TRUE);
     if (rc != Success)
       return rc;
   } else if (target == xaTIMESTAMP) {
-    rc = ChangeWindowProperty(pWin, realProperty, XA_INTEGER, 32,
-                              PropModeReplace, 1,
-                              &pSel->lastTimeChanged.milliseconds,
-                              TRUE);
+    rc = dixChangeWindowProperty(serverClient, pWin, realProperty,
+                                 XA_INTEGER, 32, PropModeReplace, 1,
+                                 &pSel->lastTimeChanged.milliseconds,
+                                 TRUE);
     if (rc != Success)
       return rc;
   } else if ((target == xaSTRING) || (target == xaTEXT)) {
-    rc = ChangeWindowProperty(pWin, realProperty, XA_STRING, 8,
-                              PropModeReplace, clientCutTextLen,
-                              clientCutText, TRUE);
+    rc = dixChangeWindowProperty(serverClient, pWin, realProperty,
+                                 XA_STRING, 8, PropModeReplace,
+                                 clientCutTextLen, clientCutText,
+                                 TRUE);
     if (rc != Success)
       return rc;
   } else if (target == xaUTF8_STRING) {
@@ -279,8 +280,9 @@
       }
     }
 
-    rc = ChangeWindowProperty(pWin, realProperty, xaUTF8_STRING, 8,
-                              PropModeReplace, len, buffer, TRUE);
+    rc = dixChangeWindowProperty(serverClient, pWin, realProperty,
+                                 xaUTF8_STRING, 8, PropModeReplace,
+                                 len, buffer, TRUE);
     free(buffer);
     if (rc != Success)
       return rc;
diff --git a/unix/xserver/hw/vnc/xvnc.c b/unix/xserver/hw/vnc/xvnc.c
index c5b684d..ef30d69 100644
--- a/unix/xserver/hw/vnc/xvnc.c
+++ b/unix/xserver/hw/vnc/xvnc.c
@@ -572,9 +572,17 @@
 
     if (strcmp(argv[i], "-inetd") == 0)
     {
+	int nullfd;
+
 	dup2(0,3);
 	vncInetdSock = 3;
-	close(2);
+
+	/* Avoid xserver >= 1.19's epoll-fd becoming fd 2 / stderr only to be
+	   replaced by /dev/null by OsInit() because the pollfd is not
+	   writable, breaking ospoll_wait(). */
+	nullfd = open("/dev/null", O_WRONLY);
+	dup2(nullfd, 2);
+	close(nullfd);
 	
 	if (!displaySpecified) {
 	    int port = vncGetSocketPort(vncInetdSock);
diff --git a/vncviewer/CConn.cxx b/vncviewer/CConn.cxx
index 140b1d6..addc30d 100644
--- a/vncviewer/CConn.cxx
+++ b/vncviewer/CConn.cxx
@@ -73,7 +73,7 @@
 
 CConn::CConn(const char* vncServerName, network::Socket* socket=NULL)
   : serverHost(0), serverPort(0), desktop(NULL),
-    pendingPFChange(false),
+    frameCount(0), pixelCount(0), pendingPFChange(false),
     currentEncoding(encodingTight), lastServerEncoding((unsigned int)-1),
     formatChange(false), encodingChange(false),
     firstUpdate(true), pendingUpdate(false), continuousUpdates(false),
@@ -223,6 +223,21 @@
   return infoText;
 }
 
+unsigned CConn::getFrameCount()
+{
+  return frameCount;
+}
+
+unsigned CConn::getPixelCount()
+{
+  return pixelCount;
+}
+
+unsigned CConn::getPosition()
+{
+  return sock->inStream().pos();
+}
+
 // The RFB core is not properly asynchronous, so it calls this callback
 // whenever it needs to block to wait for more data. Since FLTK is
 // monitoring the socket, we just make sure FLTK gets to run.
@@ -254,6 +269,15 @@
     // until the buffers are empty or things will stall.
     do {
       cc->processMsg();
+
+      // Make sure that the FLTK handling and the timers gets some CPU
+      // time in case of back to back messages
+       Fl::check();
+       Timer::checkTimeouts();
+
+       // Also check if we need to stop reading and terminate
+       if (should_exit())
+         break;
     } while (cc->sock->inStream().checkNoWait(1));
   } catch (rdr::EndOfStream& e) {
     vlog.info("%s", e.str());
@@ -337,9 +361,6 @@
 // one.
 void CConn::framebufferUpdateStart()
 {
-  ModifiablePixelBuffer* pb;
-  PlatformPixelBuffer* ppb;
-
   CConnection::framebufferUpdateStart();
 
   // Note: This might not be true if sync fences are supported
@@ -347,22 +368,6 @@
 
   requestNewUpdate();
 
-  // We might still be rendering the previous update
-  pb = getFramebuffer();
-  assert(pb != NULL);
-  ppb = dynamic_cast<PlatformPixelBuffer*>(pb);
-  assert(ppb != NULL);
-  if (ppb->isRendering()) {
-    // Need to stop monitoring the socket or we'll just busy loop
-    assert(sock != NULL);
-    Fl::remove_fd(sock->getFd());
-
-    while (ppb->isRendering())
-      run_mainloop();
-
-    Fl::add_fd(sock->getFd(), FL_READ | FL_EXCEPT, socketEvent, this);
-  }
-
   // Update the screen prematurely for very slow updates
   Fl::add_timeout(1.0, handleUpdateTimeout, this);
 }
@@ -375,6 +380,8 @@
 {
   CConnection::framebufferUpdateEnd();
 
+  frameCount++;
+
   Fl::remove_timeout(handleUpdateTimeout, this);
   desktop->updateWindow();
 
@@ -397,11 +404,6 @@
   // Compute new settings based on updated bandwidth values
   if (autoSelect)
     autoSelectFormatAndEncoding();
-
-  // Make sure that the FLTK handling and the timers gets some CPU time
-  // in case of back to back framebuffer updates.
-  Fl::check();
-  Timer::checkTimeouts();
 }
 
 // The rest of the callbacks are fairly self-explanatory...
@@ -456,12 +458,14 @@
   CConnection::dataRect(r, encoding);
 
   sock->inStream().stopTiming();
+
+  pixelCount += r.area();
 }
 
 void CConn::setCursor(int width, int height, const Point& hotspot,
-                      void* data, void* mask)
+                      const rdr::U8* data)
 {
-  desktop->setCursor(width, height, hotspot, data, mask);
+  desktop->setCursor(width, height, hotspot, data);
 }
 
 void CConn::fence(rdr::U32 flags, unsigned len, const char data[])
diff --git a/vncviewer/CConn.h b/vncviewer/CConn.h
index c934f3d..93cc278 100644
--- a/vncviewer/CConn.h
+++ b/vncviewer/CConn.h
@@ -40,6 +40,10 @@
 
   const char *connectionInfo();
 
+  unsigned getFrameCount();
+  unsigned getPixelCount();
+  unsigned getPosition();
+
   // FdInStreamBlockCallback methods
   void blockCallback();
 
@@ -66,7 +70,7 @@
   void dataRect(const rfb::Rect& r, int encoding);
 
   void setCursor(int width, int height, const rfb::Point& hotspot,
-                 void* data, void* mask);
+                 const rdr::U8* data);
 
   void fence(rdr::U32 flags, unsigned len, const char data[]);
 
@@ -89,6 +93,9 @@
 
   DesktopWindow *desktop;
 
+  unsigned frameCount;
+  unsigned pixelCount;
+
   rfb::PixelFormat serverPF;
   rfb::PixelFormat fullColourPF;
 
diff --git a/vncviewer/CMakeLists.txt b/vncviewer/CMakeLists.txt
index f11bae3..1833e70 100644
--- a/vncviewer/CMakeLists.txt
+++ b/vncviewer/CMakeLists.txt
@@ -6,9 +6,9 @@
   menukey.cxx
   CConn.cxx
   DesktopWindow.cxx
-  FLTKPixelBuffer.cxx
   UserDialog.cxx
   ServerDialog.cxx
+  Surface.cxx
   OptionsDialog.cxx
   PlatformPixelBuffer.cxx
   Viewport.cxx
@@ -33,11 +33,11 @@
 endif()
 
 if(WIN32)
-  set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} Win32PixelBuffer.cxx)
+  set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} Surface_Win32.cxx)
 elseif(APPLE)
-  set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} OSXPixelBuffer.cxx)
+  set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} Surface_OSX.cxx)
 else()
-  set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} X11PixelBuffer.cxx)
+  set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} Surface_X11.cxx)
 endif()
 
 if(WIN32 AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
@@ -48,6 +48,10 @@
 
 target_link_libraries(vncviewer rfb network rdr os ${FLTK_LIBRARIES} ${GETTEXT_LIBRARIES})
 
+if(WIN32)
+  target_link_libraries(vncviewer msimg32)
+endif()
+
 if(APPLE)
   target_link_libraries(vncviewer "-framework Cocoa" "-framework Carbon")
 endif()
diff --git a/vncviewer/DesktopWindow.cxx b/vncviewer/DesktopWindow.cxx
index 2787bee..1f0f55f 100644
--- a/vncviewer/DesktopWindow.cxx
+++ b/vncviewer/DesktopWindow.cxx
@@ -24,6 +24,7 @@
 #include <assert.h>
 #include <stdio.h>
 #include <string.h>
+#include <sys/time.h>
 
 #include <rfb/LogWriter.h>
 #include <rfb/CMsgWriter.h>
@@ -34,10 +35,13 @@
 #include "parameters.h"
 #include "vncviewer.h"
 #include "CConn.h"
+#include "Surface.h"
 #include "Viewport.h"
 
 #include <FL/Fl.H>
-#include <FL/Fl_Scroll.H>
+#include <FL/Fl_Image_Surface.H>
+#include <FL/Fl_Scrollbar.H>
+#include <FL/fl_draw.H>
 #include <FL/x.H>
 
 #ifdef WIN32
@@ -58,18 +62,29 @@
 DesktopWindow::DesktopWindow(int w, int h, const char *name,
                              const rfb::PixelFormat& serverPF,
                              CConn* cc_)
-  : Fl_Window(w, h), cc(cc_), firstUpdate(true),
-    delayedFullscreen(false), delayedDesktopSize(false)
+  : Fl_Window(w, h), cc(cc_), offscreen(NULL), overlay(NULL),
+    firstUpdate(true),
+    delayedFullscreen(false), delayedDesktopSize(false),
+    statsLastFrame(0), statsLastPixels(0), statsLastPosition(0),
+    statsGraph(NULL)
 {
-  scroll = new Fl_Scroll(0, 0, w, h);
-  scroll->color(FL_BLACK);
+  Fl_Group* group;
 
-  // Automatically adjust the scroll box to the window
-  resizable(scroll);
+  // Dummy group to prevent FLTK from moving our widgets around
+  group = new Fl_Group(0, 0, w, h);
+  group->resizable(NULL);
+  resizable(group);
 
   viewport = new Viewport(w, h, serverPF, cc);
 
-  scroll->end();
+  // Position will be adjusted later
+  hscroll = new Fl_Scrollbar(0, 0, 0, 0);
+  vscroll = new Fl_Scrollbar(0, 0, 0, 0);
+  hscroll->type(FL_HORIZONTAL);
+  hscroll->callback(handleScroll, this);
+  vscroll->callback(handleScroll, this);
+
+  group->end();
 
   callback(handleClose, this);
 
@@ -150,11 +165,8 @@
   }
 #endif
 
-  // The window manager might give us an initial window size that is different
-  // than the one we requested, and in those cases we need to manually adjust
-  // the scroll widget for things to behave sanely.
-  if ((w != this->w()) || (h != this->h()))
-    scroll->size(this->w(), this->h());
+  // Adjust layout now that we're visible and know our final size
+  repositionWidgets();
 
   if (delayedFullscreen) {
     // Hack: Fullscreen requests may be ignored, so we need a timeout for
@@ -163,6 +175,15 @@
     Fl::add_timeout(0.5, handleFullscreenTimeout, this);
     fullscreen_on();
   }
+
+  // Throughput graph for debugging
+  if (vlog.getLevel() >= LogWriter::LEVEL_DEBUG) {
+    memset(&stats, 0, sizeof(stats));
+    Fl::add_timeout(0, handleStatsTimeout, this);
+  }
+
+  // Show hint about menu key
+  Fl::add_timeout(0.5, menuOverlay, this);
 }
 
 
@@ -174,9 +195,17 @@
   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;
+  delete offscreen;
+
+  delete statsGraph;
+
   // FLTK automatically deletes all child widgets, so we shouldn't touch
   // them ourselves here
 }
@@ -242,21 +271,123 @@
 
   viewport->size(new_w, new_h);
 
-  // We might not resize the main window, so we need to manually call this
-  // to make sure the viewport is centered.
-  repositionViewport();
-
-  // repositionViewport() makes sure the scroll widget notices any changes
-  // in position, but it might be just the size that changes so we also
-  // need a poke here as well.
-  redraw();
+  repositionWidgets();
 }
 
 
-void DesktopWindow::setCursor(int width, int height, const Point& hotspot,
-                              void* data, void* mask)
+void DesktopWindow::setCursor(int width, int height,
+                              const rfb::Point& hotspot,
+                              const rdr::U8* data)
 {
-  viewport->setCursor(width, height, hotspot, data, mask);
+  viewport->setCursor(width, height, hotspot, data);
+}
+
+
+void DesktopWindow::draw()
+{
+  bool redraw;
+
+  int X, Y, W, H;
+
+  // X11 needs an off screen buffer for compositing to avoid flicker,
+  // and alpha blending doesn't work for windows on Win32
+#if !defined(__APPLE__)
+
+  // Adjust offscreen surface dimensions
+  if ((offscreen == NULL) ||
+      (offscreen->width() != w()) || (offscreen->height() != h())) {
+    delete offscreen;
+    offscreen = new Surface(w(), h());
+  }
+
+#endif
+
+  // Active area inside scrollbars
+  W = w() - (vscroll->visible() ? vscroll->w() : 0);
+  H = h() - (hscroll->visible() ? hscroll->h() : 0);
+
+  // Full redraw?
+  redraw = (damage() & ~FL_DAMAGE_CHILD);
+
+  // Simplify the clip region to a simple rectangle in order to
+  // properly draw all the layers even if they only partially overlap
+  if (redraw)
+    X = Y = 0;
+  else
+    fl_clip_box(0, 0, W, H, X, Y, W, H);
+  fl_push_no_clip();
+  fl_push_clip(X, Y, W, H);
+
+  // Redraw background only on full redraws
+  if (redraw) {
+    if (offscreen)
+      offscreen->clear(40, 40, 40);
+    else
+      fl_rectf(0, 0, W, H, 40, 40, 40);
+  }
+
+  if (offscreen) {
+    viewport->draw(offscreen);
+    viewport->clear_damage();
+  } else {
+    if (redraw)
+      draw_child(*viewport);
+    else
+      update_child(*viewport);
+  }
+
+  // Debug graph (if active)
+  if (statsGraph) {
+    int ox, oy, ow, oh;
+
+    ox = X = w() - statsGraph->width() - 30;
+    oy = Y = h() - statsGraph->height() - 30;
+    ow = statsGraph->width();
+    oh = statsGraph->height();
+
+    fl_clip_box(ox, oy, ow, oh, ox, oy, ow, oh);
+
+    if (offscreen)
+      statsGraph->blend(offscreen, ox - X, oy - Y, ox, oy, ow, oh, 204);
+    else
+      statsGraph->blend(ox - X, oy - Y, ox, oy, ow, oh, 204);
+  }
+
+  // Overlay (if active)
+  if (overlay) {
+    int ox, oy, ow, oh;
+
+    ox = X = (w() - overlay->width()) / 2;
+    oy = Y = 50;
+    ow = overlay->width();
+    oh = overlay->height();
+
+    fl_clip_box(ox, oy, ow, oh, ox, oy, ow, oh);
+
+    if (offscreen)
+      overlay->blend(offscreen, ox - X, oy - Y, ox, oy, ow, oh, overlayAlpha);
+    else
+      overlay->blend(ox - X, oy - Y, ox, oy, ow, oh, overlayAlpha);
+  }
+
+  // Flush offscreen surface to screen
+  if (offscreen) {
+    fl_clip_box(0, 0, w(), h(), X, Y, W, H);
+    offscreen->draw(X, Y, X, Y, W, H);
+  }
+
+  fl_pop_clip();
+  fl_pop_clip();
+
+  // Finally the scrollbars
+
+  if (redraw) {
+    draw_child(*hscroll);
+    draw_child(*vscroll);
+  } else {
+    update_child(*hscroll);
+    update_child(*vscroll);
+  }
 }
 
 
@@ -334,27 +465,152 @@
       Fl::add_timeout(0.5, handleResizeTimeout, this);
     }
 
-    // Deal with some scrolling corner cases
-    repositionViewport();
+    repositionWidgets();
   }
 }
 
 
+void DesktopWindow::menuOverlay(void* data)
+{
+  DesktopWindow *self;
+
+  self = (DesktopWindow*)data;
+  self->setOverlay(_("Press %s to open the context menu"),
+                   (const char*)menuKey);
+}
+
+void DesktopWindow::setOverlay(const char* text, ...)
+{
+  va_list ap;
+  char textbuf[1024];
+
+  Fl_Image_Surface *surface;
+
+  Fl_RGB_Image* imageText;
+  Fl_RGB_Image* image;
+
+  unsigned char* buffer;
+
+  int x, y;
+  int w, h;
+
+  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);
+
+#if !defined(WIN32) && !defined(__APPLE__)
+  // FLTK < 1.3.5 crashes if fl_gc is unset
+  if (!fl_gc)
+    fl_gc = XDefaultGC(fl_display, 0);
+#endif
+
+  fl_font(FL_HELVETICA, FL_NORMAL_SIZE * 2);
+  w = 0;
+  fl_measure(textbuf, w, h);
+
+  // Margins
+  w += 80;
+  h += 40;
+
+  surface = new Fl_Image_Surface(w, h);
+  surface->set_current();
+
+  fl_rectf(0, 0, w, h, 0, 0, 0);
+
+  fl_font(FL_HELVETICA, FL_NORMAL_SIZE * 2);
+  fl_color(FL_WHITE);
+  fl_draw(textbuf, 40, 20 + fl_height() - fl_descent());
+
+  imageText = surface->image();
+  delete surface;
+
+  Fl_Display_Device::display_device()->set_current();
+
+  buffer = new unsigned char[w * h * 4];
+  image = new Fl_RGB_Image(buffer, w, h, 4);
+
+  a = buffer;
+  for (x = 0;x < image->w() * image->h();x++) {
+    a[0] = a[1] = a[2] = 0x40;
+    a[3] = 0xcc;
+    a += 4;
+  }
+
+  a = buffer;
+  b = (const unsigned char*)imageText->data()[0];
+  for (y = 0;y < h;y++) {
+    for (x = 0;x < w;x++) {
+      unsigned char alpha;
+      alpha = *b;
+      a[0] = (unsigned)a[0] * (255 - alpha) / 255 + alpha;
+      a[1] = (unsigned)a[1] * (255 - alpha) / 255 + alpha;
+      a[2] = (unsigned)a[2] * (255 - alpha) / 255 + alpha;
+      a[3] = 255 - (255 - a[3]) * (255 - alpha) / 255;
+      a += 4;
+      b += imageText->d();
+    }
+    if (imageText->ld() != 0)
+      b += imageText->ld() - w * imageText->d();
+  }
+
+  delete imageText;
+
+  x = (this->w() - image->w()) / 2;
+  y = 50;
+  w = image->w();
+  h = image->h();
+
+  overlay = new Surface(image);
+  overlayAlpha = 0;
+  gettimeofday(&overlayStart, NULL);
+
+  delete image;
+
+  Fl::add_timeout(1.0/60, updateOverlay, this);
+}
+
+void DesktopWindow::updateOverlay(void *data)
+{
+  DesktopWindow *self;
+  unsigned elapsed;
+
+  self = (DesktopWindow*)data;
+
+  elapsed = msSince(&self->overlayStart);
+
+  if (elapsed < 500) {
+    self->overlayAlpha = (unsigned)255 * elapsed / 500;
+    Fl::add_timeout(1.0/60, updateOverlay, self);
+  } else if (elapsed < 3500) {
+    self->overlayAlpha = 255;
+    Fl::add_timeout(3.0, updateOverlay, self);
+  } else if (elapsed < 4000) {
+    self->overlayAlpha = (unsigned)255 * (4000 - elapsed) / 500;
+    Fl::add_timeout(1.0/60, updateOverlay, self);
+  } else {
+    delete self->overlay;
+    self->overlay = NULL;
+  }
+
+  self->damage(FL_DAMAGE_USER1);
+}
+
+
 int DesktopWindow::handle(int event)
 {
   switch (event) {
   case FL_FULLSCREEN:
     fullScreen.setParam(fullscreen_active());
 
-    if (fullscreen_active())
-      scroll->type(0);
-    else
-      scroll->type(Fl_Scroll::BOTH);
-
-    // The scroll widget isn't clever enough to actually redraw the
-    // scroll bars when they are added/removed, so we need to give
-    // it a push.
-    scroll->redraw();
+    // Update scroll bars
+    repositionWidgets();
 
     if (!fullscreenSystemKeys)
       break;
@@ -667,7 +923,7 @@
     int i;
     rdr::U32 id;
     int sx, sy, sw, sh;
-    Rect viewport_rect, screen_rect;
+    rfb::Rect viewport_rect, screen_rect;
 
     // In full screen we report all screens that are fully covered.
 
@@ -751,17 +1007,11 @@
 }
 
 
-void DesktopWindow::repositionViewport()
+void DesktopWindow::repositionWidgets()
 {
   int new_x, new_y;
 
-  // Deal with some scrolling corner cases:
-  //
-  // a) If the window is larger then the viewport, center the viewport.
-  // b) If the window is smaller than the viewport, make sure there is
-  //    no wasted space on the sides.
-  //
-  // FIXME: Doesn't compensate for scroll widget size properly.
+  // Viewport position
 
   new_x = viewport->x();
   new_y = viewport->y();
@@ -787,11 +1037,40 @@
 
   if ((new_x != viewport->x()) || (new_y != viewport->y())) {
     viewport->position(new_x, new_y);
-
-    // The scroll widget does not notice when you move around child widgets,
-    // so redraw everything to make sure things update.
-    redraw();
+    damage(FL_DAMAGE_SCROLL);
   }
+
+  // Scrollbars visbility
+
+  if (!fullscreen_active() && (w() < viewport->w()))
+    hscroll->show();
+  else
+    hscroll->hide();
+
+  if (!fullscreen_active() && (h() < viewport->h()))
+    vscroll->show();
+  else
+    vscroll->hide();
+
+  // Scrollbars positions
+
+  hscroll->resize(0, h() - Fl::scrollbar_size(),
+                  w() - (vscroll->visible() ? Fl::scrollbar_size() : 0),
+                  Fl::scrollbar_size());
+  vscroll->resize(w() - Fl::scrollbar_size(), 0,
+                  Fl::scrollbar_size(),
+                  h() - (hscroll->visible() ? Fl::scrollbar_size() : 0));
+
+  // Scrollbars range
+
+  hscroll->value(-viewport->x(),
+                 w() - (vscroll->visible() ? vscroll->w() : 0),
+                 0, viewport->w());
+  vscroll->value(-viewport->y(),
+                 h() - (hscroll->visible() ? hscroll->h() : 0),
+                 0, viewport->h());
+  hscroll->value(hscroll->clamp(hscroll->value()));
+  vscroll->value(vscroll->clamp(vscroll->value()));
 }
 
 void DesktopWindow::handleClose(Fl_Widget *wnd, void *data)
@@ -829,6 +1108,38 @@
   }
 }
 
+void DesktopWindow::scrollTo(int x, int y)
+{
+  x = hscroll->clamp(x);
+  y = vscroll->clamp(y);
+
+  hscroll->value(x);
+  vscroll->value(y);
+
+  if (!hscroll->visible())
+    x = -viewport->x();
+  if (!vscroll->visible())
+    y = -viewport->y();
+
+  // Scrollbar position results in inverse movement of
+  // the viewport widget
+  x = -x;
+  y = -y;
+
+  if ((viewport->x() == x) && (viewport->y() == y))
+    return;
+
+  viewport->position(x, y);
+  damage(FL_DAMAGE_SCROLL);
+}
+
+void DesktopWindow::handleScroll(Fl_Widget *widget, void *data)
+{
+  DesktopWindow *self = (DesktopWindow *)data;
+
+  self->scrollTo(self->hscroll->value(), self->vscroll->value());
+}
+
 void DesktopWindow::handleEdgeScroll(void *data)
 {
   DesktopWindow *self = (DesktopWindow *)data;
@@ -874,17 +1185,132 @@
   if ((dx == 0) && (dy == 0))
     return;
 
-  // Make sure we don't move the viewport too much
-  if (self->viewport->x() + dx > 0)
-    dx = -self->viewport->x();
-  if (self->viewport->x() + dx + self->viewport->w() < self->w())
-    dx = self->w() - (self->viewport->x() + self->viewport->w());
-  if (self->viewport->y() + dy > 0)
-    dy = -self->viewport->y();
-  if (self->viewport->y() + dy + self->viewport->h() < self->h())
-    dy = self->h() - (self->viewport->y() + self->viewport->h());
-
-  self->scroll->scroll_to(self->scroll->xposition() - dx, self->scroll->yposition() - dy);
+  self->scrollTo(self->hscroll->value() + dx, self->vscroll->value() + dy);
 
   Fl::repeat_timeout(0.1, handleEdgeScroll, data);
 }
+
+void DesktopWindow::handleStatsTimeout(void *data)
+{
+  DesktopWindow *self = (DesktopWindow*)data;
+
+  const size_t statsCount = sizeof(stats)/sizeof(stats[0]);
+
+  unsigned frame, pixels, pos;
+  unsigned elapsed;
+
+  const unsigned statsWidth = 200;
+  const unsigned statsHeight = 100;
+  const unsigned graphWidth = statsWidth - 10;
+  const unsigned graphHeight = statsHeight - 25;
+
+  Fl_Image_Surface *surface;
+  Fl_RGB_Image *image;
+
+  unsigned maxFPS, maxPPS, maxBPS;
+  size_t i;
+
+  char buffer[256];
+
+  frame = self->cc->getFrameCount();
+  pixels = self->cc->getPixelCount();
+  pos = self->cc->getPosition();
+  elapsed = msSince(&self->statsLastTime);
+  if (elapsed < 1)
+    elapsed = 1;
+
+  memmove(&self->stats[0], &self->stats[1], sizeof(stats[0])*(statsCount-1));
+
+  self->stats[statsCount-1].fps = (frame - self->statsLastFrame) * 1000 / elapsed;
+  self->stats[statsCount-1].pps = (pixels - self->statsLastPixels) * 1000 / elapsed;
+  self->stats[statsCount-1].bps = (pos - self->statsLastPosition) * 1000 / elapsed;
+
+  gettimeofday(&self->statsLastTime, NULL);
+  self->statsLastFrame = frame;
+  self->statsLastPixels = pixels;
+  self->statsLastPosition = pos;
+
+#if !defined(WIN32) && !defined(__APPLE__)
+  // FLTK < 1.3.5 crashes if fl_gc is unset
+  if (!fl_gc)
+    fl_gc = XDefaultGC(fl_display, 0);
+#endif
+
+  surface = new Fl_Image_Surface(statsWidth, statsHeight);
+  surface->set_current();
+
+  fl_rectf(0, 0, statsWidth, statsHeight, FL_BLACK);
+
+  fl_rect(5, 5, graphWidth, graphHeight, FL_WHITE);
+
+  maxFPS = maxPPS = maxBPS = 0;
+  for (i = 0;i < statsCount;i++) {
+    if (self->stats[i].fps > maxFPS)
+      maxFPS = self->stats[i].fps;
+    if (self->stats[i].pps > maxPPS)
+      maxPPS = self->stats[i].pps;
+    if (self->stats[i].bps > maxBPS)
+      maxBPS = self->stats[i].bps;
+  }
+
+  if (maxFPS != 0) {
+    fl_color(FL_GREEN);
+    for (i = 0;i < statsCount-1;i++) {
+      fl_line(5 + i * graphWidth / statsCount,
+              5 + graphHeight - graphHeight * self->stats[i].fps / maxFPS,
+              5 + (i+1) * graphWidth / statsCount,
+              5 + graphHeight - graphHeight * self->stats[i+1].fps / maxFPS);
+    }
+  }
+
+  if (maxPPS != 0) {
+    fl_color(FL_YELLOW);
+    for (i = 0;i < statsCount-1;i++) {
+      fl_line(5 + i * graphWidth / statsCount,
+              5 + graphHeight - graphHeight * self->stats[i].pps / maxPPS,
+              5 + (i+1) * graphWidth / statsCount,
+              5 + graphHeight - graphHeight * self->stats[i+1].pps / maxPPS);
+    }
+  }
+
+  if (maxBPS != 0) {
+    fl_color(FL_RED);
+    for (i = 0;i < statsCount-1;i++) {
+      fl_line(5 + i * graphWidth / statsCount,
+              5 + graphHeight - graphHeight * self->stats[i].bps / maxBPS,
+              5 + (i+1) * graphWidth / statsCount,
+              5 + graphHeight - graphHeight * self->stats[i+1].bps / maxBPS);
+    }
+  }
+
+  fl_font(FL_HELVETICA, 10);
+
+  fl_color(FL_GREEN);
+  snprintf(buffer, sizeof(buffer), "%u fps", self->stats[statsCount-1].fps);
+  fl_draw(buffer, 5, statsHeight - 5);
+
+  fl_color(FL_YELLOW);
+  siPrefix(self->stats[statsCount-1].pps * 8, "pix/s",
+           buffer, sizeof(buffer), 3);
+  fl_draw(buffer, 5 + (statsWidth-10)/3, statsHeight - 5);
+
+  fl_color(FL_RED);
+  iecPrefix(self->stats[statsCount-1].bps * 8, "Bps",
+            buffer, sizeof(buffer), 3);
+  fl_draw(buffer, 5 + (statsWidth-10)*2/3, statsHeight - 5);
+
+  image = surface->image();
+  delete surface;
+
+  Fl_Display_Device::display_device()->set_current();
+
+  delete self->statsGraph;
+  self->statsGraph = new Surface(image);
+  delete image;
+
+  self->damage(FL_DAMAGE_CHILD, self->w() - statsWidth - 30,
+               self->h() - statsHeight - 30,
+               statsWidth, statsHeight);
+
+  Fl::repeat_timeout(0.5, handleStatsTimeout, data);
+}
diff --git a/vncviewer/DesktopWindow.h b/vncviewer/DesktopWindow.h
index d755b6d..4224699 100644
--- a/vncviewer/DesktopWindow.h
+++ b/vncviewer/DesktopWindow.h
@@ -22,6 +22,8 @@
 
 #include <map>
 
+#include <sys/time.h>
+
 #include <rfb/Rect.h>
 #include <rfb/Pixel.h>
 
@@ -30,9 +32,16 @@
 namespace rfb { class ModifiablePixelBuffer; }
 
 class CConn;
+class Surface;
 class Viewport;
 
-class Fl_Scroll;
+class Fl_Scrollbar;
+
+#ifdef __GNUC__
+#  define __printf_attr(a, b) __attribute__((__format__ (__printf__, a, b)))
+#else
+#  define __printf_attr(a, b)
+#endif // __GNUC__
 
 class DesktopWindow : public Fl_Window {
 public:
@@ -55,9 +64,10 @@
 
   // New image for the locally rendered cursor
   void setCursor(int width, int height, const rfb::Point& hotspot,
-                 void* data, void* mask);
+                 const rdr::U8* data);
 
   // Fl_Window callback methods
+  void draw();
   void resize(int x, int y, int w, int h);
 
   int handle(int event);
@@ -65,6 +75,11 @@
   void fullscreen_on();
 
 private:
+  static void menuOverlay(void *data);
+
+  void setOverlay(const char *text, ...) __printf_attr(2, 3);
+  static void updateOverlay(void *data);
+
   static int fltkHandle(int event, Fl_Window *win);
 
   void grabKeyboard();
@@ -78,7 +93,7 @@
   static void handleResizeTimeout(void *data);
   void remoteResize(int width, int height);
 
-  void repositionViewport();
+  void repositionWidgets();
 
   static void handleClose(Fl_Widget *wnd, void *data);
 
@@ -86,16 +101,38 @@
 
   static void handleFullscreenTimeout(void *data);
 
+  void scrollTo(int x, int y);
+  static void handleScroll(Fl_Widget *wnd, void *data);
   static void handleEdgeScroll(void *data);
 
+  static void handleStatsTimeout(void *data);
+
 private:
   CConn* cc;
-  Fl_Scroll *scroll;
+  Fl_Scrollbar *hscroll, *vscroll;
   Viewport *viewport;
+  Surface *offscreen;
+  Surface *overlay;
+  unsigned char overlayAlpha;
+  struct timeval overlayStart;
 
   bool firstUpdate;
   bool delayedFullscreen;
   bool delayedDesktopSize;
+
+  struct statsEntry {
+    unsigned fps;
+    unsigned pps;
+    unsigned bps;
+  };
+  struct statsEntry stats[100];
+
+  struct timeval statsLastTime;
+  unsigned statsLastFrame;
+  unsigned statsLastPixels;
+  unsigned statsLastPosition;
+
+  Surface *statsGraph;
 };
 
 #endif
diff --git a/vncviewer/OSXPixelBuffer.cxx b/vncviewer/OSXPixelBuffer.cxx
deleted file mode 100644
index e9100f2..0000000
--- a/vncviewer/OSXPixelBuffer.cxx
+++ /dev/null
@@ -1,111 +0,0 @@
-/* Copyright 2011-2014 Pierre Ossman for Cendio AB
- * 
- * This is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- * 
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with this software; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
- * USA.
- */
-
-#ifdef HAVE_CONFIG_H
-#include <config.h>
-#endif
-
-#include <ApplicationServices/ApplicationServices.h>
-
-#include <FL/Fl_Window.H>
-#include <FL/x.H>
-
-#include <rfb/LogWriter.h>
-#include <rfb/Exception.h>
-
-#include "i18n.h"
-#include "OSXPixelBuffer.h"
-
-using namespace rfb;
-
-static rfb::LogWriter vlog("OSXPixelBuffer");
-
-OSXPixelBuffer::OSXPixelBuffer(int width, int height) :
-  PlatformPixelBuffer(rfb::PixelFormat(32, 24, false, true,
-                                       255, 255, 255, 16, 8, 0),
-                      width, height, NULL, width),
-  bitmap(NULL)
-{
-  CGColorSpaceRef lut;
-
-  data = new rdr::U8[width * height * format.bpp/8];
-  if (data == NULL)
-    throw rfb::Exception(_("Not enough memory for framebuffer"));
-
-  lut = CGColorSpaceCreateDeviceRGB();
-  if (!lut)
-    throw rfb::Exception(_("Could not create framebuffer device"));
-
-  bitmap = CGBitmapContextCreate(data, width, height, 8, width*4, lut,
-                                 kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little);
-  CGColorSpaceRelease(lut);
-  if (!bitmap)
-    throw rfb::Exception(_("Could not create framebuffer bitmap"));
-}
-
-
-OSXPixelBuffer::~OSXPixelBuffer()
-{
-  CFRelease((CGContextRef)bitmap);
-  delete [] data;
-}
-
-
-void OSXPixelBuffer::draw(int src_x, int src_y, int x, int y, int w, int h)
-{
-  CGRect rect;
-  CGContextRef gc;
-  CGAffineTransform at;
-  CGImageRef image;
-
-  gc = (CGContextRef)fl_gc;
-
-  CGContextSaveGState(gc);
-
-  // We have to use clipping to partially display an image
-
-  rect.origin.x = x - 0.5;
-  rect.origin.y = y - 0.5;
-  rect.size.width = w;
-  rect.size.height = h;
-
-  CGContextClipToRect(gc, rect);
- 
-  // Oh the hackiness that is OS X image handling...
-  // The CGContextDrawImage() routine magically flips the images and offsets
-  // them by 0.5,0.5 in order to compensate for OS X unfamiliar coordinate
-  // system. But this breaks horribly when you set up the CTM to get the
-  // more familiar top-down system (which FLTK does), meaning we have to
-  // reset the CTM back to the identity matrix and calculate new origin
-  // coordinates.
-
-  at = CGContextGetCTM(gc);
-  CGContextScaleCTM(gc, 1, -1);
-  CGContextTranslateCTM(gc, -at.tx, -at.ty);
-
-  rect.origin.x = x - src_x;
-  rect.origin.y = Fl_Window::current()->h() - (y - src_y);
-  rect.size.width = width();
-  rect.size.height = -height(); // Negative height does _not_ flip the image
-
-  image = CGBitmapContextCreateImage((CGContextRef)bitmap);
-  CGContextDrawImage(gc, rect, image);
-  CGImageRelease(image);
-
-  CGContextRestoreGState(gc);
-}
diff --git a/vncviewer/OSXPixelBuffer.h b/vncviewer/OSXPixelBuffer.h
deleted file mode 100644
index 8ae0184..0000000
--- a/vncviewer/OSXPixelBuffer.h
+++ /dev/null
@@ -1,37 +0,0 @@
-/* Copyright 2011-2014 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 __OSXPIXELBUFFER_H__
-#define __OSXPIXELBUFFER_H__
-
-#include "PlatformPixelBuffer.h"
-
-class OSXPixelBuffer: public PlatformPixelBuffer {
-public:
-  OSXPixelBuffer(int width, int height);
-  ~OSXPixelBuffer();
-
-  virtual void draw(int src_x, int src_y, int x, int y, int w, int h);
-
-protected:
-  // This is really a CGContextRef, but Apple headers conflict with FLTK
-  void *bitmap;
-};
-
-
-#endif
diff --git a/vncviewer/OptionsDialog.cxx b/vncviewer/OptionsDialog.cxx
index 2f9df9c..b018c95 100644
--- a/vncviewer/OptionsDialog.cxx
+++ b/vncviewer/OptionsDialog.cxx
@@ -700,6 +700,7 @@
                                                          CHECK_MIN_WIDTH,
                                                          CHECK_HEIGHT,
                                                          _("Accept clipboard from server")));
+  acceptClipboardCheckbox->callback(handleClipboard, this);
   ty += CHECK_HEIGHT + TIGHT_MARGIN;
 
 #if !defined(WIN32) && !defined(__APPLE__)
@@ -714,10 +715,11 @@
                                                        CHECK_MIN_WIDTH,
                                                        CHECK_HEIGHT,
                                                        _("Send clipboard to server")));
+  sendClipboardCheckbox->callback(handleClipboard, this);
   ty += CHECK_HEIGHT + TIGHT_MARGIN;
 
 #if !defined(WIN32) && !defined(__APPLE__)
-  sendPrimaryCheckbox = new Fl_Check_Button(LBLRIGHT(tx, ty,
+  sendPrimaryCheckbox = new Fl_Check_Button(LBLRIGHT(tx + INDENT, ty,
                                                      CHECK_MIN_WIDTH,
                                                      CHECK_HEIGHT,
                                                      _("Send primary selection as clipboard")));
@@ -876,6 +878,22 @@
   }
 }
 
+void OptionsDialog::handleClipboard(Fl_Widget *widget, void *data)
+{
+#if !defined(WIN32) && !defined(__APPLE__)
+  OptionsDialog *dialog = (OptionsDialog*)data;
+
+  if (dialog->acceptClipboardCheckbox->value())
+    dialog->setPrimaryCheckbox->activate();
+  else
+    dialog->setPrimaryCheckbox->deactivate();
+  if (dialog->sendClipboardCheckbox->value())
+    dialog->sendPrimaryCheckbox->activate();
+  else
+    dialog->sendPrimaryCheckbox->deactivate();
+#endif
+}
+
 void OptionsDialog::handleCancel(Fl_Widget *widget, void *data)
 {
   OptionsDialog *dialog = (OptionsDialog*)data;
diff --git a/vncviewer/OptionsDialog.h b/vncviewer/OptionsDialog.h
index 6984c72..c177560 100644
--- a/vncviewer/OptionsDialog.h
+++ b/vncviewer/OptionsDialog.h
@@ -64,6 +64,8 @@
 
   static void handleDesktopSize(Fl_Widget *widget, void *data);
 
+  static void handleClipboard(Fl_Widget *widget, void *data);
+
   static void handleCancel(Fl_Widget *widget, void *data);
   static void handleOK(Fl_Widget *widget, void *data);
 
diff --git a/vncviewer/PlatformPixelBuffer.cxx b/vncviewer/PlatformPixelBuffer.cxx
index 876ab94..4802ba4 100644
--- a/vncviewer/PlatformPixelBuffer.cxx
+++ b/vncviewer/PlatformPixelBuffer.cxx
@@ -1,4 +1,4 @@
-/* Copyright 2011-2014 Pierre Ossman for Cendio AB
+/* Copyright 2011-2016 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
@@ -16,13 +16,70 @@
  * USA.
  */
 
+#include <assert.h>
+
+#if !defined(WIN32) && !defined(__APPLE__)
+#include <sys/ipc.h>
+#include <sys/shm.h>
+#endif
+
+#include <FL/Fl.H>
+#include <FL/x.H>
+
+#include <rfb/LogWriter.h>
+#include <rdr/Exception.h>
+
 #include "PlatformPixelBuffer.h"
 
-PlatformPixelBuffer::PlatformPixelBuffer(const rfb::PixelFormat& pf,
-                                         int width, int height,
-                                         rdr::U8* data, int stride) :
-  FullFramePixelBuffer(pf, width, height, data, stride)
+static rfb::LogWriter vlog("PlatformPixelBuffer");
+
+PlatformPixelBuffer::PlatformPixelBuffer(int width, int height) :
+  FullFramePixelBuffer(rfb::PixelFormat(32, 24, false, true,
+                                       255, 255, 255, 16, 8, 0),
+                       width, height, 0, stride),
+  Surface(width, height)
+#if !defined(WIN32) && !defined(__APPLE__)
+  , shminfo(NULL), xim(NULL)
+#endif
 {
+#if !defined(WIN32) && !defined(__APPLE__)
+  if (!setupShm()) {
+    xim = XCreateImage(fl_display, CopyFromParent, 32,
+                       ZPixmap, 0, 0, width, height, 32, 0);
+    if (!xim)
+      throw rdr::Exception("XCreateImage");
+
+    xim->data = (char*)malloc(xim->bytes_per_line * xim->height);
+    if (!xim->data)
+      throw rdr::Exception("malloc");
+
+    vlog.debug("Using standard XImage");
+  }
+
+  data = (rdr::U8*)xim->data;
+  stride = xim->bytes_per_line / (getPF().bpp/8);
+#else
+  FullFramePixelBuffer::data = (rdr::U8*)Surface::data;
+  stride = width;
+#endif
+}
+
+PlatformPixelBuffer::~PlatformPixelBuffer()
+{
+#if !defined(WIN32) && !defined(__APPLE__)
+  if (shminfo) {
+    vlog.debug("Freeing shared memory XImage");
+    shmdt(shminfo->shmaddr);
+    shmctl(shminfo->shmid, IPC_RMID, 0);
+    delete shminfo;
+    shminfo = NULL;
+  }
+
+  // XDestroyImage() will free(xim->data) if appropriate
+  if (xim)
+    XDestroyImage(xim);
+  xim = NULL;
+#endif
 }
 
 void PlatformPixelBuffer::commitBufferRW(const rfb::Rect& r)
@@ -42,10 +99,104 @@
   damage.clear();
   mutex.unlock();
 
+#if !defined(WIN32) && !defined(__APPLE__)
+  GC gc;
+
+  gc = XCreateGC(fl_display, pixmap, 0, NULL);
+  if (shminfo) {
+    XShmPutImage(fl_display, pixmap, gc, xim,
+                 r.tl.x, r.tl.y, r.tl.x, r.tl.y,
+                 r.width(), r.height(), False);
+    // Need to make sure the X server has finished reading the
+    // shared memory before we return
+    XSync(fl_display, False);
+  } else {
+    XPutImage(fl_display, pixmap, gc, xim,
+              r.tl.x, r.tl.y, r.tl.x, r.tl.y, r.width(), r.height());
+  }
+  XFreeGC(fl_display, gc);
+#endif
+
   return r;
 }
 
-bool PlatformPixelBuffer::isRendering(void)
+#if !defined(WIN32) && !defined(__APPLE__)
+
+static bool caughtError;
+
+static int XShmAttachErrorHandler(Display *dpy, XErrorEvent *error)
 {
-  return false;
+  caughtError = true;
+  return 0;
 }
+
+bool PlatformPixelBuffer::setupShm()
+{
+  int major, minor;
+  Bool pixmaps;
+  XErrorHandler old_handler;
+  const char *display_name = XDisplayName (NULL);
+
+  /* Don't use MIT-SHM on remote displays */
+  if (*display_name && *display_name != ':')
+    return false;
+
+  if (!XShmQueryVersion(fl_display, &major, &minor, &pixmaps))
+    return false;
+
+  shminfo = new XShmSegmentInfo;
+
+  xim = XShmCreateImage(fl_display, CopyFromParent, 32,
+                        ZPixmap, 0, shminfo, width(), height());
+  if (!xim)
+    goto free_shminfo;
+
+  shminfo->shmid = shmget(IPC_PRIVATE,
+                          xim->bytes_per_line * xim->height,
+                          IPC_CREAT|0777);
+  if (shminfo->shmid == -1)
+    goto free_xim;
+
+  shminfo->shmaddr = xim->data = (char*)shmat(shminfo->shmid, 0, 0);
+  shmctl(shminfo->shmid, IPC_RMID, 0); // to avoid memory leakage
+  if (shminfo->shmaddr == (char *)-1)
+    goto free_xim;
+
+  shminfo->readOnly = True;
+
+  // This is the only way we can detect that shared memory won't work
+  // (e.g. because we're accessing a remote X11 server)
+  caughtError = false;
+  old_handler = XSetErrorHandler(XShmAttachErrorHandler);
+
+  if (!XShmAttach(fl_display, shminfo)) {
+    XSetErrorHandler(old_handler);
+    goto free_shmaddr;
+  }
+
+  XSync(fl_display, False);
+
+  XSetErrorHandler(old_handler);
+
+  if (caughtError)
+    goto free_shmaddr;
+
+  vlog.debug("Using shared memory XImage");
+
+  return true;
+
+free_shmaddr:
+  shmdt(shminfo->shmaddr);
+
+free_xim:
+  XDestroyImage(xim);
+  xim = NULL;
+
+free_shminfo:
+  delete shminfo;
+  shminfo = NULL;
+
+  return 0;
+}
+
+#endif
diff --git a/vncviewer/PlatformPixelBuffer.h b/vncviewer/PlatformPixelBuffer.h
index 9f0e3b1..f9038cd 100644
--- a/vncviewer/PlatformPixelBuffer.h
+++ b/vncviewer/PlatformPixelBuffer.h
@@ -1,4 +1,4 @@
-/* Copyright 2011-2014 Pierre Ossman for Cendio AB
+/* Copyright 2011-2016 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
@@ -19,26 +19,46 @@
 #ifndef __PLATFORMPIXELBUFFER_H__
 #define __PLATFORMPIXELBUFFER_H__
 
+#if !defined(WIN32) && !defined(__APPLE__)
+#include <X11/Xlib.h>
+#include <sys/ipc.h>
+#include <sys/shm.h>
+#include <X11/extensions/XShm.h>
+#endif
+
+#include <list>
+
 #include <os/Mutex.h>
 
 #include <rfb/PixelBuffer.h>
 #include <rfb/Region.h>
 
-class PlatformPixelBuffer: public rfb::FullFramePixelBuffer {
+#include "Surface.h"
+
+class PlatformPixelBuffer: public rfb::FullFramePixelBuffer, public Surface {
 public:
-  PlatformPixelBuffer(const rfb::PixelFormat& pf, int width, int height,
-                      rdr::U8* data, int stride);
+  PlatformPixelBuffer(int width, int height);
+  ~PlatformPixelBuffer();
 
   virtual void commitBufferRW(const rfb::Rect& r);
 
-  virtual void draw(int src_x, int src_y, int x, int y, int w, int h) = 0;
   rfb::Rect getDamage(void);
 
-  virtual bool isRendering(void);
+  using rfb::FullFramePixelBuffer::width;
+  using rfb::FullFramePixelBuffer::height;
 
 protected:
   os::Mutex mutex;
   rfb::Region damage;
+
+#if !defined(WIN32) && !defined(__APPLE__)
+protected:
+  bool setupShm();
+
+protected:
+  XShmSegmentInfo *shminfo;
+  XImage *xim;
+#endif
 };
 
 #endif
diff --git a/vncviewer/Win32PixelBuffer.h b/vncviewer/Surface.cxx
similarity index 63%
rename from vncviewer/Win32PixelBuffer.h
rename to vncviewer/Surface.cxx
index 728e594..fbd4143 100644
--- a/vncviewer/Win32PixelBuffer.h
+++ b/vncviewer/Surface.cxx
@@ -1,4 +1,4 @@
-/* Copyright 2011-2014 Pierre Ossman for Cendio AB
+/* Copyright 2016 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
@@ -16,23 +16,24 @@
  * USA.
  */
 
-#ifndef __WIN32PIXELBUFFER_H__
-#define __WIN32PIXELBUFFER_H__
+#include <FL/Fl_RGB_Image.H>
 
-#include <windows.h>
+#include "Surface.h"
 
-#include "PlatformPixelBuffer.h"
+Surface::Surface(int width, int height) :
+  w(width), h(height)
+{
+  alloc();
+}
 
-class Win32PixelBuffer: public PlatformPixelBuffer {
-public:
-  Win32PixelBuffer(int width, int height);
-  ~Win32PixelBuffer();
+Surface::Surface(const Fl_RGB_Image* image) :
+  w(image->w()), h(image->h())
+{
+  alloc();
+  update(image);
+}
 
-  virtual void draw(int src_x, int src_y, int x, int y, int w, int h);
-
-protected:
-  HBITMAP bitmap;
-};
-
-
-#endif
+Surface::~Surface()
+{
+  dealloc();
+}
diff --git a/vncviewer/Surface.h b/vncviewer/Surface.h
new file mode 100644
index 0000000..032889b
--- /dev/null
+++ b/vncviewer/Surface.h
@@ -0,0 +1,72 @@
+/* Copyright 2016 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 __SURFACE_H__
+#define __SURFACE_H__
+
+#if defined(WIN32)
+#include <windows.h>
+#elif defined(__APPLE__)
+// Apple headers conflict with FLTK, so redefine types here
+typedef struct CGImage* CGImageRef;
+#else
+#include <X11/extensions/Xrender.h>
+#endif
+
+class Fl_RGB_Image;
+
+class Surface {
+public:
+  Surface(int width, int height);
+  Surface(const Fl_RGB_Image* image);
+  ~Surface();
+
+  int width() { return w; }
+  int height() { return h; }
+
+  void clear(unsigned char r, unsigned char g, unsigned char b, unsigned char a=255);
+
+  void draw(int src_x, int src_y, int x, int y, int w, int h);
+  void draw(Surface* dst, int src_x, int src_y, int x, int y, int w, int h);
+
+  void blend(int src_x, int src_y, int x, int y, int w, int h, int a=255);
+  void blend(Surface* dst, int src_x, int src_y, int x, int y, int w, int h, int a=255);
+
+protected:
+  void alloc();
+  void dealloc();
+  void update(const Fl_RGB_Image* image);
+
+protected:
+  int w, h;
+
+#if defined(WIN32)
+  RGBQUAD* data;
+  HBITMAP bitmap;
+#elif defined(__APPLE__)
+  unsigned char* data;
+  CGImageRef image;
+#else
+  Pixmap pixmap;
+  Picture picture;
+  XRenderPictFormat* visFormat;
+#endif
+};
+
+#endif
+
diff --git a/vncviewer/Surface_OSX.cxx b/vncviewer/Surface_OSX.cxx
new file mode 100644
index 0000000..2dfaa47
--- /dev/null
+++ b/vncviewer/Surface_OSX.cxx
@@ -0,0 +1,249 @@
+/* Copyright 2016 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.
+ */
+
+#include <assert.h>
+
+#include <ApplicationServices/ApplicationServices.h>
+
+#include <FL/Fl_RGB_Image.H>
+#include <FL/Fl_Window.H>
+#include <FL/x.H>
+
+#include <rdr/Exception.h>
+
+#include "Surface.h"
+
+static void render(CGContextRef gc, CGImageRef image,
+                   CGBlendMode mode, CGFloat alpha,
+                   int src_x, int src_y, int src_w, int src_h,
+                   int x, int y, int w, int h)
+{
+  CGRect rect;
+
+  CGContextSaveGState(gc);
+
+  CGContextSetBlendMode(gc, mode);
+  CGContextSetAlpha(gc, alpha);
+
+  // We have to use clipping to partially display an image
+  rect.origin.x = x;
+  rect.origin.y = y;
+  rect.size.width = w;
+  rect.size.height = h;
+
+  CGContextClipToRect(gc, rect);
+
+  rect.origin.x = x - src_x;
+  rect.origin.y = y - src_y;
+  rect.size.width = src_w;
+  rect.size.height = src_h;
+
+  CGContextDrawImage(gc, rect, image);
+
+  CGContextRestoreGState(gc);
+}
+
+static CGContextRef make_bitmap(int width, int height, unsigned char* data)
+{
+  CGColorSpaceRef lut;
+  CGContextRef bitmap;
+
+  lut = CGDisplayCopyColorSpace(kCGDirectMainDisplay);
+  if (!lut) {
+    lut = CGColorSpaceCreateDeviceRGB();
+    if (!lut)
+      throw rdr::Exception("CGColorSpaceCreateDeviceRGB");
+  }
+
+  bitmap = CGBitmapContextCreate(data, width, height, 8, width*4, lut,
+                                 kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
+  CGColorSpaceRelease(lut);
+  if (!bitmap)
+    throw rdr::Exception("CGBitmapContextCreate");
+
+  return bitmap;
+}
+
+void Surface::clear(unsigned char r, unsigned char g, unsigned char b, unsigned char a)
+{
+  unsigned char* out;
+  int x, y;
+
+  r = (unsigned)r * a / 255;
+  g = (unsigned)g * a / 255;
+  b = (unsigned)b * a / 255;
+
+  out = data;
+  for (y = 0;y < width();y++) {
+    for (x = 0;x < height();x++) {
+      *out++ = b;
+      *out++ = g;
+      *out++ = r;
+      *out++ = a;
+    }
+  }
+}
+
+void Surface::draw(int src_x, int src_y, int x, int y, int w, int h)
+{
+  CGContextSaveGState(fl_gc);
+
+  // Reset the transformation matrix back to the default identity
+  // matrix as otherwise we get a massive performance hit
+  CGContextConcatCTM(fl_gc, CGAffineTransformInvert(CGContextGetCTM(fl_gc)));
+
+  // macOS Coordinates are from bottom left, not top left
+  src_y = height() - (src_y + h);
+  y = Fl_Window::current()->h() - (y + h);
+
+  render(fl_gc, image, kCGBlendModeCopy, 1.0,
+         src_x, src_y, width(), height(), x, y, w, h);
+
+  CGContextRestoreGState(fl_gc);
+}
+
+void Surface::draw(Surface* dst, int src_x, int src_y, int x, int y, int w, int h)
+{
+  CGContextRef bitmap;
+
+  bitmap = make_bitmap(dst->width(), dst->height(), dst->data);
+
+  // macOS Coordinates are from bottom left, not top left
+  src_y = height() - (src_y + h);
+  y = dst->height() - (y + h);
+
+  render(bitmap, image, kCGBlendModeCopy, 1.0,
+         src_x, src_y, width(), height(), x, y, w, h);
+
+  CGContextRelease(bitmap);
+}
+
+void Surface::blend(int src_x, int src_y, int x, int y, int w, int h, int a)
+{
+  CGContextSaveGState(fl_gc);
+
+  // Reset the transformation matrix back to the default identity
+  // matrix as otherwise we get a massive performance hit
+  CGContextConcatCTM(fl_gc, CGAffineTransformInvert(CGContextGetCTM(fl_gc)));
+
+  // macOS Coordinates are from bottom left, not top left
+  src_y = height() - (src_y + h);
+  y = Fl_Window::current()->h() - (y + h);
+
+  render(fl_gc, image, kCGBlendModeNormal, (CGFloat)a/255.0,
+         src_x, src_y, width(), height(), x, y, w, h);
+
+  CGContextRestoreGState(fl_gc);
+}
+
+void Surface::blend(Surface* dst, int src_x, int src_y, int x, int y, int w, int h, int a)
+{
+  CGContextRef bitmap;
+
+  bitmap = make_bitmap(dst->width(), dst->height(), dst->data);
+
+  // macOS Coordinates are from bottom left, not top left
+  src_y = height() - (src_y + h);
+  y = dst->height() - (y + h);
+
+  render(bitmap, image, kCGBlendModeNormal, (CGFloat)a/255.0,
+         src_x, src_y, width(), height(), x, y, w, h);
+
+  CGContextRelease(bitmap);
+}
+
+void Surface::alloc()
+{
+  CGColorSpaceRef lut;
+  CGDataProviderRef provider;
+
+  data = new unsigned char[width() * height() * 4];
+
+  lut = CGDisplayCopyColorSpace(kCGDirectMainDisplay);
+  if (!lut) {
+    lut = CGColorSpaceCreateDeviceRGB();
+    if (!lut)
+      throw rdr::Exception("CGColorSpaceCreateDeviceRGB");
+  }
+
+  provider = CGDataProviderCreateWithData(NULL, data,
+                                          width() * height() * 4, NULL);
+  if (!provider)
+    throw rdr::Exception("CGDataProviderCreateWithData");
+
+  image = CGImageCreate(width(), height(), 8, 32, width() * 4, lut,
+                        kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little,
+                        provider, NULL, false, kCGRenderingIntentDefault);
+  CGColorSpaceRelease(lut);
+  CGDataProviderRelease(provider);
+  if (!image)
+    throw rdr::Exception("CGImageCreate");
+}
+
+void Surface::dealloc()
+{
+  CGImageRelease(image);
+  delete [] data;
+}
+
+void Surface::update(const Fl_RGB_Image* image)
+{
+  int x, y;
+  const unsigned char* in;
+  unsigned char* out;
+
+  assert(image->w() == width());
+  assert(image->h() == height());
+
+  // Convert data and pre-multiply alpha
+  in = (const unsigned char*)image->data()[0];
+  out = data;
+  for (y = 0;y < image->h();y++) {
+    for (x = 0;x < image->w();x++) {
+      switch (image->d()) {
+      case 1:
+        *out++ = in[0];
+        *out++ = in[0];
+        *out++ = in[0];
+        *out++ = 0xff;
+        break;
+      case 2:
+        *out++ = (unsigned)in[0] * in[1] / 255;
+        *out++ = (unsigned)in[0] * in[1] / 255;
+        *out++ = (unsigned)in[0] * in[1] / 255;
+        *out++ = in[1];
+        break;
+      case 3:
+        *out++ = in[2];
+        *out++ = in[1];
+        *out++ = in[0];
+        *out++ = 0xff;
+        break;
+      case 4:
+        *out++ = (unsigned)in[2] * in[3] / 255;
+        *out++ = (unsigned)in[1] * in[3] / 255;
+        *out++ = (unsigned)in[0] * in[3] / 255;
+        *out++ = in[3];
+        break;
+      }
+      in += image->d();
+    }
+    if (image->ld() != 0)
+      in += image->ld() - image->w() * image->d();
+  }
+}
diff --git a/vncviewer/Surface_Win32.cxx b/vncviewer/Surface_Win32.cxx
new file mode 100644
index 0000000..9400f53
--- /dev/null
+++ b/vncviewer/Surface_Win32.cxx
@@ -0,0 +1,207 @@
+/* Copyright 2016 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.
+ */
+
+#include <assert.h>
+
+#include <FL/Fl_RGB_Image.H>
+#include <FL/x.H>
+
+#include <rdr/Exception.h>
+
+#include "Surface.h"
+
+void Surface::clear(unsigned char r, unsigned char g, unsigned char b, unsigned char a)
+{
+  RGBQUAD* out;
+  int x, y;
+
+  r = (unsigned)r * a / 255;
+  g = (unsigned)g * a / 255;
+  b = (unsigned)b * a / 255;
+
+  out = data;
+  for (y = 0;y < width();y++) {
+    for (x = 0;x < height();x++) {
+      out->rgbRed = r;
+      out->rgbGreen = g;
+      out->rgbBlue = b;
+      out->rgbReserved = a;
+      out++;
+    }
+  }
+}
+
+void Surface::draw(int src_x, int src_y, int x, int y, int w, int h)
+{
+  HDC dc;
+
+  dc = CreateCompatibleDC(fl_gc);
+  if (!dc)
+    throw rdr::SystemException("CreateCompatibleDC", GetLastError());
+
+  if (!SelectObject(dc, bitmap))
+    throw rdr::SystemException("SelectObject", GetLastError());
+
+  if (!BitBlt(fl_gc, x, y, w, h, dc, src_x, src_y, SRCCOPY)) {
+    // If the desktop we're rendering to is inactive (like when the screen
+    // is locked or the UAC is active), then GDI calls will randomly fail.
+    // This is completely undocumented so we have no idea how best to deal
+    // with it. For now, we've only seen this error and for this function
+    // so only ignore this combination.
+    if (GetLastError() != ERROR_INVALID_HANDLE)
+      throw rdr::SystemException("BitBlt", GetLastError());
+  }
+
+  DeleteDC(dc);
+}
+
+void Surface::draw(Surface* dst, int src_x, int src_y, int x, int y, int w, int h)
+{
+  HDC origdc, dstdc;
+
+  dstdc = CreateCompatibleDC(NULL);
+  if (!dstdc)
+    throw rdr::SystemException("CreateCompatibleDC", GetLastError());
+
+  if (!SelectObject(dstdc, dst->bitmap))
+    throw rdr::SystemException("SelectObject", GetLastError());
+
+  origdc = fl_gc;
+  fl_gc = dstdc;
+  draw(src_x, src_y, x, y, w, h);
+  fl_gc = origdc;
+
+  DeleteDC(dstdc);
+}
+
+void Surface::blend(int src_x, int src_y, int x, int y, int w, int h, int a)
+{
+  // Compositing doesn't work properly for window DC:s
+  assert(false);
+}
+
+void Surface::blend(Surface* dst, int src_x, int src_y, int x, int y, int w, int h, int a)
+{
+  HDC dstdc, srcdc;
+  BLENDFUNCTION blend;
+
+  dstdc = CreateCompatibleDC(NULL);
+  if (!dstdc)
+    throw rdr::SystemException("CreateCompatibleDC", GetLastError());
+  srcdc = CreateCompatibleDC(NULL);
+  if (!srcdc)
+    throw rdr::SystemException("CreateCompatibleDC", GetLastError());
+
+  if (!SelectObject(dstdc, dst->bitmap))
+    throw rdr::SystemException("SelectObject", GetLastError());
+  if (!SelectObject(srcdc, bitmap))
+    throw rdr::SystemException("SelectObject", GetLastError());
+
+  blend.BlendOp = AC_SRC_OVER;
+  blend.BlendFlags = 0;
+  blend.SourceConstantAlpha = a;
+  blend.AlphaFormat = AC_SRC_ALPHA;
+
+  if (!AlphaBlend(dstdc, x, y, w, h, srcdc, src_x, src_y, w, h, blend)) {
+    // If the desktop we're rendering to is inactive (like when the screen
+    // is locked or the UAC is active), then GDI calls will randomly fail.
+    // This is completely undocumented so we have no idea how best to deal
+    // with it. For now, we've only seen this error and for this function
+    // so only ignore this combination.
+    if (GetLastError() != ERROR_INVALID_HANDLE)
+      throw rdr::SystemException("BitBlt", GetLastError());
+  }
+
+  DeleteDC(srcdc);
+  DeleteDC(dstdc);
+}
+
+void Surface::alloc()
+{
+  BITMAPINFOHEADER bih;
+
+  data = new RGBQUAD[width() * height()];
+
+  memset(&bih, 0, sizeof(bih));
+
+  bih.biSize         = sizeof(BITMAPINFOHEADER);
+  bih.biBitCount     = 32;
+  bih.biPlanes       = 1;
+  bih.biWidth        = width();
+  bih.biHeight       = -height(); // Negative to get top-down
+  bih.biCompression  = BI_RGB;
+
+  bitmap = CreateDIBSection(NULL, (BITMAPINFO*)&bih,
+                            DIB_RGB_COLORS, (void**)&data, NULL, 0);
+  if (!bitmap)
+    throw rdr::SystemException("CreateDIBSection", GetLastError());
+}
+
+void Surface::dealloc()
+{
+  DeleteObject(bitmap);
+}
+
+void Surface::update(const Fl_RGB_Image* image)
+{
+  const unsigned char* in;
+  RGBQUAD* out;
+  int x, y;
+
+  assert(image->w() == width());
+  assert(image->h() == height());
+
+  // Convert data and pre-multiply alpha
+  in = (const unsigned char*)image->data()[0];
+  out = data;
+  for (y = 0;y < image->w();y++) {
+    for (x = 0;x < image->h();x++) {
+      switch (image->d()) {
+      case 1:
+        out->rgbBlue = in[0];
+        out->rgbGreen = in[0];
+        out->rgbRed = in[0];
+        out->rgbReserved = 0xff;
+        break;
+      case 2:
+        out->rgbBlue = (unsigned)in[0] * in[1] / 255;
+        out->rgbGreen = (unsigned)in[0] * in[1] / 255;
+        out->rgbRed = (unsigned)in[0] * in[1] / 255;
+        out->rgbReserved = in[1];
+        break;
+      case 3:
+        out->rgbBlue = in[2];
+        out->rgbGreen = in[1];
+        out->rgbRed = in[0];
+        out->rgbReserved = 0xff;
+        break;
+      case 4:
+        out->rgbBlue = (unsigned)in[2] * in[3] / 255;
+        out->rgbGreen = (unsigned)in[1] * in[3] / 255;
+        out->rgbRed = (unsigned)in[0] * in[3] / 255;
+        out->rgbReserved = in[3];
+        break;
+      }
+      in += image->d();
+      out++;
+    }
+    if (image->ld() != 0)
+      in += image->ld() - image->w() * image->d();
+  }
+}
+
diff --git a/vncviewer/Surface_X11.cxx b/vncviewer/Surface_X11.cxx
new file mode 100644
index 0000000..3523da3
--- /dev/null
+++ b/vncviewer/Surface_X11.cxx
@@ -0,0 +1,198 @@
+/* Copyright 2016 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.
+ */
+
+#include <assert.h>
+
+#include <FL/Fl_RGB_Image.H>
+#include <FL/x.H>
+
+#include <rdr/Exception.h>
+
+#include "Surface.h"
+
+void Surface::clear(unsigned char r, unsigned char g, unsigned char b, unsigned char a)
+{
+  XRenderColor color;
+
+  color.red = (unsigned)r * 65535 / 255 * a / 255;
+  color.green = (unsigned)g * 65535 / 255 * a / 255;
+  color.blue = (unsigned)b * 65535 / 255 * a / 255;
+  color.alpha = (unsigned)a * 65535 / 255;
+
+  XRenderFillRectangle(fl_display, PictOpSrc, picture, &color,
+                       0, 0, width(), height());
+}
+
+void Surface::draw(int src_x, int src_y, int x, int y, int w, int h)
+{
+  Picture winPict;
+
+  winPict = XRenderCreatePicture(fl_display, fl_window, visFormat, 0, NULL);
+  XRenderComposite(fl_display, PictOpSrc, picture, None, winPict,
+                   src_x, src_y, 0, 0, x, y, w, h);
+  XRenderFreePicture(fl_display, winPict);
+}
+
+void Surface::draw(Surface* dst, int src_x, int src_y, int x, int y, int w, int h)
+{
+  XRenderComposite(fl_display, PictOpSrc, picture, None, dst->picture,
+                   src_x, src_y, 0, 0, x, y, w, h);
+}
+
+static Picture alpha_mask(int a)
+{
+  Pixmap pixmap;
+  XRenderPictFormat* format;
+  XRenderPictureAttributes rep;
+  Picture pict;
+  XRenderColor color;
+
+  if (a == 255)
+    return None;
+
+  pixmap = XCreatePixmap(fl_display, XDefaultRootWindow(fl_display),
+                         1, 1, 8);
+
+  format = XRenderFindStandardFormat(fl_display, PictStandardA8);
+  rep.repeat = RepeatNormal;
+  pict = XRenderCreatePicture(fl_display, pixmap, format, CPRepeat, &rep);
+  XFreePixmap(fl_display, pixmap);
+
+  color.alpha = (unsigned)a * 65535 / 255;
+
+  XRenderFillRectangle(fl_display, PictOpSrc, pict, &color,
+                       0, 0, 1, 1);
+
+  return pict;
+}
+
+void Surface::blend(int src_x, int src_y, int x, int y, int w, int h, int a)
+{
+  Picture winPict, alpha;
+
+  winPict = XRenderCreatePicture(fl_display, fl_window, visFormat, 0, NULL);
+  alpha = alpha_mask(a);
+  XRenderComposite(fl_display, PictOpOver, picture, alpha, winPict,
+                   src_x, src_y, 0, 0, x, y, w, h);
+  XRenderFreePicture(fl_display, winPict);
+
+  if (alpha != None)
+    XRenderFreePicture(fl_display, alpha);
+}
+
+void Surface::blend(Surface* dst, int src_x, int src_y, int x, int y, int w, int h, int a)
+{
+  Picture alpha;
+
+  alpha = alpha_mask(a);
+  XRenderComposite(fl_display, PictOpOver, picture, alpha, dst->picture,
+                   src_x, src_y, 0, 0, x, y, w, h);
+  if (alpha != None)
+    XRenderFreePicture(fl_display, alpha);
+}
+
+
+void Surface::alloc()
+{
+  XRenderPictFormat* format;
+
+  // Might not be open at this point
+  fl_open_display();
+
+  pixmap = XCreatePixmap(fl_display, XDefaultRootWindow(fl_display),
+                         width(), height(), 32);
+
+  format = XRenderFindStandardFormat(fl_display, PictStandardARGB32);
+  picture = XRenderCreatePicture(fl_display, pixmap, format, 0, NULL);
+
+  visFormat = XRenderFindVisualFormat(fl_display, fl_visual->visual);
+}
+
+void Surface::dealloc()
+{
+  XRenderFreePicture(fl_display, picture);
+  XFreePixmap(fl_display, pixmap);
+}
+
+void Surface::update(const Fl_RGB_Image* image)
+{
+  XImage* img;
+  GC gc;
+
+  int x, y;
+  const unsigned char* in;
+  unsigned char* out;
+
+  assert(image->w() == width());
+  assert(image->h() == height());
+
+  img = XCreateImage(fl_display, CopyFromParent, 32,
+                     ZPixmap, 0, NULL, width(), height(),
+                     32, 0);
+  if (!img)
+    throw rdr::Exception("XCreateImage");
+
+  img->data = (char*)malloc(img->bytes_per_line * img->height);
+  if (!img->data)
+    throw rdr::Exception("malloc");
+
+  // Convert data and pre-multiply alpha
+  in = (const unsigned char*)image->data()[0];
+  out = (unsigned char*)img->data;
+  for (y = 0;y < img->height;y++) {
+    for (x = 0;x < img->width;x++) {
+      switch (image->d()) {
+      case 1:
+        *out++ = in[0];
+        *out++ = in[0];
+        *out++ = in[0];
+        *out++ = 0xff;
+        break;
+      case 2:
+        *out++ = (unsigned)in[0] * in[1] / 255;
+        *out++ = (unsigned)in[0] * in[1] / 255;
+        *out++ = (unsigned)in[0] * in[1] / 255;
+        *out++ = in[1];
+        break;
+      case 3:
+        *out++ = in[2];
+        *out++ = in[1];
+        *out++ = in[0];
+        *out++ = 0xff;
+        break;
+      case 4:
+        *out++ = (unsigned)in[2] * in[3] / 255;
+        *out++ = (unsigned)in[1] * in[3] / 255;
+        *out++ = (unsigned)in[0] * in[3] / 255;
+        *out++ = in[3];
+        break;
+      }
+      in += image->d();
+    }
+    if (image->ld() != 0)
+      in += image->ld() - image->w() * image->d();
+  }
+
+  gc = XCreateGC(fl_display, pixmap, 0, NULL);
+  XPutImage(fl_display, pixmap, gc, img,
+            0, 0, 0, 0, img->width, img->height);
+  XFreeGC(fl_display, gc);
+
+  XDestroyImage(img);
+}
+
diff --git a/vncviewer/Viewport.cxx b/vncviewer/Viewport.cxx
index a12f783..6a23526 100644
--- a/vncviewer/Viewport.cxx
+++ b/vncviewer/Viewport.cxx
@@ -62,15 +62,6 @@
 #include "vncviewer.h"
 
 #include "PlatformPixelBuffer.h"
-#include "FLTKPixelBuffer.h"
-
-#if defined(WIN32)
-#include "Win32PixelBuffer.h"
-#elif defined(__APPLE__)
-#include "OSXPixelBuffer.h"
-#else
-#include "X11PixelBuffer.h"
-#endif
 
 #include <FL/fl_draw.H>
 #include <FL/fl_ask.H>
@@ -111,7 +102,7 @@
   // We need to intercept keyboard events early
   Fl::add_system_handler(handleSystemEvent, this);
 
-  frameBuffer = createFramebuffer(w, h);
+  frameBuffer = new PlatformPixelBuffer(w, h);
   assert(frameBuffer);
   cc->setFramebuffer(frameBuffer);
 
@@ -189,21 +180,20 @@
   "     "};
 
 void Viewport::setCursor(int width, int height, const Point& hotspot,
-                              void* data, void* mask)
+                         const rdr::U8* data)
 {
+  int i;
+
   if (cursor) {
     if (!cursor->alloc_array)
       delete [] cursor->array;
     delete cursor;
   }
 
-  int mask_len = ((width+7)/8) * height;
-  int i;
+  for (i = 0; i < width*height; i++)
+    if (data[i*4 + 3] != 0) break;
 
-  for (i = 0; i < mask_len; i++)
-    if (((rdr::U8*)mask)[i]) break;
-
-  if ((i == mask_len) && dotWhenNoCursor) {
+  if ((i == width*height) && dotWhenNoCursor) {
     vlog.debug("cursor is empty - using dot");
 
     Fl_Pixmap pxm(dotcursor_xpm);
@@ -216,34 +206,9 @@
       cursor = new Fl_RGB_Image(buffer, 1, 1, 4);
       cursorHotspot.x = cursorHotspot.y = 0;
     } else {
-      U8 *buffer = new U8[width*height*4];
-      U8 *i, *o, *m;
-      int m_width;
-
-      const PixelFormat *pf;
-      
-      pf = &cc->cp.pf();
-
-      i = (U8*)data;
-      o = buffer;
-      m = (U8*)mask;
-      m_width = (width+7)/8;
-      for (int y = 0;y < height;y++) {
-        for (int x = 0;x < width;x++) {
-          pf->rgbFromBuffer(o, i, 1);
-
-          if (m[(m_width*y)+(x/8)] & 0x80>>(x%8))
-            o[3] = 255;
-          else
-            o[3] = 0;
-
-          o += 4;
-          i += pf->bpp/8;
-        }
-      }
-
+      U8 *buffer = new U8[width * height * 4];
+      memcpy(buffer, data, width * height * 4);
       cursor = new Fl_RGB_Image(buffer, width, height, 4);
-
       cursorHotspot = hotspot;
     }
   }
@@ -253,6 +218,19 @@
 }
 
 
+void Viewport::draw(Surface* dst)
+{
+  int X, Y, W, H;
+
+  // Check what actually needs updating
+  fl_clip_box(x(), y(), w(), h(), X, Y, W, H);
+  if ((W == 0) || (H == 0))
+    return;
+
+  frameBuffer->draw(dst, X - x(), Y - y(), X, Y, W, H);
+}
+
+
 void Viewport::draw()
 {
   int X, Y, W, H;
@@ -272,7 +250,7 @@
     vlog.debug("Resizing framebuffer from %dx%d to %dx%d",
                frameBuffer->width(), frameBuffer->height(), w, h);
 
-    frameBuffer = createFramebuffer(w, h);
+    frameBuffer = new PlatformPixelBuffer(w, h);
     assert(frameBuffer);
     cc->setFramebuffer(frameBuffer);
   }
@@ -374,35 +352,15 @@
   return Fl_Widget::handle(event);
 }
 
-
-PlatformPixelBuffer* Viewport::createFramebuffer(int w, int h)
-{
-  PlatformPixelBuffer *fb;
-
-  try {
-#if defined(WIN32)
-    fb = new Win32PixelBuffer(w, h);
-#elif defined(__APPLE__)
-    fb = new OSXPixelBuffer(w, h);
-#else
-    fb = new X11PixelBuffer(w, h);
-#endif
-  } catch (rdr::Exception& e) {
-    vlog.error(_("Unable to create platform specific framebuffer: %s"), e.str());
-    vlog.error(_("Using platform independent framebuffer"));
-    fb = new FLTKPixelBuffer(w, h);
-  }
-
-  return fb;
-}
-
-
 void Viewport::handleClipboardChange(int source, void *data)
 {
   Viewport *self = (Viewport *)data;
 
   assert(self);
 
+  if (!sendClipboard)
+    return;
+
 #if !defined(WIN32) && !defined(__APPLE__)
   if (!sendPrimary && (source == 0))
     return;
diff --git a/vncviewer/Viewport.h b/vncviewer/Viewport.h
index f73a27d..6f0710d 100644
--- a/vncviewer/Viewport.h
+++ b/vncviewer/Viewport.h
@@ -22,17 +22,14 @@
 
 #include <map>
 
-namespace rfb { class ModifiablePixelBuffer; }
-
 #include <FL/Fl_Widget.H>
 
 class Fl_Menu_Button;
 class Fl_RGB_Image;
 
-namespace rfb { class PixelTransformer; }
-
 class CConn;
 class PlatformPixelBuffer;
+class Surface;
 
 class Viewport : public Fl_Widget {
 public:
@@ -48,7 +45,9 @@
 
   // New image for the locally rendered cursor
   void setCursor(int width, int height, const rfb::Point& hotspot,
-                 void* data, void* mask);
+                 const rdr::U8* data);
+
+  void draw(Surface* dst);
 
   // Fl_Widget callback methods
 
@@ -60,8 +59,6 @@
 
 private:
 
-  PlatformPixelBuffer* createFramebuffer(int w, int h);
-
   static void handleClipboardChange(int source, void *data);
 
   void handlePointerEvent(const rfb::Point& pos, int buttonMask);
diff --git a/vncviewer/Win32PixelBuffer.cxx b/vncviewer/Win32PixelBuffer.cxx
deleted file mode 100644
index 3f82530..0000000
--- a/vncviewer/Win32PixelBuffer.cxx
+++ /dev/null
@@ -1,95 +0,0 @@
-/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright 2011-2014 Pierre Ossman for Cendio AB
- * 
- * This is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- * 
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with this software; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
- * USA.
- */
-
-#ifdef HAVE_CONFIG_H
-#include <config.h>
-#endif
-
-#include <stdlib.h>
-
-#include <windows.h>
-
-#include <FL/x.H>
-
-#include <rfb/LogWriter.h>
-#include <rfb/Exception.h>
-
-#include "i18n.h"
-#include "Win32PixelBuffer.h"
-
-using namespace rfb;
-
-static rfb::LogWriter vlog("Win32PixelBuffer");
-
-Win32PixelBuffer::Win32PixelBuffer(int width, int height) :
-  PlatformPixelBuffer(rfb::PixelFormat(32, 24, false, true,
-                                       255, 255, 255, 16, 8, 0),
-                      width, height, NULL, width),
-  bitmap(NULL)
-{
-  BITMAPINFOHEADER bih;
-
-  memset(&bih, 0, sizeof(bih));
-
-  bih.biSize         = sizeof(BITMAPINFOHEADER);
-  bih.biBitCount     = getPF().bpp;
-  bih.biSizeImage    = (getPF().bpp / 8) * width * height;
-  bih.biPlanes       = 1;
-  bih.biWidth        = width;
-  bih.biHeight       = -height; // Negative to get top-down
-  bih.biCompression  = BI_RGB;
-
-  bitmap = CreateDIBSection(NULL, (BITMAPINFO*)&bih,
-                            DIB_RGB_COLORS, (void**)&data, NULL, 0);
-  if (!bitmap) {
-    int err = GetLastError();
-    throw rdr::SystemException(_("unable to create DIB section"), err);
-  }
-}
-
-
-Win32PixelBuffer::~Win32PixelBuffer()
-{
-  DeleteObject(bitmap);
-}
-
-
-void Win32PixelBuffer::draw(int src_x, int src_y, int x, int y, int w, int h)
-{
-  HDC dc;
-
-  dc = CreateCompatibleDC(fl_gc);
-  if (!dc)
-    throw rdr::SystemException(_("CreateCompatibleDC failed"), GetLastError());
-
-  if (!SelectObject(dc, bitmap))
-    throw rdr::SystemException(_("SelectObject failed"), GetLastError());
-
-  if (!BitBlt(fl_gc, x, y, w, h, dc, src_x, src_y, SRCCOPY)) {
-    // If the desktop we're rendering to is inactive (like when the screen
-    // is locked or the UAC is active), then GDI calls will randomly fail.
-    // This is completely undocumented so we have no idea how best to deal
-    // with it. For now, we've only seen this error and for this function
-    // so only ignore this combination.
-    if (GetLastError() != ERROR_INVALID_HANDLE)
-      throw rdr::SystemException(_("BitBlt failed"), GetLastError());
-  }
-
-  DeleteDC(dc);
-}
diff --git a/vncviewer/X11PixelBuffer.cxx b/vncviewer/X11PixelBuffer.cxx
deleted file mode 100644
index ce5c4d8..0000000
--- a/vncviewer/X11PixelBuffer.cxx
+++ /dev/null
@@ -1,279 +0,0 @@
-/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright 2011-2014 Pierre Ossman for Cendio AB
- * 
- * This is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- * 
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with this software; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
- * USA.
- */
-
-#ifdef HAVE_CONFIG_H
-#include <config.h>
-#endif
-
-#include <assert.h>
-#include <stdlib.h>
-
-#include <FL/Fl.H>
-#include <FL/x.H>
-
-#include <rfb/LogWriter.h>
-#include <rfb/Exception.h>
-
-#include "i18n.h"
-#include "X11PixelBuffer.h"
-
-using namespace rfb;
-
-static rfb::LogWriter vlog("X11PixelBuffer");
-
-std::list<X11PixelBuffer*> X11PixelBuffer::shmList;
-
-static PixelFormat display_pf()
-{
-  int i;
-
-  int bpp;
-  int trueColour, bigEndian;
-  int redShift, greenShift, blueShift;
-  int redMax, greenMax, blueMax;
-
-  int nformats;
-  XPixmapFormatValues* format;
-
-  // Might not be open at this point
-  fl_open_display();
-
-  format = XListPixmapFormats(fl_display, &nformats);
-
-  for (i = 0; i < nformats; i++)
-    if (format[i].depth == fl_visual->depth) break;
-
-  if (i == nformats)
-    // TRANSLATORS: "pixmap" is an X11 concept and may not be suitable
-    // to translate.
-    throw rfb::Exception(_("Display lacks pixmap format for default depth"));
-
-  switch (format[i].bits_per_pixel) {
-  case 8:
-  case 16:
-  case 32:
-    bpp = format[i].bits_per_pixel;
-    break;
-  default:
-    // TRANSLATORS: "pixmap" is an X11 concept and may not be suitable
-    // to translate.
-    throw rfb::Exception(_("Couldn't find suitable pixmap format"));
-  }
-
-  XFree(format);
-
-  bigEndian = (ImageByteOrder(fl_display) == MSBFirst);
-  trueColour = (fl_visual->c_class == TrueColor);
-
-  if (!trueColour)
-    throw rfb::Exception(_("Only true colour displays supported"));
-
-  vlog.info(_("Using default colormap and visual, TrueColor, depth %d."),
-            fl_visual->depth);
-
-  redShift   = ffs(fl_visual->red_mask)   - 1;
-  greenShift = ffs(fl_visual->green_mask) - 1;
-  blueShift  = ffs(fl_visual->blue_mask)  - 1;
-  redMax     = fl_visual->red_mask   >> redShift;
-  greenMax   = fl_visual->green_mask >> greenShift;
-  blueMax    = fl_visual->blue_mask  >> blueShift;
-
-  return PixelFormat(bpp, fl_visual->depth, bigEndian, trueColour,
-                     redMax, greenMax, blueMax,
-                     redShift, greenShift, blueShift);
-}
-
-X11PixelBuffer::X11PixelBuffer(int width, int height) :
-  PlatformPixelBuffer(display_pf(), width, height, NULL, 0),
-  shminfo(NULL), xim(NULL), pendingPutImage(0), pendingDrawable(0)
-{
-  // Might not be open at this point
-  fl_open_display();
-
-  if (!setupShm()) {
-    xim = XCreateImage(fl_display, fl_visual->visual, fl_visual->depth,
-                       ZPixmap, 0, 0, width, height, BitmapPad(fl_display), 0);
-    if (!xim)
-      throw rfb::Exception(_("Could not create framebuffer image"));
-
-    xim->data = (char*)malloc(xim->bytes_per_line * xim->height);
-    if (!xim->data)
-      throw rfb::Exception(_("Not enough memory for framebuffer"));
-  }
-
-  data = (rdr::U8*)xim->data;
-  stride = xim->bytes_per_line / (getPF().bpp/8);
-}
-
-
-X11PixelBuffer::~X11PixelBuffer()
-{
-  if (shminfo) {
-    vlog.debug("Freeing shared memory XImage");
-    shmList.remove(this);
-    Fl::remove_system_handler(handleSystemEvent);
-    shmdt(shminfo->shmaddr);
-    shmctl(shminfo->shmid, IPC_RMID, 0);
-    delete shminfo;
-    shminfo = NULL;
-  }
-
-  // XDestroyImage() will free(xim->data) if appropriate
-  if (xim)
-    XDestroyImage(xim);
-  xim = NULL;
-}
-
-
-void X11PixelBuffer::draw(int src_x, int src_y, int x, int y, int w, int h)
-{
-  if (shminfo) {
-    XShmPutImage(fl_display, fl_window, fl_gc, xim, src_x, src_y, x, y, w, h, True);
-    pendingPutImage++;
-    assert((pendingPutImage == 1) || (pendingDrawable == fl_window));
-    pendingDrawable = fl_window;
-  } else {
-    XPutImage(fl_display, fl_window, fl_gc, xim, src_x, src_y, x, y, w, h);
-  }
-}
-
-bool X11PixelBuffer::isRendering(void)
-{
-  return pendingPutImage > 0;
-}
-
-static bool caughtError;
-
-static int XShmAttachErrorHandler(Display *dpy, XErrorEvent *error)
-{
-  caughtError = true;
-  return 0;
-}
-
-int X11PixelBuffer::setupShm()
-{
-  int major, minor;
-  Bool pixmaps;
-  XErrorHandler old_handler;
-  const char *display_name = XDisplayName (NULL);
-
-  /* Don't use MIT-SHM on remote displays */
-  if (*display_name && *display_name != ':')
-    return 0;
-
-  if (!XShmQueryVersion(fl_display, &major, &minor, &pixmaps))
-    return 0;
-
-  shminfo = new XShmSegmentInfo;
-
-  xim = XShmCreateImage(fl_display, fl_visual->visual, fl_visual->depth,
-                        ZPixmap, 0, shminfo, width(), height());
-  if (!xim)
-    goto free_shminfo;
-
-  shminfo->shmid = shmget(IPC_PRIVATE,
-                          xim->bytes_per_line * xim->height,
-                          IPC_CREAT|0777);
-  if (shminfo->shmid == -1)
-    goto free_xim;
-
-  shminfo->shmaddr = xim->data = (char*)shmat(shminfo->shmid, 0, 0);
-  if (shminfo->shmaddr == (char *)-1)
-    goto free_shm;
-
-  shminfo->readOnly = True;
-
-  // This is the only way we can detect that shared memory won't work
-  // (e.g. because we're accessing a remote X11 server)
-  caughtError = false;
-  old_handler = XSetErrorHandler(XShmAttachErrorHandler);
-
-  if (!XShmAttach(fl_display, shminfo)) {
-    XSetErrorHandler(old_handler);
-    goto free_shmaddr;
-  }
-
-  XSync(fl_display, False);
-
-  XSetErrorHandler(old_handler);
-
-  if (caughtError)
-    goto free_shmaddr;
-
-  // FLTK is a bit stupid and unreliable if you register the same
-  // callback with different data values.
-  Fl::add_system_handler(handleSystemEvent, NULL);
-  shmList.push_back(this);
-
-  vlog.debug("Using shared memory XImage");
-
-  return 1;
-
-free_shmaddr:
-  shmdt(shminfo->shmaddr);
-
-free_shm:
-  shmctl(shminfo->shmid, IPC_RMID, 0);
-
-free_xim:
-  XDestroyImage(xim);
-  xim = NULL;
-
-free_shminfo:
-  delete shminfo;
-  shminfo = NULL;
-
-  return 0;
-}
-
-int X11PixelBuffer::handleSystemEvent(void* event, void* data)
-{
-  XEvent* xevent;
-  XShmCompletionEvent* shmevent;
-
-  std::list<X11PixelBuffer*>::iterator iter;
-
-  xevent = (XEvent*)event;
-  assert(xevent);
-
-  if (xevent->type != XShmGetEventBase(fl_display) + ShmCompletion)
-    return 0;
-
-  shmevent = (XShmCompletionEvent*)event;
-
-  if (shmevent->send_event)
-    return 0;
-
-  for (iter = shmList.begin();iter != shmList.end();++iter) {
-    if (shmevent->shmseg != (*iter)->shminfo->shmseg)
-      continue;
-
-    /* HP has a buggy X server on their thin clients that sends bogus
-     * extra events with an incorrect drawable id */
-    if (shmevent->drawable != (*iter)->pendingDrawable)
-      continue;
-
-    (*iter)->pendingPutImage--;
-    assert((*iter)->pendingPutImage >= 0);
-
-    return 1;
-  }
-
-  return 0;
-}
diff --git a/vncviewer/X11PixelBuffer.h b/vncviewer/X11PixelBuffer.h
deleted file mode 100644
index a616672..0000000
--- a/vncviewer/X11PixelBuffer.h
+++ /dev/null
@@ -1,55 +0,0 @@
-/* Copyright 2011-2014 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 __X11PIXELBUFFER_H__
-#define __X11PIXELBUFFER_H__
-
-#include <X11/Xlib.h>
-#include <sys/ipc.h>
-#include <sys/shm.h>
-#include <X11/extensions/XShm.h>
-
-#include <list>
-
-#include "PlatformPixelBuffer.h"
-
-class X11PixelBuffer: public PlatformPixelBuffer {
-public:
-  X11PixelBuffer(int width, int height);
-  ~X11PixelBuffer();
-
-  virtual void draw(int src_x, int src_y, int x, int y, int w, int h);
-
-  virtual bool isRendering(void);
-
-protected:
-  int setupShm();
-
-  static int handleSystemEvent(void* event, void* data);
-
-protected:
-  XShmSegmentInfo *shminfo;
-  XImage *xim;
-  int pendingPutImage;
-  Drawable pendingDrawable;
-
-  static std::list<X11PixelBuffer*> shmList;
-};
-
-
-#endif
diff --git a/vncviewer/cocoa.mm b/vncviewer/cocoa.mm
index 3919cb8..fc3b5bc 100644
--- a/vncviewer/cocoa.mm
+++ b/vncviewer/cocoa.mm
@@ -37,6 +37,10 @@
 
 #define NoSymbol 0
 
+// These are undocumented for unknown reasons
+const int kVK_RightCommand = 0x36;
+const int kVK_Menu = 0x6E;
+
 static bool captured = false;
 
 int cocoa_capture_display(Fl_Window *win, bool all_displays)
@@ -135,37 +139,37 @@
     UInt32 mask;
 
     // We don't see any event on release of CapsLock
-    if ([nsevent keyCode] == 0x39)
+    if ([nsevent keyCode] == kVK_CapsLock)
       return 1;
 
     // These are entirely undocumented, but I cannot find any other way
     // of differentiating between left and right keys
     switch ([nsevent keyCode]) {
-    case 0x36:
+    case kVK_RightCommand:
       mask = 0x0010;
       break;
-    case 0x37:
+    case kVK_Command:
       mask = 0x0008;
       break;
-    case 0x38:
+    case kVK_Shift:
       mask = 0x0002;
       break;
-    case 0x39:
+    case kVK_CapsLock:
       // We don't see any event on release of CapsLock
       return 1;
-    case 0x3A:
+    case kVK_Option:
       mask = 0x0020;
       break;
-    case 0x3B:
+    case kVK_Control:
       mask = 0x0001;
       break;
-    case 0x3C:
+    case kVK_RightShift:
       mask = 0x0004;
       break;
-    case 0x3D:
+    case kVK_RightOption:
       mask = 0x0040;
       break;
-    case 0x3E:
+    case kVK_RightControl:
       mask = 0x2000;
       break;
     default:
@@ -197,7 +201,6 @@
 
   layout = NULL;
 
-#if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5) || defined(__x86_64__)
   TISInputSourceRef keyboard;
   CFDataRef uchr;
 
@@ -208,101 +211,6 @@
     return nil;
 
   layout = (const UCKeyboardLayout*)CFDataGetBytePtr(uchr);
-#else // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
-  KeyboardLayoutRef old_layout;
-  int kind;
-
-  err = KLGetCurrentKeyboardLayout(&old_layout);
-  if (err != noErr)
-    return nil;
-
-  err = KLGetKeyboardLayoutProperty(old_layout, kKLKind,
-                                    (const void**)&kind);
-  if (err != noErr)
-    return nil;
-
-  // Old, crufty layout format?
-  if (kind == kKLKCHRKind) {
-    void *kchr_layout;
-
-    UInt32 chars, state;
-    char buf[3];
-
-    unichar result[16];
-    ByteCount in_len, out_len;
-
-    err = KLGetKeyboardLayoutProperty(old_layout, kKLKCHRData,
-                                      (const void**)&kchr_layout);
-    if (err != noErr)
-      return nil;
-
-    state = 0;
-
-    keyCode &= 0x7f;
-    modifierFlags &= 0xff00;
-
-    chars = KeyTranslate(kchr_layout, keyCode | modifierFlags, &state);
-
-    // Dead key?
-    if (state != 0) {
-      // 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.
-      chars = KeyTranslate(kchr_layout, keyCode | modifierFlags, &state);
-    }
-
-    buf[0] = (chars >> 16) & 0xff;
-    buf[1] = chars & 0xff;
-    buf[2] = '\0';
-
-    if (buf[0] == '\0') {
-      buf[0] = buf[1];
-      buf[1] = '\0';
-    }
-
-    // The data is now in some layout specific encoding. Need to convert
-    // this to unicode.
-
-    ScriptCode script;
-    TextEncoding encoding;
-    TECObjectRef converter;
-
-    script = (ScriptCode)GetScriptManagerVariable(smKeyScript);
-
-    err = UpgradeScriptInfoToTextEncoding(script, kTextLanguageDontCare,
-                                          kTextRegionDontCare, NULL,
-                                          &encoding);
-    if (err != noErr)
-      return nil;
-
-    err = TECCreateConverter(&converter, encoding, kTextEncodingUnicodeV4_0);
-    if (err != noErr)
-      return nil;
-
-    in_len = strlen(buf);
-    out_len = sizeof(result);
-
-    err = TECConvertText(converter, (ConstTextPtr)buf, in_len, &in_len,
-                         (TextPtr)result, out_len, &out_len);
-
-    TECDisposeConverter(converter);
-
-    if (err != noErr)
-      return nil;
-
-    return [NSString stringWithCharacters:result
-                     length:(out_len / sizeof(unichar))];
-  }
-
-  if ((kind != kKLKCHRuchrKind) && (kind != kKLuchrKind))
-    return nil;
-
-  err = KLGetKeyboardLayoutProperty(old_layout, kKLuchrData,
-                                    (const void**)&layout);
-  if (err != noErr)
-    return nil;
-#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
-
   if (layout == NULL)
     return nil;
 
@@ -336,81 +244,78 @@
   return [NSString stringWithCharacters:string length:actual_len];
 }
 
-// FIXME: We use hard coded values here as the constants didn't appear
-//        in the OS X headers until 10.5.
 static const int kvk_map[][2] = {
-  { 0x24, XK_Return },
-  { 0x30, XK_Tab },
-  { 0x31, XK_space },
-  { 0x33, XK_BackSpace },
-  { 0x35, XK_Escape },
-  // This one is undocumented for unknown reasons
-  { 0x36, XK_Super_R },
-  { 0x37, XK_Super_L },
-  { 0x38, XK_Shift_L },
-  { 0x39, XK_Caps_Lock },
-  { 0x3A, XK_Alt_L },
-  { 0x3B, XK_Control_L },
-  { 0x3C, XK_Shift_R },
-  { 0x3D, XK_Alt_R },
-  { 0x3E, XK_Control_R },
-  { 0x40, XK_F17 },
-  { 0x48, XF86XK_AudioRaiseVolume },
-  { 0x49, XF86XK_AudioLowerVolume },
-  { 0x4A, XF86XK_AudioMute },
-  { 0x4F, XK_F18 },
-  { 0x50, XK_F19 },
-  { 0x5A, XK_F20 },
-  { 0x60, XK_F5 },
-  { 0x61, XK_F6 },
-  { 0x62, XK_F7 },
-  { 0x63, XK_F3 },
-  { 0x64, XK_F8 },
-  { 0x65, XK_F9 },
-  { 0x67, XK_F11 },
-  { 0x69, XK_F13 },
-  { 0x6A, XK_F16 },
-  { 0x6B, XK_F14 },
-  { 0x6D, XK_F10 },
-  // Also undocumented
-  { 0x6E, XK_Menu },
-  { 0x6F, XK_F12 },
-  { 0x71, XK_F15 },
+  { kVK_Return,         XK_Return },
+  { kVK_Tab,            XK_Tab },
+  { kVK_Space,          XK_space },
+  { kVK_Delete,         XK_BackSpace },
+  { kVK_Escape,         XK_Escape },
+  { kVK_RightCommand,   XK_Super_R },
+  { kVK_Command,        XK_Super_L },
+  { kVK_Shift,          XK_Shift_L },
+  { kVK_CapsLock,       XK_Caps_Lock },
+  { kVK_Option,         XK_Alt_L },
+  { kVK_Control,        XK_Control_L },
+  { kVK_RightShift,     XK_Shift_R },
+  { kVK_RightOption,    XK_Alt_R },
+  { kVK_RightControl,   XK_Control_R },
+  { kVK_F17,            XK_F17 },
+  { kVK_VolumeUp,       XF86XK_AudioRaiseVolume },
+  { kVK_VolumeDown,     XF86XK_AudioLowerVolume },
+  { kVK_Mute,           XF86XK_AudioMute },
+  { kVK_F18,            XK_F18 },
+  { kVK_F19,            XK_F19 },
+  { kVK_F20,            XK_F20 },
+  { kVK_F5,             XK_F5 },
+  { kVK_F6,             XK_F6 },
+  { kVK_F7,             XK_F7 },
+  { kVK_F3,             XK_F3 },
+  { kVK_F8,             XK_F8 },
+  { kVK_F9,             XK_F9 },
+  { kVK_F11,            XK_F11 },
+  { kVK_F13,            XK_F13 },
+  { kVK_F16,            XK_F16 },
+  { kVK_F14,            XK_F14 },
+  { kVK_F10,            XK_F10 },
+  { kVK_Menu,           XK_Menu },
+  { kVK_F12,            XK_F12 },
+  { kVK_F15,            XK_F15 },
   // Should we send Insert here?
-  { 0x72, XK_Help },
-  { 0x73, XK_Home },
-  { 0x74, XK_Page_Up },
-  { 0x75, XK_Delete },
-  { 0x76, XK_F4 },
-  { 0x77, XK_End },
-  { 0x78, XK_F2 },
-  { 0x79, XK_Page_Down },
-  { 0x7A, XK_F1 },
-  { 0x7B, XK_Left },
-  { 0x7C, XK_Right },
-  { 0x7D, XK_Down },
-  { 0x7E, XK_Up },
+  { kVK_Help,           XK_Help },
+  { kVK_Home,           XK_Home },
+  { kVK_PageUp,         XK_Page_Up },
+  { kVK_ForwardDelete,  XK_Delete },
+  { kVK_F4,             XK_F4 },
+  { kVK_End,            XK_End },
+  { kVK_F2,             XK_F2 },
+  { kVK_PageDown,       XK_Page_Down },
+  { kVK_F1,             XK_F1 },
+  { kVK_LeftArrow,      XK_Left },
+  { kVK_RightArrow,     XK_Right },
+  { kVK_DownArrow,      XK_Down },
+  { kVK_UpArrow,        XK_Up },
+
   // The OS X headers claim these keys are not layout independent.
   // Could it be because of the state of the decimal key?
-  /* { 0x41, XK_KP_Decimal }, */ // see below
-  { 0x43, XK_KP_Multiply },
-  { 0x45, XK_KP_Add },
+  /* { kVK_ANSI_KeypadDecimal,     XK_KP_Decimal }, */ // see below
+  { kVK_ANSI_KeypadMultiply,    XK_KP_Multiply },
+  { kVK_ANSI_KeypadPlus,        XK_KP_Add },
   // OS X doesn't have NumLock, so is this really correct?
-  { 0x47, XK_Num_Lock },
-  { 0x4B, XK_KP_Divide },
-  { 0x4C, XK_KP_Enter },
-  { 0x4E, XK_KP_Subtract },
-  { 0x51, XK_KP_Equal },
-  { 0x52, XK_KP_0 },
-  { 0x53, XK_KP_1 },
-  { 0x54, XK_KP_2 },
-  { 0x55, XK_KP_3 },
-  { 0x56, XK_KP_4 },
-  { 0x57, XK_KP_5 },
-  { 0x58, XK_KP_6 },
-  { 0x59, XK_KP_7 },
-  { 0x5B, XK_KP_8 },
-  { 0x5C, XK_KP_9 },
+  { kVK_ANSI_KeypadClear,       XK_Num_Lock },
+  { kVK_ANSI_KeypadDivide,      XK_KP_Divide },
+  { kVK_ANSI_KeypadEnter,       XK_KP_Enter },
+  { kVK_ANSI_KeypadMinus,       XK_KP_Subtract },
+  { kVK_ANSI_KeypadEquals,      XK_KP_Equal },
+  { kVK_ANSI_Keypad0,           XK_KP_0 },
+  { kVK_ANSI_Keypad1,           XK_KP_1 },
+  { kVK_ANSI_Keypad2,           XK_KP_2 },
+  { kVK_ANSI_Keypad3,           XK_KP_3 },
+  { kVK_ANSI_Keypad4,           XK_KP_4 },
+  { kVK_ANSI_Keypad5,           XK_KP_5 },
+  { kVK_ANSI_Keypad6,           XK_KP_6 },
+  { kVK_ANSI_Keypad7,           XK_KP_7 },
+  { kVK_ANSI_Keypad8,           XK_KP_8 },
+  { kVK_ANSI_Keypad9,           XK_KP_9 },
 };
 
 int cocoa_event_keysym(const void *event)
diff --git a/win/rfb_win32/DeviceFrameBuffer.cxx b/win/rfb_win32/DeviceFrameBuffer.cxx
index 22841f7..3138cce 100644
--- a/win/rfb_win32/DeviceFrameBuffer.cxx
+++ b/win/rfb_win32/DeviceFrameBuffer.cxx
@@ -1,5 +1,5 @@
 /* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
- * Copyright 2014 Pierre Ossman for Cendio AB
+ * Copyright 2014-2017 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
@@ -42,7 +42,7 @@
 // -=- DeviceFrameBuffer class
 
 DeviceFrameBuffer::DeviceFrameBuffer(HDC deviceContext, const Rect& wRect)
-  : DIBSectionBuffer(deviceContext), device(deviceContext), cursorBm(deviceContext),
+  : DIBSectionBuffer(deviceContext), device(deviceContext),
     ignoreGrabErrors(false)
 {
 
@@ -76,9 +76,6 @@
   // Configure the underlying DIB to match the device
   DIBSectionBuffer::setPF(DeviceContext::getPF(device));
   DIBSectionBuffer::setSize(w, h);
-
-  // Configure the cursor buffer
-  cursorBm.setPF(format);
 }
 
 DeviceFrameBuffer::~DeviceFrameBuffer() {
@@ -134,15 +131,14 @@
   // - If hCursor is null then there is no cursor - clear the old one
 
   if (hCursor == 0) {
-    server->setCursor(0, 0, Point(), 0, 0);
+    server->setCursor(0, 0, Point(), NULL);
     return;
   }
 
   try {
 
-    const rdr::U8* buffer;
-    rdr::U8* rwbuffer;
-    int stride;
+    int width, height;
+    rdr::U8Array buffer;
 
     // - Get the size and other details about the cursor.
 
@@ -156,97 +152,170 @@
     if (maskInfo.bmBitsPixel != 1)
       throw rdr::Exception("unsupported cursor mask format");
 
-    // - Create the cursor pixel buffer and mask storage
-    //   NB: The cursor pixel buffer is NOT used here.  Instead, we
-    //   pass the cursorBm.data pointer directly, to save overhead.
-
-    cursor.setSize(maskInfo.bmWidth, maskInfo.bmHeight);
-    cursor.setPF(format);
-    cursor.hotspot = Point(iconInfo.xHotspot, iconInfo.yHotspot);
-
-    // - Get the AND and XOR masks.  There is only an XOR mask if this is not a
-    // colour cursor.
-
+    width = maskInfo.bmWidth;
+    height = maskInfo.bmHeight;
     if (!iconInfo.hbmColor)
-      cursor.setSize(cursor.width(), cursor.height() / 2);
-    rdr::U8Array mask(maskInfo.bmWidthBytes * maskInfo.bmHeight);
-    rdr::U8* xorMask = mask.buf + cursor.height() * maskInfo.bmWidthBytes;
+      height /= 2;
 
-    if (!GetBitmapBits(iconInfo.hbmMask,
-                       maskInfo.bmWidthBytes * maskInfo.bmHeight, mask.buf))
-      throw rdr::SystemException("GetBitmapBits failed", GetLastError());
+    buffer.buf = new rdr::U8[width * height * 4];
 
-    // Configure the cursor bitmap
-    cursorBm.setSize(cursor.width(), cursor.height());
+    Point hotspot = Point(iconInfo.xHotspot, iconInfo.yHotspot);
 
-    // Draw the cursor into the bitmap
-    BitmapDC dc(device, cursorBm.bitmap);
-    if (!DrawIconEx(dc, 0, 0, hCursor, 0, 0, 0, NULL, DI_NORMAL | DI_COMPAT))
-      throw rdr::SystemException("unable to render cursor", GetLastError());
+    if (iconInfo.hbmColor) {
+      // Colour cursor
 
-    // Replace any XORed pixels with xorColour, because RFB doesn't support
-    // XORing of cursors.  XORing is used for the I-beam cursor, which is most
-    // often used over a white background, but also sometimes over a black
-    // background.  We set the XOR'd pixels to black, then draw a white outline
-    // around the whole cursor.
+      BITMAPV5HEADER bi;
+      BitmapDC dc(device, iconInfo.hbmColor);
 
-    // *** should we replace any pixels not set in mask to zero, to ensure
-    // that irrelevant data doesn't screw compression?
+      memset(&bi, 0, sizeof(BITMAPV5HEADER));
 
-    bool doOutline = false;
-    if (!iconInfo.hbmColor) {
-      rwbuffer = cursorBm.getBufferRW(cursorBm.getRect(), &stride);
-      Pixel xorColour = format.pixelFromRGB((rdr::U16)0, (rdr::U16)0, (rdr::U16)0);
-      for (int y = 0; y < cursor.height(); y++) {
-        for (int x = 0; x < cursor.width(); x++) {
-          int byte = y * maskInfo.bmWidthBytes + x / 8;
-          int bit = 7 - x % 8;
-          if ((mask.buf[byte] & (1 << bit)) && (xorMask[byte] & (1 << bit)))
-          {
-            mask.buf[byte] &= ~(1 << bit);
+      bi.bV5Size        = sizeof(BITMAPV5HEADER);
+      bi.bV5Width       = width;
+      bi.bV5Height      = -height; // Negative for top-down
+      bi.bV5Planes      = 1;
+      bi.bV5BitCount    = 32;
+      bi.bV5Compression = BI_BITFIELDS;
+      bi.bV5RedMask     = 0x000000FF;
+      bi.bV5GreenMask   = 0x0000FF00;
+      bi.bV5BlueMask    = 0x00FF0000;
+      bi.bV5AlphaMask   = 0xFF000000;
 
-            switch (format.bpp) {
-            case 8:
-              rwbuffer[y * cursor.width() + x] = xorColour;  break;
-            case 16:
-              rwbuffer[y * cursor.width() + x] = xorColour; break;
-            case 32:
-              rwbuffer[y * cursor.width() + x] = xorColour; break;
-            }
+      if (!GetDIBits(dc, iconInfo.hbmColor, 0, height,
+                     buffer.buf, (LPBITMAPINFO)&bi, DIB_RGB_COLORS))
+        throw rdr::SystemException("GetDIBits", GetLastError());
 
-            doOutline = true;
-          }
+      // We may not get the RGBA order we want, so shuffle things around
+      int ridx, gidx, bidx, aidx;
+
+      ridx = __builtin_ffs(bi.bV5RedMask) / 8;
+      gidx = __builtin_ffs(bi.bV5GreenMask) / 8;
+      bidx = __builtin_ffs(bi.bV5BlueMask) / 8;
+      // Usually not set properly
+      aidx = 6 - ridx - gidx - bidx;
+
+      if ((bi.bV5RedMask != ((unsigned)0xff << ridx*8)) ||
+          (bi.bV5GreenMask != ((unsigned)0xff << gidx*8)) ||
+          (bi.bV5BlueMask != ((unsigned)0xff << bidx*8)))
+        throw rdr::Exception("unsupported cursor colour format");
+
+      rdr::U8* rwbuffer = buffer.buf;
+      for (int y = 0; y < height; y++) {
+        for (int x = 0; x < width; x++) {
+          rdr::U8 r, g, b, a;
+
+          r = rwbuffer[ridx];
+          g = rwbuffer[gidx];
+          b = rwbuffer[bidx];
+          a = rwbuffer[aidx];
+
+          rwbuffer[0] = r;
+          rwbuffer[1] = g;
+          rwbuffer[2] = b;
+          rwbuffer[3] = a;
+
+          rwbuffer += 4;
         }
       }
-      cursorBm.commitBufferRW(cursorBm.getRect());
+    } else {
+      // B/W cursor
+
+      rdr::U8Array mask(maskInfo.bmWidthBytes * maskInfo.bmHeight);
+      rdr::U8* andMask = mask.buf;
+      rdr::U8* xorMask = mask.buf + height * maskInfo.bmWidthBytes;
+
+      if (!GetBitmapBits(iconInfo.hbmMask,
+                         maskInfo.bmWidthBytes * maskInfo.bmHeight, mask.buf))
+        throw rdr::SystemException("GetBitmapBits", GetLastError());
+
+      bool doOutline = false;
+      rdr::U8* rwbuffer = buffer.buf;
+      for (int y = 0; y < height; y++) {
+        for (int x = 0; x < width; x++) {
+          int byte = y * maskInfo.bmWidthBytes + x / 8;
+          int bit = 7 - x % 8;
+
+          if (!(andMask[byte] & (1 << bit))) {
+            // Valid pixel, so make it opaque
+            rwbuffer[3] = 0xff;
+
+            // Black or white?
+            if (xorMask[byte] & (1 << bit))
+              rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0xff;
+            else
+              rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0;
+          } else if (xorMask[byte] & (1 << bit)) {
+            // Replace any XORed pixels with black, because RFB doesn't support
+            // XORing of cursors.  XORing is used for the I-beam cursor, which is most
+            // often used over a white background, but also sometimes over a black
+            // background.  We set the XOR'd pixels to black, then draw a white outline
+            // around the whole cursor.
+
+            rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0;
+            rwbuffer[3] = 0xff;
+
+            doOutline = true;
+          } else {
+            // Transparent pixel
+            rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = rwbuffer[3] = 0;
+          }
+
+          rwbuffer += 4;
+        }
+      }
+
+      if (doOutline) {
+        vlog.debug("drawing cursor outline!");
+
+        // The buffer needs to be slightly larger to make sure there
+        // is room for the outline pixels
+        rdr::U8Array outline((width + 2)*(height + 2)*4);
+        memset(outline.buf, 0, (width + 2)*(height + 2)*4);
+
+        // Pass 1, outline everything
+        rdr::U8* in = buffer.buf;
+        rdr::U8* out = outline.buf + width*4 + 4;
+        for (int y = 0; y < height; y++) {
+          for (int x = 0; x < width; x++) {
+            // Visible pixel?
+            if (in[3] > 0) {
+              // Outline above...
+              memset(out - (width+2)*4 - 4, 0xff, 4 * 3);
+              // ...besides...
+              memset(out - 4, 0xff, 4 * 3);
+              // ...and above
+              memset(out + (width+2)*4 - 4, 0xff, 4 * 3);
+            }
+            in += 4;
+            out += 4;
+          }
+          // outline is slightly larger
+          out += 2*4;
+        }
+
+        // Pass 2, overwrite with actual cursor
+        in = buffer.buf;
+        out = outline.buf + width*4 + 4;
+        for (int y = 0; y < height; y++) {
+          for (int x = 0; x < width; x++) {
+            if (in[3] > 0)
+              memcpy(out, in, 4);
+            in += 4;
+            out += 4;
+          }
+          out += 2*4;
+        }
+
+        width += 2;
+        height += 2;
+        hotspot.x += 1;
+        hotspot.y += 1;
+
+        delete [] buffer.buf;
+        buffer.buf = outline.takeBuf();
+      }
     }
 
-    // Finally invert the AND mask so it's suitable for RFB and pack it into
-    // the minimum number of bytes per row.
-
-    int maskBytesPerRow = (cursor.width() + 7) / 8;
-
-    for (int j = 0; j < cursor.height(); j++) {
-      for (int i = 0; i < maskBytesPerRow; i++)
-        cursor.mask.buf[j * maskBytesPerRow + i]
-          = ~mask.buf[j * maskInfo.bmWidthBytes + i];
-    }
-
-    if (doOutline) {
-      vlog.debug("drawing cursor outline!");
-
-      buffer = cursorBm.getBuffer(cursorBm.getRect(), &stride);
-      cursor.imageRect(cursorBm.getRect(), buffer, stride);
-
-      cursor.drawOutline(format.pixelFromRGB((rdr::U16)0xffff, (rdr::U16)0xffff, (rdr::U16)0xffff));
-
-      buffer = cursor.getBuffer(cursor.getRect(), &stride);
-      cursorBm.imageRect(cursor.getRect(), buffer, stride);
-    }
-
-    buffer = cursorBm.getBuffer(cursorBm.getRect(), &stride);
-    server->setCursor(cursor.width(), cursor.height(), cursor.hotspot,
-                      buffer, cursor.mask.buf);
+    server->setCursor(width, height, hotspot, buffer.buf);
 
   } catch (rdr::Exception& e) {
     vlog.error("%s", e.str());
diff --git a/win/rfb_win32/DeviceFrameBuffer.h b/win/rfb_win32/DeviceFrameBuffer.h
index 8e280f8..6fccf9f 100644
--- a/win/rfb_win32/DeviceFrameBuffer.h
+++ b/win/rfb_win32/DeviceFrameBuffer.h
@@ -92,8 +92,6 @@
       Point desktopToDevice(const Point p) const {return p.translate(deviceCoords.tl);}
 
       HDC device;
-      DIBSectionBuffer cursorBm;
-      Cursor cursor;
       Rect deviceCoords;
       bool ignoreGrabErrors;
     };
diff --git a/win/rfb_win32/LaunchProcess.h b/win/rfb_win32/LaunchProcess.h
index 38521dc..7321378 100644
--- a/win/rfb_win32/LaunchProcess.h
+++ b/win/rfb_win32/LaunchProcess.h
@@ -44,7 +44,7 @@
       //   as an extra flag to the process creation call.
       void start(HANDLE userToken, bool createConsole=false);
 
-      // Detatch from the child process. After detatching from a child
+      // Detach from the child process. After detaching from a child
       //   process, no other methods should be called on the object
       //   that started it
       void detach();
diff --git a/win/rfb_win32/keymap.h b/win/rfb_win32/keymap.h
index a340d09..664312a 100644
--- a/win/rfb_win32/keymap.h
+++ b/win/rfb_win32/keymap.h
@@ -62,7 +62,7 @@
   { XK_Help,             VK_HELP, 0 },
   { XK_Break,            VK_CANCEL, 1 },
 
-  // Auxilliary Functions - must come before XK_KP_F1, etc
+  // Auxiliary Functions - must come before XK_KP_F1, etc
 
   { XK_F1,               VK_F1, 0 },
   { XK_F2,               VK_F2, 0 },
diff --git a/win/winvnc/VNCServerWin32.h b/win/winvnc/VNCServerWin32.h
index 225e634..f384bbe 100644
--- a/win/winvnc/VNCServerWin32.h
+++ b/win/winvnc/VNCServerWin32.h
@@ -91,7 +91,7 @@
     virtual void processAddressChange();
 
     // RegConfig::Callback interface
-    // Called via the EventManager whenver RegConfig sees the registry change
+    // Called via the EventManager whenever RegConfig sees the registry change
     virtual void regConfigChanged();
 
     // EventHandler interface