Browse Source

Add Cursor pseudo-encoding support (disabled for now).

To change the appearance of the cursor, we use the CSS cursor style
and set the url to a data URI scheme. The image data sent via the
cursor pseudo-encoding has to be encoded to a CUR format file before
being used in the data URI.

During Canvas initialization we try and set a simple cursor to see if
the browser has support. Opera is missing support for data URI scheme
in cursor URLs.

Disabled for now until we have a better way of specifying settings
overall (too many settings for control bar now).
Joel Martin 15 years ago
parent
commit
2c2b492c0c
6 changed files with 174 additions and 9 deletions
  1. 3 1
      README.md
  2. 3 4
      docs/TODO
  3. 5 0
      docs/links
  4. 110 4
      include/canvas.js
  5. 11 0
      include/util.js
  6. 42 0
      include/vnc.js

+ 3 - 1
README.md

@@ -178,7 +178,9 @@ In the following table Jaunty is Ubuntu 9.04 and WinXP is Windows XP.
   is faster than Firefox 3.5, the high variability of web-socket-js
   is faster than Firefox 3.5, the high variability of web-socket-js
   performance results in overall performance being lower. Middle mouse
   performance results in overall performance being lower. Middle mouse
   clicks and keyboard events need some work to work properly under
   clicks and keyboard events need some work to work properly under
-  Opera.
+  Opera. Also, Opera does not have support for setting the cursor
+  style url to a data URI scheme, so cursor pseudo-encoding is
+  disabled.
 
 
 
 
 ### Integration
 ### Integration

+ 3 - 4
docs/TODO

@@ -1,5 +1,8 @@
 Short Term:
 Short Term:
 
 
+- Proper Javascript namespacing for Canvas and RFB (using function for
+    true local variables and functions).
+
 - Timing delta between frames in proxy record log, for playback
 - Timing delta between frames in proxy record log, for playback
   support (for demo and test).
   support (for demo and test).
 
 
@@ -14,10 +17,6 @@ Short Term:
 
 
 Medium Term:
 Medium Term:
 
 
-- Implement Cursor pseudo-encoding (CSS cursor)
-    http://en.wikipedia.org/wiki/ICO_(file_format)
-    https://developer.mozilla.org/en/Using_URL_values_for_the_cursor_property
-
 - Viewport and/or scaling support.
 - Viewport and/or scaling support.
 
 
 - Status bar buttons:
 - Status bar buttons:

+ 5 - 0
docs/links

@@ -29,6 +29,11 @@ TLS Protocol:
 Generate self-signed certificate:
 Generate self-signed certificate:
     http://docs.python.org/dev/library/ssl.html#certificates
     http://docs.python.org/dev/library/ssl.html#certificates
 
 
+Cursor appearance/style (for Cursor pseudo-encoding):
+    http://en.wikipedia.org/wiki/ICO_(file_format)
+    http://www.daubnet.com/en/file-format-cur
+    https://developer.mozilla.org/en/Using_URL_values_for_the_cursor_property
+
 
 
 Related projects:
 Related projects:
     
     

+ 110 - 4
include/canvas.js

@@ -27,8 +27,9 @@ var Canvas, Canvas_native;
 // Everything namespaced inside Canvas
 // Everything namespaced inside Canvas
 Canvas = {
 Canvas = {
 
 
-prefer_js  : false,
-force_canvas : false,
+prefer_js    : false, // make private
+force_canvas : false, // make private
+cursor_uri   : true,  // make private, create getter
 
 
 true_color : false,
 true_color : false,
 colourMap  : [],
 colourMap  : [],
@@ -138,7 +139,7 @@ onMouseDisable: function (e) {
 
 
 
 
 init: function (id) {
 init: function (id) {
-    var c, imgTest, arora;
+    var c, imgTest, tval, i, curTest, curSave;
     Util.Debug(">> Canvas.init");
     Util.Debug(">> Canvas.init");
 
 
     Canvas.id = id;
     Canvas.id = id;
@@ -198,6 +199,25 @@ init: function (id) {
         Canvas._cmapImage = Canvas._cmapImageFill;
         Canvas._cmapImage = Canvas._cmapImageFill;
     }
     }
 
 
+    /*
+     * Determine browser support for setting the cursor via data URI
+     * scheme
+     */
+    curDat = [];
+    for (i=0; i < 8 * 8 * 4; i++) {
+        curDat.push(255);
+    }
+    curSave = c.style.cursor;
+    Canvas.setCursor(curDat, curDat, 2, 2, 8, 8);
+    if (c.style.cursor) {
+        Util.Info("Data URI scheme cursor supported");
+    } else {
+        Canvas.cursor_uri = false;
+        Util.Warn("Data URI scheme cursor not supported");
+    }
+    c.style.cursor = curSave;
+
+
     Canvas.colourMap = [];
     Canvas.colourMap = [];
     Canvas.prevStyle = "";
     Canvas.prevStyle = "";
     Canvas.focused = true;
     Canvas.focused = true;
@@ -263,6 +283,11 @@ stop: function () {
     /* Work around right and middle click browser behaviors */
     /* Work around right and middle click browser behaviors */
     Util.removeEvent(document, 'click', Canvas.onMouseDisable);
     Util.removeEvent(document, 'click', Canvas.onMouseDisable);
     Util.removeEvent(document.body, 'contextmenu', Canvas.onMouseDisable);
     Util.removeEvent(document.body, 'contextmenu', Canvas.onMouseDisable);
+
+    // Turn off cursor rendering
+    if (Canvas.cursor_uri) {
+        c.style.cursor = "default";
+    }
 },
 },
 
 
 /*
 /*
@@ -530,8 +555,89 @@ getKeysym: function(e) {
     } 
     } 
 
 
     return keysym;
     return keysym;
-}
+},
+
 
 
+isCursor: function() {
+    return Canvas.cursor_uri;
+},
+
+setCursor: function(pixels, mask, hotx, hoty, w, h) {
+    var cur = [], cmap, IHDRsz, ANDsz, XORsz, url, idx, x, y;
+    //Util.Debug(">> setCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h);
+    
+    if (!Canvas.cursor_uri) {
+        Util.Warn("setCursor called but no cursor data URI support");
+        return;
+    }
+
+    cmap = Canvas.colourMap;
+    IHDRsz = 40;
+    ANDsz = w * h * 4;
+    XORsz = Math.ceil( (w * h) / 8.0 );
+
+    // Main header
+    cur.push16le(0);      // Reserved
+    cur.push16le(2);      // .CUR type
+    cur.push16le(1);      // Number of images, 1 for non-animated ico
+
+    // Cursor #1 header
+    cur.push(w);          // width
+    cur.push(h);          // height
+    cur.push(0);          // colors, 0 -> true-color
+    cur.push(0);          // reserved
+    cur.push16le(hotx);   // hotspot x coordinate
+    cur.push16le(hoty);   // hotspot y coordinate
+    cur.push32le(IHDRsz + XORsz + ANDsz); // cursor data byte size
+    cur.push32le(22);     // offset of cursor data in the file
+
+    // Cursor #1 InfoHeader
+    cur.push32le(IHDRsz); // Infoheader size
+    cur.push32le(w);      // Cursor width
+    cur.push32le(h*2);    // XOR+AND height
+    cur.push16le(1);      // number of planes
+    cur.push16le(32);     // bits per pixel
+    cur.push32le(0);      // Type of compression
+    cur.push32le(XORsz + ANDsz); // Size of Image
+    cur.push32le(0);
+    cur.push32le(0);
+    cur.push32le(0);
+    cur.push32le(0);
+
+    // XOR/color data
+    for (y = h-1; y >= 0; y--) {
+        for (x = 0; x < w; x++) {
+            idx = y * Math.ceil(w / 8) + Math.floor(x/8);
+            alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
+
+            if (Canvas.true_color) {
+                idx = ((w * y) + x) * 4;
+                cur.push(pixels[idx + 2]); // blue
+                cur.push(pixels[idx + 1]); // green
+                cur.push(pixels[idx + 0]); // red
+                cur.push(alpha); // red
+            } else {
+                idx = (w * y) + x;
+                rgb = cmap[pixels[idx]];
+                cur.push(rgb[2]);          // blue
+                cur.push(rgb[1]);          // green
+                cur.push(rgb[0]);          // red
+                cur.push(alpha);           // alpha
+            }
+        }
+    }
+
+    // AND/bitmask data (ignored, just needs to be right size)
+    for (y = 0; y < h; y++) {
+        for (x = 0; x < Math.ceil(w / 8); x++) {
+            cur.push(0x00);
+        }
+    }
+
+    url = "data:image/x-icon;base64," + Base64.encode(cur);
+    $(Canvas.id).style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default";
+    //Util.Debug("<< setCursor, cur.length: " + cur.length);
+}
 
 
 };
 };
 
 

+ 11 - 0
include/util.js

@@ -77,6 +77,10 @@ Array.prototype.push16 = function (num) {
     this.push((num >> 8) & 0xFF,
     this.push((num >> 8) & 0xFF,
               (num     ) & 0xFF  );
               (num     ) & 0xFF  );
 };
 };
+Array.prototype.push16le = function (num) {
+    this.push((num     ) & 0xFF,
+              (num >> 8) & 0xFF  );
+};
 
 
 
 
 Array.prototype.shift32 = function () {
 Array.prototype.shift32 = function () {
@@ -97,6 +101,13 @@ Array.prototype.push32 = function (num) {
               (num >>  8) & 0xFF,
               (num >>  8) & 0xFF,
               (num      ) & 0xFF  );
               (num      ) & 0xFF  );
 };
 };
+Array.prototype.push32le = function (num) {
+    this.push((num      ) & 0xFF,
+              (num >>  8) & 0xFF,
+              (num >> 16) & 0xFF,
+              (num >> 24) & 0xFF  );
+};
+
 
 
 Array.prototype.shiftStr = function (len) {
 Array.prototype.shiftStr = function (len) {
     var arr = this.splice(0, len);
     var arr = this.splice(0, len);

+ 42 - 0
include/vnc.js

@@ -85,6 +85,10 @@ encodings      : [
 //    ['compress_hi',      -247, 'set_compress_level']
 //    ['compress_hi',      -247, 'set_compress_level']
     ],
     ],
 
 
+encodingCursor :
+    ['Cursor',           -239, 'set_cursor'],
+
+
 setUpdateState: function(externalUpdateState) {
 setUpdateState: function(externalUpdateState) {
     RFB.externalUpdateState = externalUpdateState;
     RFB.externalUpdateState = externalUpdateState;
 },
 },
@@ -145,6 +149,14 @@ load: function () {
         RFB.updateState('fatal', "No working Canvas");
         RFB.updateState('fatal', "No working Canvas");
     }
     }
 
 
+    // Add Cursor pseudo-encoding if supported
+/*
+    if (Canvas.isCursor()) {
+        Util.Debug("Adding Cursor pseudo-encoding to encoding list");
+        RFB.encodings.push(RFB.encodingCursor);
+    }
+*/
+
     // Populate encoding lookup tables
     // Populate encoding lookup tables
     RFB.encHandlers = {};
     RFB.encHandlers = {};
     RFB.encNames = {};
     RFB.encNames = {};
@@ -1013,10 +1025,40 @@ set_desktopsize : function () {
     RFB.timing.fbu_rt_start = (new Date()).getTime();
     RFB.timing.fbu_rt_start = (new Date()).getTime();
     // Send a new non-incremental request
     // Send a new non-incremental request
     RFB.send_array(RFB.fbUpdateRequest(0));
     RFB.send_array(RFB.fbUpdateRequest(0));
+
+    RFB.FBU.bytes = 0;
+    RFB.FBU.rects -= 1;
+
     Util.Debug("<< set_desktopsize");
     Util.Debug("<< set_desktopsize");
+},
+
+set_cursor: function () {
+    var x, y, w, h, pixelslength, masklength;
+    //Util.Debug(">> set_cursor");
+    x = RFB.FBU.x;  // hotspot-x
+    y = RFB.FBU.y;  // hotspot-y
+    w = RFB.FBU.width;
+    h = RFB.FBU.height;
+
+    pixelslength = w * h * RFB.fb_Bpp;
+    masklength = Math.floor((w + 7) / 8) * h;
+
+    if (RFB.RQ.length < (pixelslength + masklength)) {
+        //Util.Debug("waiting for cursor encoding bytes");
+        RFB.FBU.bytes = pixelslength + masklength;
+        return false;
+    }
+
+    //Util.Debug("   set_cursor, x: " + x + ", y: " + y + ", w: " + w + ", h: " + h);
+
+    Canvas.setCursor(RFB.RQ.shiftBytes(pixelslength),
+                     RFB.RQ.shiftBytes(masklength),
+                     x, y, w, h);
 
 
     RFB.FBU.bytes = 0;
     RFB.FBU.bytes = 0;
     RFB.FBU.rects -= 1;
     RFB.FBU.rects -= 1;
+
+    //Util.Debug("<< set_cursor");
 },
 },
 
 
 set_jpeg_quality : function () {
 set_jpeg_quality : function () {