diff --git a/README.md b/README.md index 02d7c67..5a26a7d 100644 --- a/README.md +++ b/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 performance results in overall performance being lower. Middle mouse 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 diff --git a/docs/TODO b/docs/TODO index c2bda7f..ec76ad6 100644 --- a/docs/TODO +++ b/docs/TODO @@ -1,5 +1,8 @@ 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 support (for demo and test). @@ -11,13 +14,12 @@ Short Term: http://excanvas.sourceforge.net/ http://code.google.com/p/fxcanvas/ +- Fix cursor URI detection in Arora: + - allows data URI, but doesn't actually work + 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. - Status bar buttons: diff --git a/docs/links b/docs/links index 6e18015..64ea754 100644 --- a/docs/links +++ b/docs/links @@ -29,6 +29,11 @@ TLS Protocol: Generate self-signed certificate: 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: diff --git a/include/black.css b/include/black.css index 2d9d0ad..4cc195e 100644 --- a/include/black.css +++ b/include/black.css @@ -1,5 +1,4 @@ body { - background: #ddd; margin: 0; font-size: 13px; color: #111; @@ -56,7 +55,7 @@ body { margin: 0px; padding: 1em; } -#VNC_status_bar input { +.VNC_status_button { font-size: 10px; margin: 0px; padding: 0px; @@ -64,24 +63,46 @@ body { #VNC_status { text-align: center; } -#VNC_buttons { - text-align: right; +#VNC_settings_menu { + display: none; + position: absolute; + width: 12em; + border: 1px solid #888; + background-color: #f0f2f6; + padding: 5px; margin: 3px; + z-index: 100; opacity: 1; + text-align: left; white-space: normal; +} +#VNC_settings_menu ul { + list-style: none; + margin: 0; + padding: 0; } +.VNC_buttons_right { + text-align: right; +} +.VNC_buttons_left { + text-align: left; +} .VNC_status_normal { + background: #111; color: #fff; } .VNC_status_error { + background: #111; color: #f44; } .VNC_status_warn { + background: #111; color: #ff4; } + #VNC_screen { -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; - background: #000; + background: #111; padding: 20px; margin: 0 auto; color: #FFF; @@ -93,6 +114,7 @@ body { table-layout: auto; } #VNC_canvas { + background: #111; margin: 0 auto; } #VNC_clipboard { diff --git a/include/canvas.js b/include/canvas.js index eea5746..6741afc 100644 --- a/include/canvas.js +++ b/include/canvas.js @@ -16,8 +16,9 @@ Canvas_native = true; // Everything namespaced inside Canvas Canvas = { -prefer_js : false, -force_canvas : false, +prefer_js : false, // make private +force_canvas : false, // make private +cursor_uri : true, // make private true_color : false, colourMap : [], @@ -49,6 +50,9 @@ loadExtra : function () { onMouseButton: function(e, down) { var evt, pos, bmask; + if (! Canvas.focused) { + return true; + } evt = (e ? e : window.event); pos = Util.getEventPosition(e, $(Canvas.id), Canvas.scale); bmask = 1 << evt.button; @@ -124,6 +128,9 @@ onKeyUp : function (e) { onMouseDisable: function (e) { var evt, pos; + if (! Canvas.focused) { + return true; + } evt = (e ? e : window.event); pos = Util.getPosition($(Canvas.id)); /* Stop propagation if inside canvas area */ @@ -141,7 +148,7 @@ onMouseDisable: function (e) { init: function (id) { - var c, imgTest, arora; + var c, imgTest, tval, i, curTest, curSave; Util.Debug(">> Canvas.init"); Canvas.id = id; @@ -201,6 +208,25 @@ init: function (id) { 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.changeCursor(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.prevStyle = ""; Canvas.focused = true; @@ -319,6 +345,11 @@ stop: function () { /* Work around right and middle click browser behaviors */ Util.removeEvent(document, 'click', Canvas.onMouseDisable); Util.removeEvent(document.body, 'contextmenu', Canvas.onMouseDisable); + + // Turn off cursor rendering + if (Canvas.cursor_uri) { + c.style.cursor = "default"; + } }, /* @@ -586,8 +617,88 @@ getKeysym: function(e) { } return keysym; -} +}, +isCursor: function() { + return Canvas.cursor_uri; +}, +changeCursor: function(pixels, mask, hotx, hoty, w, h) { + var cur = [], cmap, IHDRsz, ANDsz, XORsz, url, idx, x, y; + //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h); + + if (!Canvas.cursor_uri) { + Util.Warn("changeCursor 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("<< changeCursor, cur.length: " + cur.length); +} + }; diff --git a/include/default_controls.js b/include/default_controls.js index 75df71b..1ea5baf 100644 --- a/include/default_controls.js +++ b/include/default_controls.js @@ -10,12 +10,16 @@ var DefaultControls = { +settingsOpen : false, + +// Render default controls and initialize settings menu load: function(target) { - var url, html; + var url, html, encrypt, cursor, base64, i, sheet, sheets, + DC = DefaultControls; /* Handle state updates */ - RFB.setUpdateState(DefaultControls.updateState); - RFB.setClipboardReceive(DefaultControls.clipReceive); + RFB.setUpdateState(DC.updateState); + RFB.setClipboardReceive(DC.clipReceive); /* Populate the 'target' DOM element with default controls */ if (!target) { target = 'vnc'; } @@ -27,10 +31,6 @@ load: function(target) { html += '
  • Port:
  • '; html += '
  • Password: ' + sheets[i].title + ''; + } + html += ' Style
  • '; + + // Logging selection dropdown + html += '
  • Logging
  • '; + + html += '
    '; + html += '
  • > settingsApply"); + var curSS, newSS, DC = DefaultControls; + DC.saveSetting('encrypt'); + DC.saveSetting('base64'); + DC.saveSetting('true_color'); + if (Canvas.isCursor()) { + DC.saveSetting('cursor'); + } + DC.saveSetting('stylesheet'); + DC.saveSetting('logging'); + + // Settings with immediate (non-connected related) effect + Util.selectStylesheet(DC.getSetting('stylesheet')); + Util.init_logging(DC.getSetting('logging')); + + Util.Debug("<< settingsApply"); +}, + + + setPassword: function() { console.log("setPassword"); RFB.sendPassword($('VNC_password').value); @@ -113,6 +306,7 @@ updateState: function(state, msg) { case 'fatal': c.disabled = true; cad.disabled = true; + DefaultControls.settingsDisabled(true); klass = "VNC_status_error"; break; case 'normal': @@ -121,6 +315,7 @@ updateState: function(state, msg) { zb.onclick = DefaultControls.setScale; c.disabled = false; cad.disabled = false; + DefaultControls.settingsDisabled(true); klass = "VNC_status_normal"; break; @@ -131,6 +326,7 @@ updateState: function(state, msg) { c.disabled = false; cad.disabled = true; + DefaultControls.settingsDisabled(false); klass = "VNC_status_normal"; break; case 'password': @@ -139,11 +335,13 @@ updateState: function(state, msg) { c.disabled = false; cad.disabled = true; + DefaultControls.settingsDisabled(true); klass = "VNC_status_warn"; break; default: c.disabled = true; cad.disabled = true; + DefaultControls.settingsDisabled(true); klass = "VNC_status_warn"; break; } @@ -157,28 +355,36 @@ updateState: function(state, msg) { }, connect: function() { - var host, port, password, encrypt, true_color; + var host, port, password, DC = DefaultControls; + + DC.closeSettingsMenu(); + host = $('VNC_host').value; port = $('VNC_port').value; password = $('VNC_password').value; - encrypt = $('VNC_encrypt').checked; - true_color = $('VNC_true_color').checked; if ((!host) || (!port)) { throw("Must set host and port"); } - RFB.connect(host, port, password, encrypt, true_color); + RFB.setEncrypt(DC.getSetting('encrypt')); + RFB.setBase64(DC.getSetting('base64')); + RFB.setTrueColor(DC.getSetting('true_color')); + RFB.setCursor(DC.getSetting('cursor')); + + RFB.connect(host, port, password); }, disconnect: function() { + DefaultControls.closeSettingsMenu(); + RFB.disconnect(); }, -clipFocus: function() { +canvasBlur: function() { Canvas.focused = false; }, -clipBlur: function() { +canvasFocus: function() { Canvas.focused = true; }, diff --git a/include/plain.css b/include/plain.css index dff781c..c8d853b 100644 --- a/include/plain.css +++ b/include/plain.css @@ -36,7 +36,7 @@ margin: 0px; padding: 0px; } -#VNC_status_bar input { +.VNC_status_button { font-size: 10px; margin: 0px; padding: 0px; @@ -44,10 +44,28 @@ #VNC_status { text-align: center; } -#VNC_buttons { - text-align: right; +#VNC_settings_menu { + display: none; + position: absolute; + width: 12em; + border: 1px solid #888; + background-color: #f0f2f6; + padding: 5px; margin: 3px; + z-index: 100; opacity: 1; + text-align: left; white-space: normal; +} +#VNC_settings_menu ul { + list-style: none; + margin: 0; + padding: 0; } +.VNC_buttons_right { + text-align: right; +} +.VNC_buttons_left { + text-align: left; +} .VNC_status_normal { background: #eee; } diff --git a/include/util.js b/include/util.js index 4cf3e49..124f560 100644 --- a/include/util.js +++ b/include/util.js @@ -14,37 +14,44 @@ Util = {}; -// Logging/debug routines -if (typeof window.console === "undefined") { - if (typeof window.opera !== "undefined") { - window.console = { - 'log' : window.opera.postError, - 'warn' : window.opera.postError, - 'error': window.opera.postError }; - } else { - window.console = { - 'log' : function(m) {}, - 'warn' : function(m) {}, - 'error': function(m) {}}; +/* + * Logging/debug routines + */ + +Util.init_logging = function (level) { + if (typeof window.console === "undefined") { + if (typeof window.opera !== "undefined") { + window.console = { + 'log' : window.opera.postError, + 'warn' : window.opera.postError, + 'error': window.opera.postError }; + } else { + window.console = { + 'log' : function(m) {}, + 'warn' : function(m) {}, + 'error': function(m) {}}; + } + } + + Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; + switch (level) { + case 'debug': Util.Debug = function (msg) { console.log(msg); }; + case 'info': Util.Info = function (msg) { console.log(msg); }; + case 'warn': Util.Warn = function (msg) { console.warn(msg); }; + case 'error': Util.Error = function (msg) { console.error(msg); }; + break; + default: + throw("invalid logging type '" + level + "'"); } } +// Initialize logging level +Util.init_logging( (document.location.href.match( + /logging=([A-Za-z0-9\._\-]*)/) || + ['', 'warn'])[1] ); -Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; - -Util.logging = (document.location.href.match( - /logging=([A-Za-z0-9\._\-]*)/) || ['', 'warn'])[1]; -switch (Util.logging) { - case 'debug': Util.Debug = function (msg) { console.log(msg); }; - case 'info': Util.Info = function (msg) { console.log(msg); }; - case 'warn': Util.Warn = function (msg) { console.warn(msg); }; - case 'error': Util.Error = function (msg) { console.error(msg); }; - break; - default: - throw("invalid logging type '" + Util.logging + "'"); -} - - -// Simple DOM selector by ID +/* + * Simple DOM selector by ID + */ if (!window.$) { $ = function (id) { if (document.getElementById) { @@ -77,6 +84,10 @@ Array.prototype.push16 = function (num) { this.push((num >> 8) & 0xFF, (num ) & 0xFF ); }; +Array.prototype.push16le = function (num) { + this.push((num ) & 0xFF, + (num >> 8) & 0xFF ); +}; Array.prototype.shift32 = function () { @@ -97,6 +108,13 @@ Array.prototype.push32 = function (num) { (num >> 8) & 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) { var arr = this.splice(0, len); @@ -245,3 +263,70 @@ Util.Flash = (function(){ version = v.match(/\d+/g); return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0}; }()); + +/* + * Cookie handling. Dervied from: http://www.quirksmode.org/js/cookies.html + */ +// No days means only for this browser session +Util.createCookie = function(name,value,days) { + if (days) { + var date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + var expires = "; expires="+date.toGMTString(); + } + else var expires = ""; + document.cookie = name+"="+value+expires+"; path=/"; +}; + +Util.readCookie = function(name, defaultValue) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return (typeof defaultValue !== 'undefined') ? defaultValue : null; +}; + +Util.eraseCookie = function(name) { + createCookie(name,"",-1); +}; + +/* + * Alternate stylesheet selection + */ +Util.getStylesheets = function() { var i, links, sheets = []; + links = document.getElementsByTagName("link") + for (i = 0; i < links.length; i++) { + if (links[i].title && + links[i].rel.toUpperCase().indexOf("STYLESHEET") > -1) { + sheets.push(links[i]); + } + } + return sheets; +}; + +// No sheet means try and use value from cookie, null sheet used to +// clear all alternates. +Util.selectStylesheet = function(sheet) { + var i, link, sheets = Util.getStylesheets(); + if (typeof sheet === 'undefined') { + sheet = 'default'; + } + for (i=0; i < sheets.length; i++) { + link = sheets[i]; + if (link.title === sheet) { + Util.Debug("Using stylesheet " + sheet); + link.disabled = false; + } else { + Util.Debug("Skipping stylesheet " + link.title); + link.disabled = true; + } + } + return sheet; +}; + +// call once to disable alternates and get around webkit bug +Util.selectStylesheet(null); + diff --git a/include/vnc.js b/include/vnc.js index 71d80aa..aa3effc 100644 --- a/include/vnc.js +++ b/include/vnc.js @@ -58,11 +58,11 @@ loadExtras: function () { host : '', port : 5900, password : '', + encrypt : true, true_color : false, - b64encode : true, // false means UTF-8 on the wire -//b64encode : false, // false means UTF-8 on the wire +local_cursor : true, connectTimeout : 2000, // time to wait for connection @@ -74,6 +74,7 @@ encodings : [ ['RRE', 0x02, 'display_rre'], ['RAW', 0x00, 'display_raw'], ['DesktopSize', -223, 'set_desktopsize'], + ['Cursor', -239, 'set_cursor'], // Psuedo-encoding settings ['JPEG_quality_lo', -32, 'set_jpeg_quality'], @@ -82,6 +83,7 @@ encodings : [ // ['compress_hi', -247, 'set_compress_level'] ], + setUpdateState: function(externalUpdateState) { RFB.externalUpdateState = externalUpdateState; }, @@ -94,6 +96,43 @@ setCanvasID: function(canvasID) { RFB.canvasID = canvasID; }, +setEncrypt: function(encrypt) { + if ((!encrypt) || (encrypt in {'0':1, 'no':1, 'false':1})) { + RFB.encrypt = false; + } else { + RFB.encrypt = true; + } +}, + +setBase64: function(b64) { + if ((!b64) || (b64 in {'0':1, 'no':1, 'false':1})) { + RFB.b64encode = false; + } else { + RFB.b64encode = true; + } + Util.Debug("Set b64encode to: " + RFB.b64encode); +}, + +setTrueColor: function(trueColor) { + if ((!trueColor) || (trueColor in {'0':1, 'no':1, 'false':1})) { + RFB.true_color = false; + } else { + RFB.true_color = true; + } +}, + +setCursor: function(cursor) { + if ((!cursor) || (cursor in {'0':1, 'no':1, 'false':1})) { + RFB.local_cursor = false; + } else { + if (Canvas.isCursor()) { + RFB.local_cursor = true; + } else { + Util.Warn("Browser does not support local cursor"); + } + } +}, + sendPassword: function(passwd) { RFB.password = passwd; RFB.state = "Authentication"; @@ -151,24 +190,12 @@ load: function () { //Util.Debug("<< load"); }, -connect: function (host, port, password, encrypt, true_color) { +connect: function (host, port, password) { //Util.Debug(">> connect"); RFB.host = host; RFB.port = port; RFB.password = (password !== undefined) ? password : ""; - RFB.encrypt = (encrypt !== undefined) ? encrypt : true; - if ((RFB.encrypt === "0") || - (RFB.encrypt === "no") || - (RFB.encrypt === "false")) { - RFB.encrypt = false; - } - RFB.true_color = (true_color !== undefined) ? true_color: true; - if ((RFB.true_color === "0") || - (RFB.true_color === "no") || - (RFB.true_color === "false")) { - RFB.true_color = false; - } if ((!RFB.host) || (!RFB.port)) { RFB.updateState('failed', "Must set host and port"); @@ -486,7 +513,11 @@ init_msg: function () { RFB.timing.history_start = (new Date()).getTime(); setTimeout(RFB.update_timings, 1000); - RFB.updateState('normal', "Connected to: " + RFB.fb_name); + if (RFB.encrypt) { + RFB.updateState('normal', "Connected (encrypted) to: " + RFB.fb_name); + } else { + RFB.updateState('normal', "Connected (unencrypted) to: " + RFB.fb_name); + } break; } //Util.Debug("<< init_msg"); @@ -1010,10 +1041,40 @@ set_desktopsize : function () { RFB.timing.fbu_rt_start = (new Date()).getTime(); // Send a new non-incremental request RFB.send_array(RFB.fbUpdateRequest(0)); - Util.Debug("<< set_desktopsize"); RFB.FBU.bytes = 0; RFB.FBU.rects -= 1; + + 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.changeCursor(RFB.RQ.shiftBytes(pixelslength), + RFB.RQ.shiftBytes(masklength), + x, y, w, h); + + RFB.FBU.bytes = 0; + RFB.FBU.rects -= 1; + + //Util.Debug("<< set_cursor"); }, set_jpeg_quality : function () { @@ -1059,14 +1120,24 @@ fixColourMapEntries: function () { clientEncodings: function () { //Util.Debug(">> clientEncodings"); - var arr, i; + var arr, i, encList = []; + + for (i=0; i> WebSocket.onclose"); if (RFB.state === 'normal') { RFB.updateState('failed', 'Server disconnected'); + } else if (RFB.state === 'ProtocolVersion') { + RFB.updateState('failed', 'Failed to connect to server'); } else { RFB.updateState('disconnected', 'VNC disconnected'); } diff --git a/tests/canvas.html b/tests/canvas.html index 3bc77d2..bd1a363 100644 --- a/tests/canvas.html +++ b/tests/canvas.html @@ -1,5 +1,14 @@ - Canvas Performance Test + + Canvas Performance Test + + + + + + + Iterations:   @@ -22,13 +31,6 @@ - - - - + + + + +

    Roll over the buttons to test cursors

    +
    + + + +
    +
    +
    + Debug:
    + +
    +
    + + Canvas not supported. + + + + + diff --git a/tests/face.png b/tests/face.png new file mode 100644 index 0000000..74c30d8 Binary files /dev/null and b/tests/face.png differ diff --git a/vnc.html b/vnc.html index 2261d34..78d27cd 100644 --- a/vnc.html +++ b/vnc.html @@ -4,8 +4,8 @@ noVNC example: simple example using default controls VNC Client - - + +