diff --git a/app/styles/base.css b/app/styles/base.css index 9db83bf6..1dc1e0cf 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -847,7 +847,7 @@ select:active { /* Main container */ #noVNC_container { - width: 100%; + width: 80%; height: 100%; background-color: #313131; border-bottom-right-radius: 800px 600px; @@ -866,6 +866,65 @@ select:active { ime-mode: disabled; } +#noVNC_info_panel { + position: fixed; + top: 0; + right: 0; + padding: 0 20px; + width: 20%; + height: 100%; + background-color: #fff; + box-sizing: border-box; + overflow: hidden; +} + +.noVNC_info_item { + width: 100%; + padding: 20px 0; + box-sizing: border-box; + border-bottom: 1px solid rgba(0, 0, 0, .15); + text-align: center; +} + +.noVNC_info_item_label { + display: inline-block; + width: 100%; + margin-bottom: 15px; + font-weight: 600; + font-size: 14px; + text-transform: uppercase; + color: rgba(0, 0, 0, .5); +} + +.noVNC_info_item button { + display: inline-block; + padding: 4px 4px; + margin: 10px 5px 20px 0; + vertical-align: middle; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; +} + +#noVNC_mouse_coordinates, +#noVNC_click_stack { + width: 100%; + font-weight: 600; +} + +#noVNC_mouse_coordinates { + font-size: 20px; + text-align: center; +} + +#noVNC_click_stack { + text-align: left; + line-height: 1.4; + margin: 0; + box-sizing: border-box; + height: 400px; + overflow-y: scroll; +} + /*Default noVNC logo.*/ /* From: http://fonts.googleapis.com/css?family=Orbitron:700 */ @font-face { diff --git a/app/ui.js b/app/ui.js index 9c5e922f..8b056170 100644 --- a/app/ui.js +++ b/app/ui.js @@ -37,6 +37,8 @@ const UI = { lastKeyboardinput: null, defaultKeyboardinputLen: 100, + canvasInteractionEvents: Array(), + inhibit_reconnect: true, reconnect_callback: null, reconnect_password: null, @@ -165,6 +167,7 @@ const UI = { UI.initSetting('view_only', false); UI.initSetting('img_bgrx_mode', false); UI.initSetting('show_dot', false); + UI.initSetting('show_pointer', false); UI.initSetting('path', 'websockify'); UI.initSetting('repeaterID', ''); UI.initSetting('reconnect', false); @@ -357,6 +360,8 @@ const UI = { UI.addSettingChangeHandler('img_bgrx_mode', UI.applyBGRXMode); UI.addSettingChangeHandler('show_dot'); UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor); + UI.addSettingChangeHandler('show_pointer'); + UI.addSettingChangeHandler('show_pointer', UI.updateShowPointerCursor); UI.addSettingChangeHandler('host'); UI.addSettingChangeHandler('port'); UI.addSettingChangeHandler('path'); @@ -707,6 +712,79 @@ const UI = { } }, + trackMouse() { + UI.rfb.canvas.addEventListener('mousemove', function(e) { + let scaleRatioX = UI.rfb.canvas.width / UI.rfb.canvas.clientWidth; + let scaleRatioY = UI.rfb.canvas.height / UI.rfb.canvas.clientHeight; + let x = Math.floor(e.offsetX * scaleRatioX); + let y = Math.floor(e.offsetY * scaleRatioY); + document.getElementById('noVNC_mouse_coordinates').innerHTML = "(" + x + ", " + y + ")" + }); + }, + + trackClicks() { + document.getElementById('noVNC_click_stack_copy').addEventListener('click', function() { + let text = JSON.stringify(UI.canvasInteractionEvents); + if (!navigator.clipboard) { + var textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position="fixed"; //avoid scrolling to bottom + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + var successful = document.execCommand('copy'); + var msg = successful ? 'successful' : 'unsuccessful'; + console.log('Fallback: Copying text command was ' + msg); + } catch (err) { + console.error('Fallback: Oops, unable to copy', err); + } + + document.body.removeChild(textArea); + return; + } + navigator.clipboard.writeText(text).then(function() { + console.log('Async: Copying to clipboard was successful!'); + }, function(err) { + console.error('Async: Could not copy text: ', err); + }); + }); + + document.getElementById('noVNC_click_stack_clear').addEventListener('click', function() { + UI.canvasInteractionEvents = Array(); + UI.updateInteractionStackUI(); + }); + + UI.rfb.canvas.addEventListener('mouseup', function(e) { + let scaleRatioX = UI.rfb.canvas.width / UI.rfb.canvas.clientWidth; + let scaleRatioY = UI.rfb.canvas.height / UI.rfb.canvas.clientHeight; + let x = Math.floor(e.offsetX * scaleRatioX); + let y = Math.floor(e.offsetY * scaleRatioY); + UI.canvasInteractionEvents.push({name: e.type, x: x, y: y}) + UI.updateInteractionStackUI(); + }); + }, + + attachDownloadScreenshotButton() { + document.getElementById('noVNC_download_screenshot').addEventListener('click', function() { + let link = document.createElement('a'); + link.download = 'screenshot.png'; + link.href = UI.rfb.canvas.toDataURL("image/png"); + link.click(); + }); + }, + + updateInteractionStackUI() { + document.getElementById('noVNC_click_stack').innerHTML = ''; + for (var i = 0; i < UI.canvasInteractionEvents.length; i++) { + let e = UI.canvasInteractionEvents[i]; + let el = document.createElement('li'); + el.innerText = e.name + ' at (' + e.x + ', ' + e.y + ')'; + document.getElementById('noVNC_click_stack').append(el); + } + }, + /* ------^------- * /VISUAL * ============== @@ -1035,6 +1113,11 @@ const UI = { UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; UI.rfb.showDotCursor = UI.getSetting('show_dot'); + UI.rfb.showPointerCursor = UI.getSetting('show_pointer'); + + UI.trackMouse(); + UI.trackClicks(); + UI.attachDownloadScreenshotButton(); UI.updateViewOnly(); // requires UI.rfb }, @@ -1653,6 +1736,11 @@ const UI = { UI.rfb.showDotCursor = UI.getSetting('show_dot'); }, + updateShowPointerCursor() { + if (!UI.rfb) return; + UI.rfb.showPointerCursor = UI.getSetting('show_pointer'); + }, + updateLogging() { WebUtil.init_logging(UI.getSetting('logging')); }, diff --git a/core/rfb.js b/core/rfb.js index 49a16bb6..3fa940e1 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -251,6 +251,10 @@ export default class RFB extends EventTargetMixin { Log.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated"); this._showDotCursor = options.showDotCursor; } + this._showPointerCursor = false; + if (options.showPointerCursor !== undefined) { + this._showPointerCursor = options.showPointerCursor; + } } // ===== PROPERTIES ===== @@ -271,6 +275,8 @@ export default class RFB extends EventTargetMixin { } } + get canvas() { return this._canvas; } + get capabilities() { return this._capabilities; } get touchButton() { return this._mouse.touchButton; } @@ -313,6 +319,12 @@ export default class RFB extends EventTargetMixin { this._refreshCursor(); } + get showPointerCursor() { return this._showPointerCursor; } + set showPointerCursor(show) { + this._showPointerCursor = show; + this._refreshCursor(); + } + get background() { return this._screen.style.background; } set background(cssValue) { this._screen.style.background = cssValue; } @@ -1882,11 +1894,16 @@ export default class RFB extends EventTargetMixin { this._rfb_connection_state !== "connected") { return; } - const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage; - this._cursor.change(image.rgbaPixels, - image.hotx, image.hoty, - image.w, image.h - ); + + if (this._showPointerCursor) { + this._cursor.changeToDefaultCursor(); + } else { + const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage; + this._cursor.change(image.rgbaPixels, + image.hotx, image.hoty, + image.w, image.h + ); + } } static genDES(password, challenge) { diff --git a/core/util/cursor.js b/core/util/cursor.js index c7a084f0..b5c8b8cd 100644 --- a/core/util/cursor.js +++ b/core/util/cursor.js @@ -121,6 +121,10 @@ export default class Cursor { } } + changeToDefaultCursor() { + this._target.style.cursor = 'default'; + } + clear() { this._target.style.cursor = 'none'; this._canvas.width = 0; diff --git a/vnc.html b/vnc.html index d1e6c683..05977e35 100644 --- a/vnc.html +++ b/vnc.html @@ -16,7 +16,7 @@ noVNC - + @@ -243,6 +243,9 @@

  • +
  • + +
  • @@ -328,6 +331,26 @@ autocomplete="off" spellcheck="false" tabindex="-1"> +
    +
    + Mouse coordinates +
    + (0, 0) +
    +
    +
    + Screenshot + +
    +
    + Interaction stack + + +
      +
    +
    +
    +