Loosen up heuristics for detecting account creation forms.
[chromium-blink-merge.git] / remoting / webapp / client_session.js
blob64058ad8ee6161eee433be24aa3dfd690f6c7a16
1 // Copyright (c) 2012 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.
5 /**
6 * @fileoverview
7 * Class handling creation and teardown of a remoting client session.
9 * The ClientSession class controls lifetime of the client plugin
10 * object and provides the plugin with the functionality it needs to
11 * establish connection. Specifically it:
12 * - Delivers incoming/outgoing signaling messages,
13 * - Adjusts plugin size and position when destop resolution changes,
15 * This class should not access the plugin directly, instead it should
16 * do it through ClientPlugin class which abstracts plugin version
17 * differences.
20 'use strict';
22 /** @suppress {duplicate} */
23 var remoting = remoting || {};
25 /**
26 * @param {string} hostJid The jid of the host to connect to.
27 * @param {string} hostPublicKey The base64 encoded version of the host's
28 * public key.
29 * @param {string} sharedSecret The access code for IT2Me or the PIN
30 * for Me2Me.
31 * @param {string} authenticationMethods Comma-separated list of
32 * authentication methods the client should attempt to use.
33 * @param {string} authenticationTag A host-specific tag to mix into
34 * authentication hashes.
35 * @param {string} email The username for the talk network.
36 * @param {remoting.ClientSession.Mode} mode The mode of this connection.
37 * @param {function(remoting.ClientSession.State,
38 remoting.ClientSession.State):void} onStateChange
39 * The callback to invoke when the session changes state.
40 * @constructor
42 remoting.ClientSession = function(hostJid, hostPublicKey, sharedSecret,
43 authenticationMethods, authenticationTag,
44 email, mode, onStateChange) {
45 this.state = remoting.ClientSession.State.CREATED;
47 this.hostJid = hostJid;
48 this.hostPublicKey = hostPublicKey;
49 this.sharedSecret = sharedSecret;
50 this.authenticationMethods = authenticationMethods;
51 this.authenticationTag = authenticationTag;
52 this.email = email;
53 this.mode = mode;
54 this.clientJid = '';
55 this.sessionId = '';
56 /** @type {remoting.ClientPlugin} */
57 this.plugin = null;
58 this.scaleToFit = false;
59 this.hasReceivedFrame_ = false;
60 this.logToServer = new remoting.LogToServer();
61 this.onStateChange = onStateChange;
63 /** @type {number?} @private */
64 this.notifyClientDimensionsTimer_ = null;
66 /** @private */
67 this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this);
68 /** @private */
69 this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
70 /** @private */
71 this.callEnableShrink_ = this.setScaleToFit.bind(this, true);
72 /** @private */
73 this.callDisableShrink_ = this.setScaleToFit.bind(this, false);
74 /** @private */
75 this.callToggleFullScreen_ = this.toggleFullScreen_.bind(this);
76 /** @private */
77 this.screenOptionsMenu_ = new remoting.MenuButton(
78 document.getElementById('screen-options-menu'),
79 this.onShowOptionsMenu_.bind(this));
80 /** @private */
81 this.sendKeysMenu_ = new remoting.MenuButton(
82 document.getElementById('send-keys-menu')
85 /** @type {HTMLElement} @private */
86 this.shrinkToFit_ = document.getElementById('enable-shrink-to-fit');
87 /** @type {HTMLElement} @private */
88 this.originalSize_ = document.getElementById('disable-shrink-to-fit');
89 /** @type {HTMLElement} @private */
90 this.fullScreen_ = document.getElementById('toggle-full-screen');
92 this.shrinkToFit_.addEventListener('click', this.callEnableShrink_, false);
93 this.originalSize_.addEventListener('click', this.callDisableShrink_, false);
94 this.fullScreen_.addEventListener('click', this.callToggleFullScreen_, false);
95 /** @type {number?} @private */
96 this.bumpScrollTimer_ = null;
97 /**
98 * Allow error reporting to be suppressed in situations where it would not
99 * be useful, for example, when the device is offline.
101 * @type {boolean} @private
103 this.logErrors_ = true;
106 // Note that the positive values in both of these enums are copied directly
107 // from chromoting_scriptable_object.h and must be kept in sync. The negative
108 // values represent states transitions that occur within the web-app that have
109 // no corresponding plugin state transition.
110 /** @enum {number} */
111 remoting.ClientSession.State = {
112 CREATED: -3,
113 BAD_PLUGIN_VERSION: -2,
114 UNKNOWN_PLUGIN_ERROR: -1,
115 UNKNOWN: 0,
116 CONNECTING: 1,
117 INITIALIZING: 2,
118 CONNECTED: 3,
119 CLOSED: 4,
120 FAILED: 5
123 /** @enum {number} */
124 remoting.ClientSession.ConnectionError = {
125 UNKNOWN: -1,
126 NONE: 0,
127 HOST_IS_OFFLINE: 1,
128 SESSION_REJECTED: 2,
129 INCOMPATIBLE_PROTOCOL: 3,
130 NETWORK_FAILURE: 4,
131 HOST_OVERLOAD: 5
134 // The mode of this session.
135 /** @enum {number} */
136 remoting.ClientSession.Mode = {
137 IT2ME: 0,
138 ME2ME: 1
142 * Type used for performance statistics collected by the plugin.
143 * @constructor
145 remoting.ClientSession.PerfStats = function() {};
146 /** @type {number} */
147 remoting.ClientSession.PerfStats.prototype.videoBandwidth;
148 /** @type {number} */
149 remoting.ClientSession.PerfStats.prototype.videoFrameRate;
150 /** @type {number} */
151 remoting.ClientSession.PerfStats.prototype.captureLatency;
152 /** @type {number} */
153 remoting.ClientSession.PerfStats.prototype.encodeLatency;
154 /** @type {number} */
155 remoting.ClientSession.PerfStats.prototype.decodeLatency;
156 /** @type {number} */
157 remoting.ClientSession.PerfStats.prototype.renderLatency;
158 /** @type {number} */
159 remoting.ClientSession.PerfStats.prototype.roundtripLatency;
161 // Keys for connection statistics.
162 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
163 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
164 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
165 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
166 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
167 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
168 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
171 * The current state of the session.
172 * @type {remoting.ClientSession.State}
174 remoting.ClientSession.prototype.state = remoting.ClientSession.State.UNKNOWN;
177 * The last connection error. Set when state is set to FAILED.
178 * @type {remoting.ClientSession.ConnectionError}
180 remoting.ClientSession.prototype.error =
181 remoting.ClientSession.ConnectionError.NONE;
184 * The id of the client plugin
186 * @const
188 remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin';
191 * Callback to invoke when the state is changed.
193 * @param {remoting.ClientSession.State} oldState The previous state.
194 * @param {remoting.ClientSession.State} newState The current state.
196 remoting.ClientSession.prototype.onStateChange =
197 function(oldState, newState) { };
200 * @param {Element} container The element to add the plugin to.
201 * @param {string} id Id to use for the plugin element .
202 * @return {remoting.ClientPlugin} Create plugin object for the locally
203 * installed plugin.
205 remoting.ClientSession.prototype.createClientPlugin_ = function(container, id) {
206 var plugin = /** @type {remoting.ViewerPlugin} */
207 document.createElement('embed');
209 plugin.id = id;
210 plugin.src = 'about://none';
211 plugin.type = 'application/vnd.chromium.remoting-viewer';
212 plugin.width = 0;
213 plugin.height = 0;
214 plugin.tabIndex = 0; // Required, otherwise focus() doesn't work.
215 container.appendChild(plugin);
217 return new remoting.ClientPluginAsync(plugin);
221 * Callback function called when the plugin element gets focus.
223 remoting.ClientSession.prototype.pluginGotFocus_ = function() {
224 remoting.clipboard.initiateToHost();
228 * Callback function called when the plugin element loses focus.
230 remoting.ClientSession.prototype.pluginLostFocus_ = function() {
231 if (this.plugin) {
232 // Release all keys to prevent them becoming 'stuck down' on the host.
233 this.plugin.releaseAllKeys();
234 if (this.plugin.element()) {
235 // Focus should stay on the element, not (for example) the toolbar.
236 this.plugin.element().focus();
242 * Adds <embed> element to |container| and readies the sesion object.
244 * @param {Element} container The element to add the plugin to.
245 * @param {string} oauth2AccessToken A valid OAuth2 access token.
247 remoting.ClientSession.prototype.createPluginAndConnect =
248 function(container, oauth2AccessToken) {
249 this.plugin = this.createClientPlugin_(container, this.PLUGIN_ID);
251 this.plugin.element().focus();
253 /** @param {boolean} result */
254 this.plugin.initialize(
255 this.onPluginInitialized_.bind(this, oauth2AccessToken));
256 this.plugin.element().addEventListener(
257 'focus', this.callPluginGotFocus_, false);
258 this.plugin.element().addEventListener(
259 'blur', this.callPluginLostFocus_, false);
263 * @param {string} oauth2AccessToken
264 * @param {boolean} initialized
266 remoting.ClientSession.prototype.onPluginInitialized_ =
267 function(oauth2AccessToken, initialized) {
268 if (!initialized) {
269 console.error('ERROR: remoting plugin not loaded');
270 this.plugin.cleanup();
271 delete this.plugin;
272 this.setState_(remoting.ClientSession.State.UNKNOWN_PLUGIN_ERROR);
273 return;
276 if (!this.plugin.isSupportedVersion()) {
277 this.plugin.cleanup();
278 delete this.plugin;
279 this.setState_(remoting.ClientSession.State.BAD_PLUGIN_VERSION);
280 return;
283 // Show the Send Keys menu only if the plugin has the injectKeyEvent feature,
284 // and the Ctrl-Alt-Del button only in Me2Me mode.
285 if (!this.plugin.hasFeature(remoting.ClientPlugin.Feature.INJECT_KEY_EVENT)) {
286 var sendKeysElement = document.getElementById('send-keys-menu');
287 sendKeysElement.hidden = true;
288 } else if (this.mode != remoting.ClientSession.Mode.ME2ME) {
289 var sendCadElement = document.getElementById('send-ctrl-alt-del');
290 sendCadElement.hidden = true;
293 // Remap the right Control key to the right Win / Cmd key on ChromeOS
294 // platforms, if the plugin has the remapKey feature.
295 if (this.plugin.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY) &&
296 remoting.runningOnChromeOS()) {
297 this.plugin.remapKey(0x0700e4, 0x0700e7);
300 // Enable scale-to-fit if and only if the plugin is new enough for
301 // high-quality scaling.
302 this.setScaleToFit(this.plugin.hasFeature(
303 remoting.ClientPlugin.Feature.HIGH_QUALITY_SCALING));
305 /** @param {string} msg The IQ stanza to send. */
306 this.plugin.onOutgoingIqHandler = this.sendIq_.bind(this);
307 /** @param {string} msg The message to log. */
308 this.plugin.onDebugMessageHandler = function(msg) {
309 console.log('plugin: ' + msg);
312 this.plugin.onConnectionStatusUpdateHandler =
313 this.onConnectionStatusUpdate_.bind(this);
314 this.plugin.onConnectionReadyHandler =
315 this.onConnectionReady_.bind(this);
316 this.plugin.onDesktopSizeUpdateHandler =
317 this.onDesktopSizeChanged_.bind(this);
319 this.connectPluginToWcs_(oauth2AccessToken);
323 * Deletes the <embed> element from the container, without sending a
324 * session_terminate request. This is to be called when the session was
325 * disconnected by the Host.
327 * @return {void} Nothing.
329 remoting.ClientSession.prototype.removePlugin = function() {
330 if (this.plugin) {
331 this.plugin.element().removeEventListener(
332 'focus', this.callPluginGotFocus_, false);
333 this.plugin.element().removeEventListener(
334 'blur', this.callPluginLostFocus_, false);
335 this.plugin.cleanup();
336 this.plugin = null;
338 this.shrinkToFit_.removeEventListener('click', this.callEnableShrink_, false);
339 this.originalSize_.removeEventListener('click', this.callDisableShrink_,
340 false);
341 this.fullScreen_.removeEventListener('click', this.callToggleFullScreen_,
342 false);
346 * Deletes the <embed> element from the container and disconnects.
348 * @return {void} Nothing.
350 remoting.ClientSession.prototype.disconnect = function() {
351 // The plugin won't send a state change notification, so we explicitly log
352 // the fact that the connection has closed.
353 this.logToServer.logClientSessionStateChange(
354 remoting.ClientSession.State.CLOSED,
355 remoting.ClientSession.ConnectionError.NONE, this.mode);
356 if (remoting.wcs) {
357 remoting.wcs.setOnIq(function(stanza) {});
358 this.sendIq_(
359 '<cli:iq ' +
360 'to="' + this.hostJid + '" ' +
361 'type="set" ' +
362 'id="session-terminate" ' +
363 'xmlns:cli="jabber:client">' +
364 '<jingle ' +
365 'xmlns="urn:xmpp:jingle:1" ' +
366 'action="session-terminate" ' +
367 'initiator="' + this.clientJid + '" ' +
368 'sid="' + this.sessionId + '">' +
369 '<reason><success/></reason>' +
370 '</jingle>' +
371 '</cli:iq>');
373 this.removePlugin();
377 * Sends a key combination to the remoting client, by sending down events for
378 * the given keys, followed by up events in reverse order.
380 * @private
381 * @param {[number]} keys Key codes to be sent.
382 * @return {void} Nothing.
384 remoting.ClientSession.prototype.sendKeyCombination_ = function(keys) {
385 for (var i = 0; i < keys.length; i++) {
386 this.plugin.injectKeyEvent(keys[i], true);
388 for (var i = arguments.length -1; i >= 0; i--) {
389 this.plugin.injectKeyEvent(keys[i], false);
394 * Sends a Ctrl-Alt-Del sequence to the remoting client.
396 * @return {void} Nothing.
398 remoting.ClientSession.prototype.sendCtrlAltDel = function() {
399 this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
403 * Sends a Print Screen keypress to the remoting client.
405 * @return {void} Nothing.
407 remoting.ClientSession.prototype.sendPrintScreen = function() {
408 this.sendKeyCombination_([0x070046]);
412 * Enables or disables the client's scale-to-fit feature.
414 * @param {boolean} scaleToFit True to enable scale-to-fit, false otherwise.
415 * @return {void} Nothing.
417 remoting.ClientSession.prototype.setScaleToFit = function(scaleToFit) {
418 this.scaleToFit = scaleToFit;
419 this.updateDimensions();
420 // If enabling scaling, reset bump-scroll offsets.
421 if (scaleToFit) {
422 this.scroll_(0, 0);
427 * Returns whether the client is currently scaling the host to fit the tab.
429 * @return {boolean} The current scale-to-fit setting.
431 remoting.ClientSession.prototype.getScaleToFit = function() {
432 return this.scaleToFit;
436 * Called when the client receives its first frame.
438 * @return {void} Nothing.
440 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
441 this.hasReceivedFrame_ = true;
445 * @return {boolean} Whether the client has received a video buffer.
447 remoting.ClientSession.prototype.hasReceivedFrame = function() {
448 return this.hasReceivedFrame_;
452 * Sends an IQ stanza via the http xmpp proxy.
454 * @private
455 * @param {string} msg XML string of IQ stanza to send to server.
456 * @return {void} Nothing.
458 remoting.ClientSession.prototype.sendIq_ = function(msg) {
459 console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(msg));
460 // Extract the session id, so we can close the session later.
461 var parser = new DOMParser();
462 var iqNode = parser.parseFromString(msg, 'text/xml').firstChild;
463 var jingleNode = iqNode.firstChild;
464 if (jingleNode) {
465 var action = jingleNode.getAttribute('action');
466 if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
467 this.sessionId = jingleNode.getAttribute('sid');
471 // Send the stanza.
472 if (remoting.wcs) {
473 remoting.wcs.sendIq(msg);
474 } else {
475 console.error('Tried to send IQ before WCS was ready.');
476 this.setState_(remoting.ClientSession.State.FAILED);
481 * Connects the plugin to WCS.
483 * @private
484 * @param {string} oauth2AccessToken A valid OAuth2 access token.
485 * @return {void} Nothing.
487 remoting.ClientSession.prototype.connectPluginToWcs_ =
488 function(oauth2AccessToken) {
489 this.clientJid = remoting.wcs.getJid();
490 if (this.clientJid == '') {
491 console.error('Tried to connect without a full JID.');
493 remoting.formatIq.setJids(this.clientJid, this.hostJid);
494 var plugin = this.plugin;
495 var forwardIq = plugin.onIncomingIq.bind(plugin);
496 /** @param {string} stanza The IQ stanza received. */
497 var onIncomingIq = function(stanza) {
498 console.log(remoting.timestamp(),
499 remoting.formatIq.prettifyReceiveIq(stanza));
500 forwardIq(stanza);
502 remoting.wcs.setOnIq(onIncomingIq);
503 this.plugin.connect(this.hostJid, this.hostPublicKey, this.clientJid,
504 this.sharedSecret, this.authenticationMethods,
505 this.authenticationTag);
509 * Callback that the plugin invokes to indicate that the connection
510 * status has changed.
512 * @private
513 * @param {number} status The plugin's status.
514 * @param {number} error The plugin's error state, if any.
516 remoting.ClientSession.prototype.onConnectionStatusUpdate_ =
517 function(status, error) {
518 if (status == remoting.ClientSession.State.CONNECTED) {
519 this.onDesktopSizeChanged_();
520 } else if (status == remoting.ClientSession.State.FAILED) {
521 this.error = /** @type {remoting.ClientSession.ConnectionError} */ (error);
523 this.setState_(/** @type {remoting.ClientSession.State} */ (status));
527 * Callback that the plugin invokes to indicate when the connection is
528 * ready.
530 * @private
531 * @param {boolean} ready True if the connection is ready.
533 remoting.ClientSession.prototype.onConnectionReady_ = function(ready) {
534 if (!ready) {
535 this.plugin.element().classList.add("session-client-inactive");
536 } else {
537 this.plugin.element().classList.remove("session-client-inactive");
542 * @private
543 * @param {remoting.ClientSession.State} newState The new state for the session.
544 * @return {void} Nothing.
546 remoting.ClientSession.prototype.setState_ = function(newState) {
547 var oldState = this.state;
548 this.state = newState;
549 if (this.onStateChange) {
550 this.onStateChange(oldState, newState);
552 // If connection errors are being suppressed from the logs, translate
553 // FAILED to CLOSED here. This ensures that the duration is still logged.
554 var state = this.state;
555 if (this.state == remoting.ClientSession.State.FAILED &&
556 !this.logErrors_) {
557 console.log('Suppressing error.');
558 state = remoting.ClientSession.State.CLOSED;
560 this.logToServer.logClientSessionStateChange(state, this.error, this.mode);
564 * This is a callback that gets called when the window is resized.
566 * @return {void} Nothing.
568 remoting.ClientSession.prototype.onResize = function() {
569 this.updateDimensions();
571 if (this.notifyClientDimensionsTimer_) {
572 window.clearTimeout(this.notifyClientDimensionsTimer_);
573 this.notifyClientDimensionsTimer_ = null;
576 // Defer notifying the host of the change until the window stops resizing, to
577 // avoid overloading the control channel with notifications.
578 this.notifyClientDimensionsTimer_ = window.setTimeout(
579 this.plugin.notifyClientDimensions.bind(this.plugin,
580 window.innerWidth,
581 window.innerHeight),
582 1000);
584 // If bump-scrolling is enabled, adjust the plugin margins to fully utilize
585 // the new window area.
586 this.scroll_(0, 0);
590 * Requests that the host pause or resume video updates.
592 * @param {boolean} pause True to pause video, false to resume.
593 * @return {void} Nothing.
595 remoting.ClientSession.prototype.pauseVideo = function(pause) {
596 if (this.plugin) {
597 this.plugin.pauseVideo(pause)
602 * This is a callback that gets called when the plugin notifies us of a change
603 * in the size of the remote desktop.
605 * @private
606 * @return {void} Nothing.
608 remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() {
609 console.log('desktop size changed: ' +
610 this.plugin.desktopWidth + 'x' +
611 this.plugin.desktopHeight +' @ ' +
612 this.plugin.desktopXDpi + 'x' +
613 this.plugin.desktopYDpi + ' DPI');
614 this.updateDimensions();
618 * Refreshes the plugin's dimensions, taking into account the sizes of the
619 * remote desktop and client window, and the current scale-to-fit setting.
621 * @return {void} Nothing.
623 remoting.ClientSession.prototype.updateDimensions = function() {
624 if (this.plugin.desktopWidth == 0 ||
625 this.plugin.desktopHeight == 0) {
626 return;
629 var windowWidth = window.innerWidth;
630 var windowHeight = window.innerHeight;
631 var scale = 1.0;
633 if (this.getScaleToFit()) {
634 var scaleFitWidth = 1.0 * windowWidth / this.plugin.desktopWidth;
635 var scaleFitHeight = 1.0 * windowHeight / this.plugin.desktopHeight;
636 scale = Math.min(1.0, scaleFitHeight, scaleFitWidth);
639 var width = this.plugin.desktopWidth * scale;
640 var height = this.plugin.desktopHeight * scale;
642 // Resize the plugin if necessary.
643 this.plugin.element().width = width;
644 this.plugin.element().height = height;
646 // Position the container.
647 // TODO(wez): We should take into account scrollbars when positioning.
648 var parentNode = this.plugin.element().parentNode;
650 if (width < windowWidth) {
651 parentNode.style.left = (windowWidth - width) / 2 + 'px';
652 } else {
653 parentNode.style.left = '0';
656 if (height < windowHeight) {
657 parentNode.style.top = (windowHeight - height) / 2 + 'px';
658 } else {
659 parentNode.style.top = '0';
662 console.log('plugin dimensions: ' +
663 parentNode.style.left + ',' +
664 parentNode.style.top + '-' +
665 width + 'x' + height + '.');
669 * Returns an associative array with a set of stats for this connection.
671 * @return {remoting.ClientSession.PerfStats} The connection statistics.
673 remoting.ClientSession.prototype.getPerfStats = function() {
674 return this.plugin.getPerfStats();
678 * Logs statistics.
680 * @param {remoting.ClientSession.PerfStats} stats
682 remoting.ClientSession.prototype.logStatistics = function(stats) {
683 this.logToServer.logStatistics(stats, this.mode);
687 * Enable or disable logging of connection errors. For example, if attempting
688 * a connection using a cached JID, errors should not be logged because the
689 * JID will be refreshed and the connection retried.
691 * @param {boolean} enable True to log errors; false to suppress them.
693 remoting.ClientSession.prototype.logErrors = function(enable) {
694 this.logErrors_ = enable;
698 * Toggles between full-screen and windowed mode.
699 * @return {void} Nothing.
700 * @private
702 remoting.ClientSession.prototype.toggleFullScreen_ = function() {
703 if (document.webkitIsFullScreen) {
704 document.webkitCancelFullScreen();
705 this.enableBumpScroll_(false);
706 } else {
707 document.body.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
708 // Don't enable bump scrolling immediately because it can result in
709 // onMouseMove firing before the webkitIsFullScreen property can be
710 // read safely (crbug.com/132180).
711 window.setTimeout(this.enableBumpScroll_.bind(this, true), 0);
716 * Updates the options menu to reflect the current scale-to-fit and full-screen
717 * settings.
718 * @return {void} Nothing.
719 * @private
721 remoting.ClientSession.prototype.onShowOptionsMenu_ = function() {
722 remoting.MenuButton.select(this.shrinkToFit_, this.scaleToFit);
723 remoting.MenuButton.select(this.originalSize_, !this.scaleToFit);
724 remoting.MenuButton.select(this.fullScreen_, document.webkitIsFullScreen);
728 * Scroll the client plugin by the specified amount, keeping it visible.
729 * Note that this is only used in content full-screen mode (not windowed or
730 * browser full-screen modes), where window.scrollBy and the scrollTop and
731 * scrollLeft properties don't work.
732 * @param {number} dx The amount by which to scroll horizontally. Positive to
733 * scroll right; negative to scroll left.
734 * @param {number} dy The amount by which to scroll vertically. Positive to
735 * scroll down; negative to scroll up.
736 * @return {boolean} True if the requested scroll had no effect because both
737 * vertical and horizontal edges of the screen have been reached.
738 * @private
740 remoting.ClientSession.prototype.scroll_ = function(dx, dy) {
741 var plugin = this.plugin.element();
742 var style = plugin.style;
745 * Helper function for x- and y-scrolling
746 * @param {number|string} curr The current margin, eg. "10px".
747 * @param {number} delta The requested scroll amount.
748 * @param {number} windowBound The size of the window, in pixels.
749 * @param {number} pluginBound The size of the plugin, in pixels.
750 * @param {{stop: boolean}} stop Reference parameter used to indicate when
751 * the scroll has reached one of the edges and can be stopped in that
752 * direction.
753 * @return {string} The new margin value.
755 var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) {
756 var minMargin = Math.min(0, windowBound - pluginBound);
757 var result = (curr ? parseFloat(curr) : 0) - delta;
758 result = Math.min(0, Math.max(minMargin, result));
759 stop.stop = (result == 0 || result == minMargin);
760 return result + "px";
763 var stopX = { stop: false };
764 style.marginLeft = adjustMargin(style.marginLeft, dx,
765 window.innerWidth, plugin.width, stopX);
766 var stopY = { stop: false };
767 style.marginTop = adjustMargin(style.marginTop, dy,
768 window.innerHeight, plugin.height, stopY);
769 return stopX.stop && stopY.stop;
773 * Enable or disable bump-scrolling.
774 * @private
775 * @param {boolean} enable True to enable bump-scrolling, false to disable it.
777 remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) {
778 if (enable) {
779 /** @type {null|function(Event):void} */
780 this.onMouseMoveRef_ = this.onMouseMove_.bind(this);
781 this.plugin.element().addEventListener('mousemove', this.onMouseMoveRef_,
782 false);
783 } else {
784 this.plugin.element().removeEventListener('mousemove', this.onMouseMoveRef_,
785 false);
786 this.onMouseMoveRef_ = null;
791 * @param {Event} event The mouse event.
792 * @private
794 remoting.ClientSession.prototype.onMouseMove_ = function(event) {
795 if (this.bumpScrollTimer_) {
796 window.clearTimeout(this.bumpScrollTimer_);
797 this.bumpScrollTimer_ = null;
799 // It's possible to leave content full-screen mode without using the Screen
800 // Options menu, so we disable bump scrolling as soon as we detect this.
801 if (!document.webkitIsFullScreen) {
802 this.enableBumpScroll_(false);
806 * Compute the scroll speed based on how close the mouse is to the edge.
807 * @param {number} mousePos The mouse x- or y-coordinate
808 * @param {number} size The width or height of the content area.
809 * @return {number} The scroll delta, in pixels.
811 var computeDelta = function(mousePos, size) {
812 var threshold = 10;
813 if (mousePos >= size - threshold) {
814 return 1 + 5 * (mousePos - (size - threshold)) / threshold;
815 } else if (mousePos <= threshold) {
816 return -1 - 5 * (threshold - mousePos) / threshold;
818 return 0;
821 var dx = computeDelta(event.x, window.innerWidth);
822 var dy = computeDelta(event.y, window.innerHeight);
824 if (dx != 0 || dy != 0) {
825 /** @type {remoting.ClientSession} */
826 var that = this;
828 * Scroll the view, and schedule a timer to do so again unless we've hit
829 * the edges of the screen. This timer is cancelled when the mouse moves.
830 * @param {number} expected The time at which we expect to be called.
832 var repeatScroll = function(expected) {
833 /** @type {number} */
834 var now = new Date().getTime();
835 /** @type {number} */
836 var timeout = 10;
837 var lateAdjustment = 1 + (now - expected) / timeout;
838 if (!that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) {
839 that.bumpScrollTimer_ = window.setTimeout(
840 function() { repeatScroll(now + timeout); },
841 timeout);
844 repeatScroll(new Date().getTime());