From da6dd8932e3d87e39ffc8afcb371a61ff1342e87 Mon Sep 17 00:00:00 2001 From: Joel Martin Date: Wed, 21 Jul 2010 20:34:23 -0500 Subject: [PATCH] API changes. Client cursor and settings menu. The following API changes may affect integrators: - Settings have been moved out of the RFB.connect() call. Each setting now has it's own setter function: setEncrypt, setBase64, setTrueColor, setCursor. - Encrypt and cursor settings now default to on. - CSS changes: - VNC_status_bar for input buttons switched to a element class. - VNC_buttons split into VNC_buttons_right and VNC_buttons_left - New id styles for VNC_settings_menu and VNC_setting Note: the encrypt, true_color and cursor, logging setting can all be set on load using query string variables (in addition to host, port and password). Client cursor (cursor pseudo-encoding) support has been polished and activated. The RFB settings are now presented as radio button list items in a drop-down "Settings" menu when using the default controls. Also, in the settings menu is the ability to select between alternate style-sheets. Cookie and stylesheet selection support added to util.js. --- docs/TODO | 3 + include/black.css | 32 ++++- include/canvas.js | 19 ++- include/default_controls.js | 258 ++++++++++++++++++++++++++++++++---- include/plain.css | 24 +++- include/util.js | 129 ++++++++++++++---- include/vnc.js | 99 +++++++++----- tests/canvas.html | 24 ++-- tests/cursor.html | 123 +++++++++++++++++ tests/face.png | Bin 0 -> 2303 bytes vnc.html | 4 +- vnc_auto.html | 13 +- 12 files changed, 607 insertions(+), 121 deletions(-) create mode 100644 tests/cursor.html create mode 100644 tests/face.png diff --git a/docs/TODO b/docs/TODO index 95b60cc4..ec76ad69 100644 --- a/docs/TODO +++ b/docs/TODO @@ -14,6 +14,9 @@ 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: diff --git a/include/black.css b/include/black.css index 2d9d0add..4cc195ef 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 8e822c48..be9e7fe5 100644 --- a/include/canvas.js +++ b/include/canvas.js @@ -29,7 +29,7 @@ Canvas = { prefer_js : false, // make private force_canvas : false, // make private -cursor_uri : true, // make private, create getter +cursor_uri : true, // make private true_color : false, colourMap : [], @@ -47,6 +47,9 @@ mouseMove : null, onMouseButton: function(e, down) { var evt, pos, bmask; + if (! Canvas.focused) { + return true; + } evt = (e ? e : window.event); pos = Util.getEventPosition(e, $(Canvas.id)); bmask = 1 << evt.button; @@ -122,6 +125,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 */ @@ -208,7 +214,7 @@ init: function (id) { curDat.push(255); } curSave = c.style.cursor; - Canvas.setCursor(curDat, curDat, 2, 2, 8, 8); + Canvas.changeCursor(curDat, curDat, 2, 2, 8, 8); if (c.style.cursor) { Util.Info("Data URI scheme cursor supported"); } else { @@ -561,13 +567,12 @@ getKeysym: function(e) { isCursor: function() { return Canvas.cursor_uri; }, - -setCursor: function(pixels, mask, hotx, hoty, w, h) { +changeCursor: 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); + //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h); if (!Canvas.cursor_uri) { - Util.Warn("setCursor called but no cursor data URI support"); + Util.Warn("changeCursor called but no cursor data URI support"); return; } @@ -636,7 +641,7 @@ setCursor: function(pixels, mask, hotx, hoty, w, h) { 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); + //Util.Debug("<< changeCursor, cur.length: " + cur.length); } }; diff --git a/include/default_controls.js b/include/default_controls.js index a6c7ab0b..aa42203d 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); @@ -103,6 +296,7 @@ updateState: function(state, msg) { case 'fatal': c.disabled = true; cad.disabled = true; + DefaultControls.settingsDisabled(true); klass = "VNC_status_error"; break; case 'normal': @@ -110,6 +304,7 @@ updateState: function(state, msg) { c.onclick = DefaultControls.disconnect; c.disabled = false; cad.disabled = false; + DefaultControls.settingsDisabled(true); klass = "VNC_status_normal"; break; case 'disconnected': @@ -119,6 +314,7 @@ updateState: function(state, msg) { c.disabled = false; cad.disabled = true; + DefaultControls.settingsDisabled(false); klass = "VNC_status_normal"; break; case 'password': @@ -127,11 +323,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; } @@ -145,28 +343,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 dff781c1..c8d853ba 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 64e3f93d..f7a83b28 100644 --- a/include/util.js +++ b/include/util.js @@ -14,37 +14,44 @@ var 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) { @@ -254,3 +261,69 @@ Util.Flash = (function(){ 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 4b77bc6e..93413952 100644 --- a/include/vnc.js +++ b/include/vnc.js @@ -61,11 +61,11 @@ RFB = { 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 @@ -77,6 +77,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'], @@ -85,9 +86,6 @@ encodings : [ // ['compress_hi', -247, 'set_compress_level'] ], -encodingCursor : - ['Cursor', -239, 'set_cursor'], - setUpdateState: function(externalUpdateState) { RFB.externalUpdateState = externalUpdateState; @@ -101,6 +99,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"; @@ -149,14 +184,6 @@ load: function () { 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 RFB.encHandlers = {}; RFB.encNames = {}; @@ -167,24 +194,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"); @@ -501,7 +516,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"); @@ -1051,9 +1070,9 @@ set_cursor: function () { //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); + Canvas.changeCursor(RFB.RQ.shiftBytes(pixelslength), + RFB.RQ.shiftBytes(masklength), + x, y, w, h); RFB.FBU.bytes = 0; RFB.FBU.rects -= 1; @@ -1104,14 +1123,24 @@ fixColourMapEntries: function () { clientEncodings: function () { //Util.Debug(">> clientEncodings"); - var arr, i; + var arr, i, encList = []; + + for (i=0; i - 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 0000000000000000000000000000000000000000..74c30d82f9736beffef5db259b3e40bb6de4ea2b GIT binary patch literal 2303 zcmVDcqz&LP?>7h(O|oIKntKx%ray+TL^a+~)T0!`f>n3cTgh|6ym( z?DyOM%r~>k`rm(L+4(DM%f`8sHkw5f14ij{imA#v*WJ|Q00hwIJr+{BoC65U{pI4W zU}}#{ib*uVz}!qTPqpNmuB>m}lqp;|fAU$yL}^e;Q60pI*t4&1c<9(`|Ju24|G~27 zSu((R_><5N$zql^I-kpZ zxo72fZ~prFbr*Ln>Byv0XOf=!`#-n6^y-54_LC<^95UH_mW+wwC=H*dHC zfI|R*;zVqF>9sA7|5}LTvB!Vsx+y6{6er9w%vO%8vxXRB+;yMd{=%EPb`#NtYp-%s z?wYlidY&JIfvN;VOc`SgrP8T$t8I>iF=`wHvRWC>A@;>qrt&O#}eAoZo%xO_yA}?zTJc5judC%A~baBA-Me zF2r#V1p#RlRD2k9LH8npY{z6l-EsE? zJ*)M-x6P7>0DNonSO2(bMgPIR6|25f_R6QmN2aGrNf?A-XpAvhDJ3hF%3q(|N=QJW z_u;#5z5Rj4=2kbACZn|079&FpaGIK$8|v#zr4qC3d++#8V|^Y(EGZ325m!4aPE3C9 zj=K(ha&*DM#TTvYUcF|0ds|DPPzQin$YwKNxpYlyYrSP@P;68%Q5xC1_e0;CG$w=z zeZMqO9No8n;=O(T)O5J8z46b#-}2SV*VfJxYUio7rST8Frsuldjw8V@1i%;r7+?Wx z_q%&;zU7`rANlE~4QoO2-SaJK*B)rQ8VEl)fRXpq|e(t8a}ZCjpv8Kwdg z0TN(f7QjfIOy?Fe%S4sInz=LoitxsFwRe9T`Xpe1B9KFjgE4@EIS2M1jKXkyYy>cX zIM6o`g~|BnF@Vi}>FNS71{nESOULr^*c<8Y^XGc1DVW+**Senc7*PZh*4hFTNROO6 zas2o(Arg?BHx#p;n?9JD$IWhb?Ac-PSa{b1Ojt@Km6MzDcs>TG+WCTbODIzo9*fB;+kyHn6x>3ss zn^~X)WX~qk097`dv7-bOgOX|yC4ijWPGv|KBLG*4Y3>$k=BM>!r&?7sYNu^b1;msH zoK!PNf$`aBnHf_`KoKY=2#^2?K!Bu@0VpC9gXL8+<)+)_?Gph+)yfqv^NM?6)f2@q zQQyv9Ehj;h+44dD>~aO5h$yP^s?BK?^u4pon9%5mIh}rKI9GpF&D@&*b6eN4Vf@HW zW;KE$qA-(f3MLQ3RA4+9f2PQXslWte0#F1~CB>k`pDLzvd81=vLW?h(939HHY&;|7 zGsQE@lnmp_0JG{rLC30%gZrNIihVP%8AgSuLKJ|i>7QxKE1eiRxPMW1kI@xopO;id zjIw!VNzI&vwW2S7wZO0V?3EN@$R^}`hJft4#4Cx#AecQbX33s>d}?WtU&a>2k+SxsGkmkfP8BAe9nAB#BI-(csv9J=Z>v&Nw6wgXI`s2IqWc zH-qJ2)iG8Fs{k07X);qJPpI(UGI&$P!&dq-sTx21;MM$jTT_{ZsT6lzmpcvs7LLu4 zq>zb_Qp(8xSe1UW>gxM5xh#nfK^m}O6`0)$mIcd!fs9Y8q>}+HhO{mlo!Hq;B5^+b zV29h?l8i7W zHYPGUG&*E_xt(4&J$Crmq5n87zepEWyDqO%T$fvxHA@QoY7mvyQb-}CluAe$hbMID zx9MD}YxPZ;x&=laC3Tc^*_dfE5^xA<<}7A>N$eMo_U}$Y!&`b&*+nkroI5V(+~w6x z0>FZ?{wVNjsjtGMQc^0VlvFAyzhnHrrBd0}&Q2hq_c5KJCYp`m#=aUXamD5ObJf(~_N^7l+Hku681c23KG260j+j4B%vMuhM$(Xxg z&;I|PG0r(STq0xWG!h5^!)YYTVl}{74V}+@QX_)d7PBquiza;mp++)VGluGdoI5eb Z{tpYglKBOBdFlWF002ovPDHLkV1m%%QeprA literal 0 HcmV?d00001 diff --git a/vnc.html b/vnc.html index 480264a7..9a4596a5 100644 --- a/vnc.html +++ b/vnc.html @@ -4,8 +4,8 @@ noVNC example: simple example using default controls VNC Client - - + +