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.
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
22 /** @suppress {duplicate} */
23 var remoting
= remoting
|| {};
26 * @param {string} hostJid The jid of the host to connect to.
27 * @param {string} hostPublicKey The base64 encoded version of the host's
29 * @param {string} sharedSecret The access code for IT2Me or the PIN
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.
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
;
56 /** @type {remoting.ClientPlugin} */
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;
67 this.callPluginLostFocus_
= this.pluginLostFocus_
.bind(this);
69 this.callPluginGotFocus_
= this.pluginGotFocus_
.bind(this);
71 this.callEnableShrink_
= this.setScaleToFit
.bind(this, true);
73 this.callDisableShrink_
= this.setScaleToFit
.bind(this, false);
75 this.callToggleFullScreen_
= this.toggleFullScreen_
.bind(this);
77 this.screenOptionsMenu_
= new remoting
.MenuButton(
78 document
.getElementById('screen-options-menu'),
79 this.onShowOptionsMenu_
.bind(this));
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;
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
= {
113 BAD_PLUGIN_VERSION
: -2,
114 UNKNOWN_PLUGIN_ERROR
: -1,
123 /** @enum {number} */
124 remoting
.ClientSession
.ConnectionError
= {
129 INCOMPATIBLE_PROTOCOL
: 3,
134 // The mode of this session.
135 /** @enum {number} */
136 remoting
.ClientSession
.Mode
= {
142 * Type used for performance statistics collected by the plugin.
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
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
205 remoting
.ClientSession
.prototype.createClientPlugin_ = function(container
, id
) {
206 var plugin
= /** @type {remoting.ViewerPlugin} */
207 document
.createElement('embed');
210 plugin
.src
= 'about://none';
211 plugin
.type
= 'application/vnd.chromium.remoting-viewer';
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() {
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
) {
269 console
.error('ERROR: remoting plugin not loaded');
270 this.plugin
.cleanup();
272 this.setState_(remoting
.ClientSession
.State
.UNKNOWN_PLUGIN_ERROR
);
276 if (!this.plugin
.isSupportedVersion()) {
277 this.plugin
.cleanup();
279 this.setState_(remoting
.ClientSession
.State
.BAD_PLUGIN_VERSION
);
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() {
331 this.plugin
.element().removeEventListener(
332 'focus', this.callPluginGotFocus_
, false);
333 this.plugin
.element().removeEventListener(
334 'blur', this.callPluginLostFocus_
, false);
335 this.plugin
.cleanup();
338 this.shrinkToFit_
.removeEventListener('click', this.callEnableShrink_
, false);
339 this.originalSize_
.removeEventListener('click', this.callDisableShrink_
,
341 this.fullScreen_
.removeEventListener('click', this.callToggleFullScreen_
,
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
);
357 remoting
.wcs
.setOnIq(function(stanza
) {});
360 'to="' + this.hostJid
+ '" ' +
362 'id="session-terminate" ' +
363 'xmlns:cli="jabber:client">' +
365 'xmlns="urn:xmpp:jingle:1" ' +
366 'action="session-terminate" ' +
367 'initiator="' + this.clientJid
+ '" ' +
368 'sid="' + this.sessionId
+ '">' +
369 '<reason><success/></reason>' +
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.
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.
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.
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
;
465 var action
= jingleNode
.getAttribute('action');
466 if (jingleNode
.nodeName
== 'jingle' && action
== 'session-initiate') {
467 this.sessionId
= jingleNode
.getAttribute('sid');
473 remoting
.wcs
.sendIq(msg
);
475 console
.error('Tried to send IQ before WCS was ready.');
476 this.setState_(remoting
.ClientSession
.State
.FAILED
);
481 * Connects the plugin to WCS.
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
));
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.
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
531 * @param {boolean} ready True if the connection is ready.
533 remoting
.ClientSession
.prototype.onConnectionReady_ = function(ready
) {
535 this.plugin
.element().classList
.add("session-client-inactive");
537 this.plugin
.element().classList
.remove("session-client-inactive");
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
&&
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
,
584 // If bump-scrolling is enabled, adjust the plugin margins to fully utilize
585 // the new window area.
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
) {
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.
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) {
629 var windowWidth
= window
.innerWidth
;
630 var windowHeight
= window
.innerHeight
;
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';
653 parentNode
.style
.left
= '0';
656 if (height
< windowHeight
) {
657 parentNode
.style
.top
= (windowHeight
- height
) / 2 + 'px';
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();
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.
702 remoting
.ClientSession
.prototype.toggleFullScreen_ = function() {
703 if (document
.webkitIsFullScreen
) {
704 document
.webkitCancelFullScreen();
705 this.enableBumpScroll_(false);
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
718 * @return {void} Nothing.
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.
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
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.
775 * @param {boolean} enable True to enable bump-scrolling, false to disable it.
777 remoting
.ClientSession
.prototype.enableBumpScroll_ = function(enable
) {
779 /** @type {null|function(Event):void} */
780 this.onMouseMoveRef_
= this.onMouseMove_
.bind(this);
781 this.plugin
.element().addEventListener('mousemove', this.onMouseMoveRef_
,
784 this.plugin
.element().removeEventListener('mousemove', this.onMouseMoveRef_
,
786 this.onMouseMoveRef_
= null;
791 * @param {Event} event The mouse event.
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
) {
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
;
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} */
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} */
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
); },
844 repeatScroll(new Date().getTime());