1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 * Class handling user-facing aspects of the client session.
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
16 * True to enable mouse lock.
17 * This is currently disabled because the current client plugin does not
18 * properly handle mouse lock and delegated large cursors at the same time.
19 * This should be re-enabled (by removing this flag) once a version of
20 * the plugin that supports both has reached Chrome Stable channel.
25 remoting.enableMouseLock = false;
28 * @param {remoting.ClientSession} session
29 * @param {HTMLElement} container
30 * @param {remoting.Host} host
31 * @param {remoting.DesktopConnectedView.Mode} mode The mode of this connection.
32 * @param {string} defaultRemapKeys The default set of remap keys, to use
33 * when the client doesn't define any.
34 * @param {function(remoting.Error, remoting.ClientPlugin): void} onInitialized
36 * @extends {base.EventSourceImpl}
38 remoting.DesktopConnectedView = function(session, container, host, mode,
39 defaultRemapKeys, onInitialized) {
40 this.session_ = session;
42 /** @type {HTMLElement} @private */
43 this.container_ = container;
45 /** @type {remoting.ClientPlugin} @private */
54 /** @type {string} @private */
55 this.defaultRemapKeys_ = defaultRemapKeys;
58 * Called when the UI is finished initializing.
59 * @type {function(remoting.Error, remoting.ClientPlugin):void}
61 this.onInitialized_ = onInitialized;
63 /** @type {function(boolean=):void} @private */
64 this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this)
67 this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this);
69 this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
70 /** @type {Element} @private */
71 this.debugRegionContainer_ =
72 this.container_.querySelector('.debug-region-container');
74 /** @type {Element} @private */
75 this.mouseCursorOverlay_ =
76 this.container_.querySelector('.mouse-cursor-overlay');
78 /** @private {remoting.DesktopViewport} */
79 this.viewport_ = null;
81 /** @type {Element} */
82 var img = this.mouseCursorOverlay_;
83 /** @param {Event} event @private */
84 this.updateMouseCursorPosition_ = function(event) {
85 img.style.top = event.offsetY + 'px';
86 img.style.left = event.offsetX + 'px';
89 /** @type {remoting.VideoFrameRecorder} @private */
90 this.videoFrameRecorder_ = null;
93 // The mode of this session.
95 remoting.DesktopConnectedView.Mode = {
101 // Keys for per-host settings.
102 remoting.DesktopConnectedView.KEY_REMAP_KEYS = 'remapKeys';
103 remoting.DesktopConnectedView.KEY_RESIZE_TO_CLIENT = 'resizeToClient';
104 remoting.DesktopConnectedView.KEY_SHRINK_TO_FIT = 'shrinkToFit';
105 remoting.DesktopConnectedView.KEY_DESKTOP_SCALE = 'desktopScale';
108 * Get host display name.
112 remoting.DesktopConnectedView.prototype.getHostDisplayName = function() {
113 return this.host_.hostName;
117 * @return {remoting.DesktopConnectedView.Mode} The current state.
119 remoting.DesktopConnectedView.prototype.getMode = function() {
124 * @return {boolean} True if shrink-to-fit is enabled; false otherwise.
126 remoting.DesktopConnectedView.prototype.getShrinkToFit = function() {
127 if (this.viewport_) {
128 return this.viewport_.getShrinkToFit();
134 * @return {boolean} True if resize-to-client is enabled; false otherwise.
136 remoting.DesktopConnectedView.prototype.getResizeToClient = function() {
137 if (this.viewport_) {
138 return this.viewport_.getResizeToClient();
144 * @return {Element} The element that should host the plugin.
147 remoting.DesktopConnectedView.prototype.getPluginContainer_ = function() {
148 return this.container_.querySelector('.client-plugin-container');
151 /** @return {remoting.DesktopViewport} */
152 remoting.DesktopConnectedView.prototype.getViewportForTesting = function() {
153 return this.viewport_;
157 * Adds <embed> element to the UI container and readies the session object.
159 * @param {function(string, string):boolean} onExtensionMessage The handler for
160 * protocol extension messages. Returns true if a message is recognized;
162 * @param {Array<string>} requiredCapabilities A list of capabilities
163 * required by this application.
165 remoting.DesktopConnectedView.prototype.createPluginAndConnect =
166 function(onExtensionMessage, requiredCapabilities) {
167 this.plugin_ = remoting.ClientPlugin.factory.createPlugin(
168 this.getPluginContainer_(),
169 onExtensionMessage, requiredCapabilities);
171 this.host_.options.load().then(function(){
172 that.plugin_.initialize(that.onPluginInitialized_.bind(that));
177 * @param {boolean} initialized
179 remoting.DesktopConnectedView.prototype.onPluginInitialized_ = function(
182 console.error('ERROR: remoting plugin not loaded');
183 this.onInitialized_(remoting.Error.MISSING_PLUGIN, this.plugin_);
187 if (!this.plugin_.isSupportedVersion()) {
188 this.onInitialized_(remoting.Error.BAD_PLUGIN_VERSION, this.plugin_);
192 // Show the Send Keys menu only if the plugin has the injectKeyEvent feature,
193 // and the Ctrl-Alt-Del button only in Me2Me mode.
194 if (!this.plugin_.hasFeature(
195 remoting.ClientPlugin.Feature.INJECT_KEY_EVENT)) {
196 var sendKeysElement = document.getElementById('send-keys-menu');
197 sendKeysElement.hidden = true;
198 } else if (this.mode_ != remoting.DesktopConnectedView.Mode.ME2ME &&
199 this.mode_ != remoting.DesktopConnectedView.Mode.APP_REMOTING) {
200 var sendCadElement = document.getElementById('send-ctrl-alt-del');
201 sendCadElement.hidden = true;
204 // Apply customized key remappings if the plugin supports remapKeys.
205 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY)) {
206 this.applyRemapKeys_(true);
209 // TODO(wez): Only allow mouse lock if the app has the pointerLock permission.
210 // Enable automatic mouse-lock.
211 if (remoting.enableMouseLock &&
212 this.plugin_.hasFeature(remoting.ClientPlugin.Feature.ALLOW_MOUSE_LOCK)) {
213 this.plugin_.allowMouseLock();
216 this.plugin_.setMouseCursorHandler(this.updateMouseCursorImage_.bind(this));
218 this.onInitialized_(remoting.Error.NONE, this.plugin_);
222 * This is a callback that gets called when the window is resized.
224 * @return {void} Nothing.
226 remoting.DesktopConnectedView.prototype.onResize = function() {
227 if (this.viewport_) {
228 this.viewport_.onResize();
233 * Callback that the plugin invokes to indicate when the connection is
236 * @param {boolean} ready True if the connection is ready.
238 remoting.DesktopConnectedView.prototype.onConnectionReady = function(ready) {
240 this.container_.classList.add('session-client-inactive');
242 this.container_.classList.remove('session-client-inactive');
247 * Deletes the <embed> element from the container, without sending a
248 * session_terminate request. This is to be called when the session was
249 * disconnected by the Host.
251 * @return {void} Nothing.
253 remoting.DesktopConnectedView.prototype.removePlugin = function() {
255 this.plugin_.element().removeEventListener(
256 'focus', this.callPluginGotFocus_, false);
257 this.plugin_.element().removeEventListener(
258 'blur', this.callPluginLostFocus_, false);
259 this.plugin_.dispose();
263 this.updateClientSessionUi_(null);
267 * @param {remoting.ClientSession} clientSession The active session, or null if
268 * there is no connection.
270 remoting.DesktopConnectedView.prototype.updateClientSessionUi_ = function(
272 if (clientSession == null) {
273 if (remoting.windowFrame) {
274 remoting.windowFrame.setDesktopConnectedView(null);
276 if (remoting.toolbar) {
277 remoting.toolbar.setDesktopConnectedView(null);
279 if (remoting.optionsMenu) {
280 remoting.optionsMenu.setDesktopConnectedView(null);
283 document.body.classList.remove('connected');
284 this.container_.removeEventListener(
285 'mousemove', this.updateMouseCursorPosition_, true);
286 // Stop listening for full-screen events.
287 remoting.fullscreen.removeListener(this.callOnFullScreenChanged_);
289 base.dispose(this.viewport_);
290 this.viewport_ = null;
292 var scrollerElement = document.getElementById('scroller');
293 this.viewport_ = new remoting.DesktopViewport(
294 scrollerElement || document.body,
295 this.plugin_.hostDesktop(),
297 if (remoting.windowFrame) {
298 remoting.windowFrame.setDesktopConnectedView(this);
300 if (remoting.toolbar) {
301 remoting.toolbar.setDesktopConnectedView(this);
303 if (remoting.optionsMenu) {
304 remoting.optionsMenu.setDesktopConnectedView(this);
307 document.body.classList.add('connected');
308 this.container_.addEventListener(
309 'mousemove', this.updateMouseCursorPosition_, true);
311 // Activate full-screen related UX.
312 remoting.fullscreen.addListener(this.callOnFullScreenChanged_);
313 this.onFullScreenChanged_(remoting.fullscreen.isActive());
314 this.setFocusHandlers_();
319 * Constrains the focus to the plugin element.
322 remoting.DesktopConnectedView.prototype.setFocusHandlers_ = function() {
323 this.plugin_.element().addEventListener(
324 'focus', this.callPluginGotFocus_, false);
325 this.plugin_.element().addEventListener(
326 'blur', this.callPluginLostFocus_, false);
327 this.plugin_.element().focus();
331 * Set the shrink-to-fit and resize-to-client flags and save them if this is
332 * a Me2Me connection.
334 * @param {boolean} shrinkToFit True if the remote desktop should be scaled
335 * down if it is larger than the client window; false if scroll-bars
336 * should be added in this case.
337 * @param {boolean} resizeToClient True if window resizes should cause the
338 * host to attempt to resize its desktop to match the client window size;
339 * false to disable this behaviour for subsequent window resizes--the
340 * current host desktop size is not restored in this case.
341 * @return {void} Nothing.
343 remoting.DesktopConnectedView.prototype.setScreenMode =
344 function(shrinkToFit, resizeToClient) {
345 this.viewport_.setScreenMode(shrinkToFit, resizeToClient);
349 * Called when the full-screen status has changed, either via the
350 * remoting.Fullscreen class, or via a system event such as the Escape key
352 * @param {boolean=} fullscreen True if the app is entering full-screen mode;
353 * false if it is leaving it.
356 remoting.DesktopConnectedView.prototype.onFullScreenChanged_ = function (
358 if (this.viewport_) {
359 this.viewport_.enableBumpScroll(Boolean(fullscreen));
364 * Callback function called when the plugin element gets focus.
366 remoting.DesktopConnectedView.prototype.pluginGotFocus_ = function() {
367 remoting.clipboard.initiateToHost();
371 * Callback function called when the plugin element loses focus.
373 remoting.DesktopConnectedView.prototype.pluginLostFocus_ = function() {
375 // Release all keys to prevent them becoming 'stuck down' on the host.
376 this.plugin_.releaseAllKeys();
377 if (this.plugin_.element()) {
378 // Focus should stay on the element, not (for example) the toolbar.
379 // Due to crbug.com/246335, we can't restore the focus immediately,
380 // otherwise the plugin gets confused about whether or not it has focus.
382 this.plugin_.element().focus.bind(this.plugin_.element()), 0);
388 * @param {string} url
389 * @param {number} hotspotX
390 * @param {number} hotspotY
392 remoting.DesktopConnectedView.prototype.updateMouseCursorImage_ =
393 function(url, hotspotX, hotspotY) {
394 this.mouseCursorOverlay_.hidden = !url;
396 this.mouseCursorOverlay_.style.marginLeft = '-' + hotspotX + 'px';
397 this.mouseCursorOverlay_.style.marginTop = '-' + hotspotY + 'px';
398 this.mouseCursorOverlay_.src = url;
403 * Sets and stores the key remapping setting for the current host.
405 * @param {string} remappings Comma separated list of key remappings.
407 remoting.DesktopConnectedView.prototype.setRemapKeys = function(remappings) {
408 // Cancel any existing remappings and apply the new ones.
409 this.applyRemapKeys_(false);
410 this.host_.options.remapKeys = remappings;
411 this.applyRemapKeys_(true);
413 // Save the new remapping setting.
414 this.host_.options.save();
418 * Applies the configured key remappings to the session, or resets them.
420 * @param {boolean} apply True to apply remappings, false to cancel them.
422 remoting.DesktopConnectedView.prototype.applyRemapKeys_ = function(apply) {
423 var remapKeys = this.host_.options.remapKeys;
424 if (remapKeys == '') {
425 remapKeys = this.defaultRemapKeys_;
426 if (remapKeys == '') {
431 var remappings = remapKeys.split(',');
432 for (var i = 0; i < remappings.length; ++i) {
433 var keyCodes = remappings[i].split('>');
434 if (keyCodes.length != 2) {
435 console.log('bad remapKey: ' + remappings[i]);
438 var fromKey = parseInt(keyCodes[0], 0);
439 var toKey = parseInt(keyCodes[1], 0);
440 if (!fromKey || !toKey) {
441 console.log('bad remapKey code: ' + remappings[i]);
445 console.log('remapKey 0x' + fromKey.toString(16) +
446 '>0x' + toKey.toString(16));
447 this.plugin_.remapKey(fromKey, toKey);
449 console.log('cancel remapKey 0x' + fromKey.toString(16));
450 this.plugin_.remapKey(fromKey, fromKey);
456 * Sends a key combination to the remoting client, by sending down events for
457 * the given keys, followed by up events in reverse order.
459 * @param {Array<number>} keys Key codes to be sent.
460 * @return {void} Nothing.
463 remoting.DesktopConnectedView.prototype.sendKeyCombination_ = function(keys) {
464 for (var i = 0; i < keys.length; i++) {
465 this.plugin_.injectKeyEvent(keys[i], true);
467 for (var i = 0; i < keys.length; i++) {
468 this.plugin_.injectKeyEvent(keys[i], false);
473 * Sends a Ctrl-Alt-Del sequence to the remoting client.
475 * @return {void} Nothing.
477 remoting.DesktopConnectedView.prototype.sendCtrlAltDel = function() {
478 console.log('Sending Ctrl-Alt-Del.');
479 this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
483 * Sends a Print Screen keypress to the remoting client.
485 * @return {void} Nothing.
487 remoting.DesktopConnectedView.prototype.sendPrintScreen = function() {
488 console.log('Sending Print Screen.');
489 this.sendKeyCombination_([0x070046]);
493 * Requests that the host pause or resume video updates.
495 * @param {boolean} pause True to pause video, false to resume.
496 * @return {void} Nothing.
498 remoting.DesktopConnectedView.prototype.pauseVideo = function(pause) {
500 this.plugin_.pauseVideo(pause);
505 * Requests that the host pause or resume audio.
507 * @param {boolean} pause True to pause audio, false to resume.
508 * @return {void} Nothing.
510 remoting.DesktopConnectedView.prototype.pauseAudio = function(pause) {
512 this.plugin_.pauseAudio(pause)
516 remoting.DesktopConnectedView.prototype.initVideoFrameRecorder = function() {
517 this.videoFrameRecorder_ = new remoting.VideoFrameRecorder(this.plugin_);
521 * Returns true if the ClientSession can record video frames to a file.
524 remoting.DesktopConnectedView.prototype.canRecordVideo = function() {
525 return !!this.videoFrameRecorder_;
529 * Returns true if the ClientSession is currently recording video frames.
532 remoting.DesktopConnectedView.prototype.isRecordingVideo = function() {
533 if (!this.videoFrameRecorder_) {
536 return this.videoFrameRecorder_.isRecording();
540 * Starts or stops recording of video frames.
542 remoting.DesktopConnectedView.prototype.startStopRecording = function() {
543 if (this.videoFrameRecorder_) {
544 this.videoFrameRecorder_.startStopRecording();
549 * Handles protocol extension messages.
550 * @param {string} type Type of extension message.
551 * @param {Object} message The parsed extension message data.
552 * @return {boolean} True if the message was recognized, false otherwise.
554 remoting.DesktopConnectedView.prototype.handleExtensionMessage =
555 function(type, message) {
556 if (this.videoFrameRecorder_) {
557 return this.videoFrameRecorder_.handleMessage(type, message);
563 * Handles dirty region debug messages.
565 * @param {{rects:Array<Array<number>>}} region Dirty region of the latest
568 remoting.DesktopConnectedView.prototype.handleDebugRegion = function(region) {
569 while (this.debugRegionContainer_.firstChild) {
570 this.debugRegionContainer_.removeChild(
571 this.debugRegionContainer_.firstChild);
574 var rects = region.rects;
575 for (var i = 0; i < rects.length; ++i) {
576 var rect = document.createElement('div');
577 rect.classList.add('debug-region-rect');
578 rect.style.left = rects[i][0] + 'px';
579 rect.style.top = rects[i][1] +'px';
580 rect.style.width = rects[i][2] +'px';
581 rect.style.height = rects[i][3] + 'px';
582 this.debugRegionContainer_.appendChild(rect);