Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(807)

Side by Side Diff: remoting/webapp/client_session.js

Issue 336293003: Refactor tool-bar event handlers. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 6 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « remoting/webapp/client_screen.js ('k') | remoting/webapp/event_handlers.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * @fileoverview 6 * @fileoverview
7 * Class handling creation and teardown of a remoting client session. 7 * Class handling creation and teardown of a remoting client session.
8 * 8 *
9 * The ClientSession class controls lifetime of the client plugin 9 * The ClientSession class controls lifetime of the client plugin
10 * object and provides the plugin with the functionality it needs to 10 * object and provides the plugin with the functionality it needs to
11 * establish connection. Specifically it: 11 * establish connection. Specifically it:
12 * - Delivers incoming/outgoing signaling messages, 12 * - Delivers incoming/outgoing signaling messages,
13 * - Adjusts plugin size and position when destop resolution changes, 13 * - Adjusts plugin size and position when destop resolution changes,
14 * 14 *
15 * This class should not access the plugin directly, instead it should 15 * This class should not access the plugin directly, instead it should
16 * do it through ClientPlugin class which abstracts plugin version 16 * do it through ClientPlugin class which abstracts plugin version
17 * differences. 17 * differences.
18 */ 18 */
19 19
20 'use strict'; 20 'use strict';
21 21
22 /** @suppress {duplicate} */ 22 /** @suppress {duplicate} */
23 var remoting = remoting || {}; 23 var remoting = remoting || {};
24 24
25 /** 25 /**
26 * @param {string} hostDisplayName A human-readable name for the host.
26 * @param {string} accessCode The IT2Me access code. Blank for Me2Me. 27 * @param {string} accessCode The IT2Me access code. Blank for Me2Me.
27 * @param {function(boolean, function(string): void): void} fetchPin 28 * @param {function(boolean, function(string): void): void} fetchPin
28 * Called by Me2Me connections when a PIN needs to be obtained 29 * Called by Me2Me connections when a PIN needs to be obtained
29 * interactively. 30 * interactively.
30 * @param {function(string, string, string, 31 * @param {function(string, string, string,
31 * function(string, string): void): void} 32 * function(string, string): void): void}
32 * fetchThirdPartyToken Called by Me2Me connections when a third party 33 * fetchThirdPartyToken Called by Me2Me connections when a third party
33 * authentication token must be obtained. 34 * authentication token must be obtained.
34 * @param {string} authenticationMethods Comma-separated list of 35 * @param {string} authenticationMethods Comma-separated list of
35 * authentication methods the client should attempt to use. 36 * authentication methods the client should attempt to use.
36 * @param {string} hostId The host identifier for Me2Me, or empty for IT2Me. 37 * @param {string} hostId The host identifier for Me2Me, or empty for IT2Me.
37 * Mixed into authentication hashes for some authentication methods. 38 * Mixed into authentication hashes for some authentication methods.
38 * @param {string} hostJid The jid of the host to connect to. 39 * @param {string} hostJid The jid of the host to connect to.
39 * @param {string} hostPublicKey The base64 encoded version of the host's 40 * @param {string} hostPublicKey The base64 encoded version of the host's
40 * public key. 41 * public key.
41 * @param {remoting.ClientSession.Mode} mode The mode of this connection. 42 * @param {remoting.ClientSession.Mode} mode The mode of this connection.
42 * @param {string} clientPairingId For paired Me2Me connections, the 43 * @param {string} clientPairingId For paired Me2Me connections, the
43 * pairing id for this client, as issued by the host. 44 * pairing id for this client, as issued by the host.
44 * @param {string} clientPairedSecret For paired Me2Me connections, the 45 * @param {string} clientPairedSecret For paired Me2Me connections, the
45 * paired secret for this client, as issued by the host. 46 * paired secret for this client, as issued by the host.
46 * @constructor 47 * @constructor
47 * @extends {base.EventSource} 48 * @extends {base.EventSource}
48 */ 49 */
49 remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken, 50 remoting.ClientSession = function(hostDisplayName, accessCode, fetchPin,
50 authenticationMethods, 51 fetchThirdPartyToken, authenticationMethods,
51 hostId, hostJid, hostPublicKey, mode, 52 hostId, hostJid, hostPublicKey, mode,
52 clientPairingId, clientPairedSecret) { 53 clientPairingId, clientPairedSecret) {
53 /** @private */ 54 /** @private */
54 this.state_ = remoting.ClientSession.State.CREATED; 55 this.state_ = remoting.ClientSession.State.CREATED;
55 56
56 /** @private */ 57 /** @private */
57 this.error_ = remoting.Error.NONE; 58 this.error_ = remoting.Error.NONE;
58 59
59 /** @private */ 60 /** @private */
61 this.hostDisplayName_ = hostDisplayName;
62 /** @private */
60 this.hostJid_ = hostJid; 63 this.hostJid_ = hostJid;
61 /** @private */ 64 /** @private */
62 this.hostPublicKey_ = hostPublicKey; 65 this.hostPublicKey_ = hostPublicKey;
63 /** @private */ 66 /** @private */
64 this.accessCode_ = accessCode; 67 this.accessCode_ = accessCode;
65 /** @private */ 68 /** @private */
66 this.fetchPin_ = fetchPin; 69 this.fetchPin_ = fetchPin;
67 /** @private */ 70 /** @private */
68 this.fetchThirdPartyToken_ = fetchThirdPartyToken; 71 this.fetchThirdPartyToken_ = fetchThirdPartyToken;
69 /** @private */ 72 /** @private */
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after
102 * 105 *
103 * @type {boolean} @private 106 * @type {boolean} @private
104 */ 107 */
105 this.logHostOfflineErrors_ = true; 108 this.logHostOfflineErrors_ = true;
106 109
107 /** @private */ 110 /** @private */
108 this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this); 111 this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this);
109 /** @private */ 112 /** @private */
110 this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this); 113 this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
111 /** @private */ 114 /** @private */
112 this.callSetScreenMode_ = this.onSetScreenMode_.bind(this);
113 /** @private */
114 this.callToggleFullScreen_ = remoting.fullscreen.toggle.bind( 115 this.callToggleFullScreen_ = remoting.fullscreen.toggle.bind(
115 remoting.fullscreen); 116 remoting.fullscreen);
116 /** @private */ 117 /** @private */
117 this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this) 118 this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this)
118 119
119 /** @private */ 120 /** @private */
120 this.screenOptionsMenu_ = new remoting.MenuButton( 121 this.screenOptionsMenu_ = new remoting.MenuButton(
121 document.getElementById('screen-options-menu'), 122 document.getElementById('screen-options-menu'),
122 this.onShowOptionsMenu_.bind(this)); 123 this.onShowOptionsMenu_.bind(this));
123 /** @private */ 124 /** @private */
(...skipping 13 matching lines...) Expand all
137 this.fullScreenButton_ = document.getElementById('toggle-full-screen'); 138 this.fullScreenButton_ = document.getElementById('toggle-full-screen');
138 139
139 /** @type {remoting.GnubbyAuthHandler} @private */ 140 /** @type {remoting.GnubbyAuthHandler} @private */
140 this.gnubbyAuthHandler_ = null; 141 this.gnubbyAuthHandler_ = null;
141 142
142 if (this.mode_ == remoting.ClientSession.Mode.IT2ME) { 143 if (this.mode_ == remoting.ClientSession.Mode.IT2ME) {
143 // Resize-to-client is not supported for IT2Me hosts. 144 // Resize-to-client is not supported for IT2Me hosts.
144 this.resizeToClientButton_.hidden = true; 145 this.resizeToClientButton_.hidden = true;
145 } else { 146 } else {
146 this.resizeToClientButton_.hidden = false; 147 this.resizeToClientButton_.hidden = false;
147 this.resizeToClientButton_.addEventListener(
148 'click', this.callSetScreenMode_, false);
149 } 148 }
150 149
151 this.shrinkToFitButton_.addEventListener(
152 'click', this.callSetScreenMode_, false);
153 this.fullScreenButton_.addEventListener( 150 this.fullScreenButton_.addEventListener(
154 'click', this.callToggleFullScreen_, false); 151 'click', this.callToggleFullScreen_, false);
155 this.defineEvents(Object.keys(remoting.ClientSession.Events)); 152 this.defineEvents(Object.keys(remoting.ClientSession.Events));
156 }; 153 };
157 154
158 base.extend(remoting.ClientSession, base.EventSource); 155 base.extend(remoting.ClientSession, base.EventSource);
159 156
160 /** @enum {string} */ 157 /** @enum {string} */
161 remoting.ClientSession.Events = { 158 remoting.ClientSession.Events = {
162 stateChanged: 'stateChanged', 159 stateChanged: 'stateChanged',
163 videoChannelStateChanged: 'videoChannelStateChanged' 160 videoChannelStateChanged: 'videoChannelStateChanged'
164 }; 161 };
165 162
166 /** 163 /**
164 * Get host display name.
165 *
166 * @return {string}
167 */
168 remoting.ClientSession.prototype.getHostDisplayName = function() {
169 return this.hostDisplayName_;
170 };
171
172 /**
167 * Called when the window or desktop size or the scaling settings change, 173 * Called when the window or desktop size or the scaling settings change,
168 * to set the scroll-bar visibility. 174 * to set the scroll-bar visibility.
169 * 175 *
170 * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is 176 * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is
171 * fixed. 177 * fixed.
172 */ 178 */
173 remoting.ClientSession.prototype.updateScrollbarVisibility = function() { 179 remoting.ClientSession.prototype.updateScrollbarVisibility = function() {
174 var needsVerticalScroll = false; 180 var needsVerticalScroll = false;
175 var needsHorizontalScroll = false; 181 var needsHorizontalScroll = false;
176 if (!this.shrinkToFit_) { 182 if (!this.shrinkToFit_) {
(...skipping 18 matching lines...) Expand all
195 } else { 201 } else {
196 scroller.classList.add('no-horizontal-scroll'); 202 scroller.classList.add('no-horizontal-scroll');
197 } 203 }
198 if (needsVerticalScroll) { 204 if (needsVerticalScroll) {
199 scroller.classList.remove('no-vertical-scroll'); 205 scroller.classList.remove('no-vertical-scroll');
200 } else { 206 } else {
201 scroller.classList.add('no-vertical-scroll'); 207 scroller.classList.add('no-vertical-scroll');
202 } 208 }
203 }; 209 };
204 210
211 /**
212 * @return {boolean} True if shrink-to-fit is enabled; false otherwise.
213 */
214 remoting.ClientSession.prototype.getShrinkToFit = function() {
215 return this.shrinkToFit_;
216 };
217
218 /**
219 * @return {boolean} True if resize-to-client is enabled; false otherwise.
220 */
221 remoting.ClientSession.prototype.getResizeToClient = function() {
222 return this.resizeToClient_;
223 };
224
205 // Note that the positive values in both of these enums are copied directly 225 // Note that the positive values in both of these enums are copied directly
206 // from chromoting_scriptable_object.h and must be kept in sync. The negative 226 // from chromoting_scriptable_object.h and must be kept in sync. The negative
207 // values represent state transitions that occur within the web-app that have 227 // values represent state transitions that occur within the web-app that have
208 // no corresponding plugin state transition. 228 // no corresponding plugin state transition.
209 /** @enum {number} */ 229 /** @enum {number} */
210 remoting.ClientSession.State = { 230 remoting.ClientSession.State = {
211 CONNECTION_CANCELED: -3, // Connection closed (gracefully) before connecting. 231 CONNECTION_CANCELED: -3, // Connection closed (gracefully) before connecting.
212 CONNECTION_DROPPED: -2, // Succeeded, but subsequently closed with an error. 232 CONNECTION_DROPPED: -2, // Succeeded, but subsequently closed with an error.
213 CREATED: -1, 233 CREATED: -1,
214 UNKNOWN: 0, 234 UNKNOWN: 0,
(...skipping 341 matching lines...) Expand 10 before | Expand all | Expand 10 after
556 if (this.plugin_) { 576 if (this.plugin_) {
557 this.plugin_.element().removeEventListener( 577 this.plugin_.element().removeEventListener(
558 'focus', this.callPluginGotFocus_, false); 578 'focus', this.callPluginGotFocus_, false);
559 this.plugin_.element().removeEventListener( 579 this.plugin_.element().removeEventListener(
560 'blur', this.callPluginLostFocus_, false); 580 'blur', this.callPluginLostFocus_, false);
561 this.plugin_.cleanup(); 581 this.plugin_.cleanup();
562 this.plugin_ = null; 582 this.plugin_ = null;
563 } 583 }
564 584
565 // Delete event handlers that aren't relevent when not connected. 585 // Delete event handlers that aren't relevent when not connected.
566 this.resizeToClientButton_.removeEventListener(
567 'click', this.callSetScreenMode_, false);
568 this.shrinkToFitButton_.removeEventListener(
569 'click', this.callSetScreenMode_, false);
570 this.fullScreenButton_.removeEventListener( 586 this.fullScreenButton_.removeEventListener(
571 'click', this.callToggleFullScreen_, false); 587 'click', this.callToggleFullScreen_, false);
572 588
573 // Leave full-screen mode, and stop listening for related events. 589 // Leave full-screen mode, and stop listening for related events.
574 var listener = this.callOnFullScreenChanged_; 590 var listener = this.callOnFullScreenChanged_;
575 remoting.fullscreen.syncWithMaximize(false); 591 remoting.fullscreen.syncWithMaximize(false);
576 remoting.fullscreen.activate( 592 remoting.fullscreen.activate(
577 false, 593 false,
578 function() { 594 function() {
579 remoting.fullscreen.removeListener(listener); 595 remoting.fullscreen.removeListener(listener);
580 }); 596 });
581 if (remoting.windowFrame) { 597 if (remoting.windowFrame) {
582 remoting.windowFrame.setConnected(false); 598 remoting.windowFrame.setConnected(false);
583 } 599 }
600 remoting.toolbar.setClientSession(null);
584 601
585 // Remove mediasource-rendering class from video-contained - this will also 602 // Remove mediasource-rendering class from video-contained - this will also
586 // hide the <video> element. 603 // hide the <video> element.
587 /** @type {HTMLElement} */(document.getElementById('video-container')) 604 /** @type {HTMLElement} */(document.getElementById('video-container'))
588 .classList.remove('mediasource-rendering'); 605 .classList.remove('mediasource-rendering');
589 }; 606 };
590 607
591 /** 608 /**
592 * Disconnect the current session with a particular |error|. The session will 609 * Disconnect the current session with a particular |error|. The session will
593 * raise a |stateChanged| event in response to it. The caller should then call 610 * raise a |stateChanged| event in response to it. The caller should then call
(...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after
669 this.plugin_.injectKeyEvent(keys[i], false); 686 this.plugin_.injectKeyEvent(keys[i], false);
670 } 687 }
671 } 688 }
672 689
673 /** 690 /**
674 * Sends a Ctrl-Alt-Del sequence to the remoting client. 691 * Sends a Ctrl-Alt-Del sequence to the remoting client.
675 * 692 *
676 * @return {void} Nothing. 693 * @return {void} Nothing.
677 */ 694 */
678 remoting.ClientSession.prototype.sendCtrlAltDel = function() { 695 remoting.ClientSession.prototype.sendCtrlAltDel = function() {
696 console.log('Sending Ctrl-Alt-Del.');
679 this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]); 697 this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
680 } 698 }
681 699
682 /** 700 /**
683 * Sends a Print Screen keypress to the remoting client. 701 * Sends a Print Screen keypress to the remoting client.
684 * 702 *
685 * @return {void} Nothing. 703 * @return {void} Nothing.
686 */ 704 */
687 remoting.ClientSession.prototype.sendPrintScreen = function() { 705 remoting.ClientSession.prototype.sendPrintScreen = function() {
706 console.log('Sending Print Screen.');
688 this.sendKeyCombination_([0x070046]); 707 this.sendKeyCombination_([0x070046]);
689 } 708 }
690 709
691 /** 710 /**
692 * Sets and stores the key remapping setting for the current host. 711 * Sets and stores the key remapping setting for the current host.
693 * 712 *
694 * @param {string} remappings Comma separated list of key remappings. 713 * @param {string} remappings Comma separated list of key remappings.
695 */ 714 */
696 remoting.ClientSession.prototype.setRemapKeys = function(remappings) { 715 remoting.ClientSession.prototype.setRemapKeys = function(remappings) {
697 // Cancel any existing remappings and apply the new ones. 716 // Cancel any existing remappings and apply the new ones.
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after
740 '>0x' + toKey.toString(16)); 759 '>0x' + toKey.toString(16));
741 this.plugin_.remapKey(fromKey, toKey); 760 this.plugin_.remapKey(fromKey, toKey);
742 } else { 761 } else {
743 console.log('cancel remapKey 0x' + fromKey.toString(16)); 762 console.log('cancel remapKey 0x' + fromKey.toString(16));
744 this.plugin_.remapKey(fromKey, fromKey); 763 this.plugin_.remapKey(fromKey, fromKey);
745 } 764 }
746 } 765 }
747 } 766 }
748 767
749 /** 768 /**
750 * Callback for the two "screen mode" related menu items: Resize desktop to
751 * fit and Shrink to fit.
752 *
753 * @param {Event} event The click event indicating which mode was selected.
754 * @return {void} Nothing.
755 * @private
756 */
757 remoting.ClientSession.prototype.onSetScreenMode_ = function(event) {
758 var shrinkToFit = this.shrinkToFit_;
759 var resizeToClient = this.resizeToClient_;
760 if (event.target == this.shrinkToFitButton_) {
761 shrinkToFit = !shrinkToFit;
762 }
763 if (event.target == this.resizeToClientButton_) {
764 resizeToClient = !resizeToClient;
765 }
766 this.setScreenMode_(shrinkToFit, resizeToClient);
767 };
768
769 /**
770 * Set the shrink-to-fit and resize-to-client flags and save them if this is 769 * Set the shrink-to-fit and resize-to-client flags and save them if this is
771 * a Me2Me connection. 770 * a Me2Me connection.
772 * 771 *
773 * @param {boolean} shrinkToFit True if the remote desktop should be scaled 772 * @param {boolean} shrinkToFit True if the remote desktop should be scaled
774 * down if it is larger than the client window; false if scroll-bars 773 * down if it is larger than the client window; false if scroll-bars
775 * should be added in this case. 774 * should be added in this case.
776 * @param {boolean} resizeToClient True if window resizes should cause the 775 * @param {boolean} resizeToClient True if window resizes should cause the
777 * host to attempt to resize its desktop to match the client window size; 776 * host to attempt to resize its desktop to match the client window size;
778 * false to disable this behaviour for subsequent window resizes--the 777 * false to disable this behaviour for subsequent window resizes--the
779 * current host desktop size is not restored in this case. 778 * current host desktop size is not restored in this case.
780 * @return {void} Nothing. 779 * @return {void} Nothing.
781 * @private
782 */ 780 */
783 remoting.ClientSession.prototype.setScreenMode_ = 781 remoting.ClientSession.prototype.setScreenMode =
784 function(shrinkToFit, resizeToClient) { 782 function(shrinkToFit, resizeToClient) {
785 if (resizeToClient && !this.resizeToClient_) { 783 if (resizeToClient && !this.resizeToClient_) {
786 var clientArea = this.getClientArea_(); 784 var clientArea = this.getClientArea_();
787 this.plugin_.notifyClientResolution(clientArea.width, 785 this.plugin_.notifyClientResolution(clientArea.width,
788 clientArea.height, 786 clientArea.height,
789 window.devicePixelRatio); 787 window.devicePixelRatio);
790 } 788 }
791 789
792 // If enabling shrink, reset bump-scroll offsets. 790 // If enabling shrink, reset bump-scroll offsets.
793 var needsScrollReset = shrinkToFit && !this.shrinkToFit_; 791 var needsScrollReset = shrinkToFit && !this.shrinkToFit_;
(...skipping 181 matching lines...) Expand 10 before | Expand all | Expand 10 after
975 this.plugin_.notifyClientResolution(clientArea.width, 973 this.plugin_.notifyClientResolution(clientArea.width,
976 clientArea.height, 974 clientArea.height,
977 window.devicePixelRatio); 975 window.devicePixelRatio);
978 } 976 }
979 // Activate full-screen related UX. 977 // Activate full-screen related UX.
980 remoting.fullscreen.addListener(this.callOnFullScreenChanged_); 978 remoting.fullscreen.addListener(this.callOnFullScreenChanged_);
981 remoting.fullscreen.syncWithMaximize(true); 979 remoting.fullscreen.syncWithMaximize(true);
982 if (remoting.windowFrame) { 980 if (remoting.windowFrame) {
983 remoting.windowFrame.setConnected(true); 981 remoting.windowFrame.setConnected(true);
984 } 982 }
983 remoting.toolbar.setClientSession(this);
985 984
986 } else if (status == remoting.ClientSession.State.FAILED) { 985 } else if (status == remoting.ClientSession.State.FAILED) {
987 switch (error) { 986 switch (error) {
988 case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE: 987 case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
989 this.error_ = remoting.Error.HOST_IS_OFFLINE; 988 this.error_ = remoting.Error.HOST_IS_OFFLINE;
990 break; 989 break;
991 case remoting.ClientSession.ConnectionError.SESSION_REJECTED: 990 case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
992 this.error_ = remoting.Error.INVALID_ACCESS_CODE; 991 this.error_ = remoting.Error.INVALID_ACCESS_CODE;
993 break; 992 break;
994 case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL: 993 case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
(...skipping 512 matching lines...) Expand 10 before | Expand all | Expand 10 after
1507 * @return {{width: number, height: number}} The height of the window's client 1506 * @return {{width: number, height: number}} The height of the window's client
1508 * area. This differs between apps v1 and apps v2 due to the custom window 1507 * area. This differs between apps v1 and apps v2 due to the custom window
1509 * borders used by the latter. 1508 * borders used by the latter.
1510 * @private 1509 * @private
1511 */ 1510 */
1512 remoting.ClientSession.prototype.getClientArea_ = function() { 1511 remoting.ClientSession.prototype.getClientArea_ = function() {
1513 return remoting.windowFrame ? 1512 return remoting.windowFrame ?
1514 remoting.windowFrame.getClientArea() : 1513 remoting.windowFrame.getClientArea() :
1515 { 'width': window.innerWidth, 'height': window.innerHeight }; 1514 { 'width': window.innerWidth, 'height': window.innerHeight };
1516 } 1515 }
OLDNEW
« no previous file with comments | « remoting/webapp/client_screen.js ('k') | remoting/webapp/event_handlers.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698