OLD | NEW |
| (Empty) |
1 // Copyright 2011 (c) The Native Client 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. | |
4 | |
5 /** | |
6 * @fileoverview Implement the view controller class, ViewController, that | |
7 * owns the Life NaCl module and wraps JavaScript bridge calls to it. This | |
8 * class also handles certain UI interactions, such as mouse drags and keyboard | |
9 * shortcuts. | |
10 */ | |
11 | |
12 goog.provide('life.controllers.ViewController'); | |
13 | |
14 goog.require('goog.Disposable'); | |
15 goog.require('goog.dom'); | |
16 goog.require('goog.events.EventTarget'); | |
17 goog.require('goog.fx.DragEvent'); | |
18 goog.require('goog.object'); | |
19 goog.require('goog.style'); | |
20 goog.require('uikit.events.Dragger'); | |
21 | |
22 /** | |
23 * Constructor for the ViewController class. This class encapsulates the | |
24 * Life NaCl module in a view. It also produces some UI events, such as mouse | |
25 * drags. | |
26 * @param {!Object} nativeModule The DOM element that represents a | |
27 * ViewController (usually the <EMBED> element that contains the NaCl | |
28 * module). If undefined, an error is thrown. | |
29 * @constructor | |
30 * @extends {goog.events.EventTarget} | |
31 */ | |
32 life.controllers.ViewController = function(nativeModule) { | |
33 goog.events.EventTarget.call(this); | |
34 /** | |
35 * The element containing the Life NaCl module that corresponds to | |
36 * this object instance. If null or undefined, an exception is thrown. | |
37 * @type {Element} | |
38 * @private | |
39 */ | |
40 if (!nativeModule) { | |
41 throw new Error('ViewController() requires a valid NaCl module'); | |
42 } | |
43 // The container is the containing DOM element. | |
44 this.module_ = nativeModule; | |
45 | |
46 /** | |
47 * The play mode. Accessed via playMode(), mutated via setPlayMode(). | |
48 * Note that changing the play mode can cause the simulation to restart. | |
49 * @type {enum} | |
50 * @private | |
51 */ | |
52 this.playMode_ = life.controllers.ViewController.PlayModes_.RANDOM_SEED; | |
53 | |
54 /** | |
55 * Indicate whether the simulation is running. | |
56 * @type {bool} | |
57 * @private | |
58 */ | |
59 this.isRunning_ = false; | |
60 | |
61 /** | |
62 * The map of stamps. Initialized to a default stamp that produces a glider | |
63 * when using the "normal" Conway rules of 23/3. | |
64 * @type {Hash} | |
65 * @private | |
66 */ | |
67 this.stampDictionary_ = {}; | |
68 this.addStampWithId('***\n*..\n.*.\n', this.DEFAULT_STAMP_ID); | |
69 | |
70 /** | |
71 * Mouse drag event object. | |
72 * @type {life.events.Dragger} | |
73 * @private | |
74 */ | |
75 this.dragListener_ = new uikit.events.Dragger(nativeModule); | |
76 // Hook up a Dragger and listen to the drag events coming from it, then | |
77 // reprocess the events as Life DRAG events. | |
78 goog.events.listen(this.dragListener_, goog.fx.Dragger.EventType.START, | |
79 this.handleStartDrag_, false, this); | |
80 goog.events.listen(this.dragListener_, goog.fx.Dragger.EventType.END, | |
81 this.handleEndDrag_, false, this); | |
82 goog.events.listen(this.dragListener_, goog.fx.Dragger.EventType.DRAG, | |
83 this.handleDrag_, false, this); | |
84 }; | |
85 goog.inherits(life.controllers.ViewController, goog.events.EventTarget); | |
86 | |
87 /** | |
88 * Values for the play mode. These come from the |PLAY_MODE_SELECT| element. | |
89 * @enum {string} | |
90 * @private | |
91 */ | |
92 life.controllers.ViewController.PlayModes_ = { | |
93 RANDOM_SEED: 'random_seed', | |
94 STAMP: 'stamp' | |
95 }; | |
96 | |
97 /** | |
98 * The id for the default stamp. | |
99 * @type {string} | |
100 */ | |
101 life.controllers.ViewController.prototype.DEFAULT_STAMP_ID = 'default_stamp'; | |
102 | |
103 /** | |
104 * Override of disposeInternal() to unhook all the listeners and dispose | |
105 * of retained objects. | |
106 * @override | |
107 */ | |
108 life.controllers.ViewController.prototype.disposeInternal = function() { | |
109 life.controllers.ViewController.superClass_.disposeInternal.call(this); | |
110 goog.events.unlisten(this.dragListener_, goog.fx.Dragger.EventType.START, | |
111 this.handleStartDrag_, false, this); | |
112 goog.events.unlisten(this.dragListener_, goog.fx.Dragger.EventType.DRAG, | |
113 this.handleDrag_, false, this); | |
114 goog.events.unlisten(this.dragListener_, goog.fx.Dragger.EventType.END, | |
115 this.handleEndDrag_, false, this); | |
116 this.dragListener_ = null; | |
117 this.module_ = null; | |
118 this.stampDictionary_ = null; | |
119 }; | |
120 | |
121 /** | |
122 * Simple wrapper that forwards the "clear" method to the NaCl module. | |
123 */ | |
124 life.controllers.ViewController.prototype.clear = function() { | |
125 this.invokeMethod_('clear'); | |
126 } | |
127 | |
128 /** | |
129 * Return the current play mode. | |
130 * @return {enum} The current play mode. | |
131 */ | |
132 life.controllers.ViewController.prototype.playMode = function() { | |
133 return this.playMode_; | |
134 } | |
135 | |
136 /** | |
137 * Set the play mode to one of RANDOM_SEED or STAMP. Changing the play mode | |
138 * can cause the simulation to restart in the new play mode. Do nothing if the | |
139 * play mode is set to the current mode. | |
140 * @param {string} playMode The new play mode. | |
141 */ | |
142 life.controllers.ViewController.prototype.setPlayMode = function(playMode) { | |
143 if (playMode == this.playMode_) | |
144 return; | |
145 this.playMode_ = playMode; | |
146 if (this.isRunning_) { | |
147 this.invokeMethod_('runSimulation', { mode: this.playMode_ }); | |
148 } | |
149 } | |
150 | |
151 /** | |
152 * Set the automaton rules. The rules are expressed as an object that maps | |
153 * birth and keep-alive rules to neighbour counts. | |
154 * @param {Object.<Array>} automatonRules The new rule string. | |
155 */ | |
156 life.controllers.ViewController.prototype.setAutomatonRules = | |
157 function(automatonRules) { | |
158 var ruleString = [automatonRules.keepAliveRule.join(''), | |
159 automatonRules.birthRule.join('')].join('/'); | |
160 this.invokeMethod_('setAutomatonRules', { rules: ruleString }); | |
161 } | |
162 | |
163 /** | |
164 * Add a stamp to the simulation. The stamp is expressed as a string where | |
165 * each character represents a cell: '*' is a live cell and '.' is a dead one. | |
166 * A new-line represents the end of arow of cells. See the .LIF 1.05 format | |
167 * for more details: | |
168 * http://psoup.math.wisc.edu/mcell/ca_files_formats.html | |
169 * If a stamp with |stampId| already exists, then it gets replaced with the | |
170 * new |stampDefinition|. | |
171 * @param {!string} stampDescription The new stamp description. | |
172 * @param {!string} stampId The id associated with this stamp. | |
173 */ | |
174 life.controllers.ViewController.prototype.addStampWithId = | |
175 function(stampDescription, stampId) { | |
176 this.stampDictionary_[stampId] = stampDescription; | |
177 } | |
178 | |
179 /** | |
180 * Set the current stamp. If a stamp with id |stampId| doesn't exist, then | |
181 * do nothing. | |
182 * @param {!string} stampDescription The new stamp description. | |
183 * @param {!string} stampId The id associated with this stamp. | |
184 * @return {bool} Success. | |
185 */ | |
186 life.controllers.ViewController.prototype.selectStamp = function(stampId) { | |
187 if (stampId in this.stampDictionary_) { | |
188 this.invokeMethod_('setCurrentStamp', | |
189 { description: this.stampDictionary_[stampId] }); | |
190 return true; | |
191 } | |
192 return false; | |
193 } | |
194 | |
195 /** | |
196 * Return the encoded string for stamp with id |stampId|. If no such stamp | |
197 * exists, return null. | |
198 * @return {?string} The current stamp string. | |
199 */ | |
200 life.controllers.ViewController.prototype.stampWithId = function(stampId) { | |
201 if (stampId in this.stampDictionary_) | |
202 return this.stampDictionary_[stampId]; | |
203 return null; | |
204 } | |
205 | |
206 /** | |
207 * Start the simulation. Does nothing if it's already running. | |
208 */ | |
209 life.controllers.ViewController.prototype.run = function() { | |
210 this.isRunning_ = true; | |
211 this.invokeMethod_('runSimulation', { mode: this.playMode_ }); | |
212 } | |
213 | |
214 /** | |
215 * Stop the simulation. Does nothing if it's already stopped. | |
216 */ | |
217 life.controllers.ViewController.prototype.stop = function() { | |
218 this.isRunning_ = false; | |
219 this.invokeMethod_('stopSimulation'); | |
220 } | |
221 | |
222 | |
223 /** | |
224 * Set the stamp sound to the file pointed at by |stampSoundUrl|. For now, | |
225 * only .wav files are supported. If there is a currentstamp sound in effect, | |
226 * it will be replaced with this one. | |
227 * @param {!string} stampSoundUrl The Url that points to a .wav file containing | |
228 * the stamp sound. | |
229 */ | |
230 life.controllers.ViewController.prototype.setStampSoundUrl = | |
231 function(stampSoundUrl) { | |
232 if (stampSoundUrl.length > 0) { | |
233 this.invokeMethod_('setStampSoundUrl', { soundUrl: stampSoundUrl }); | |
234 } | |
235 } | |
236 | |
237 /** | |
238 * Convert the coordinate system of |point| to the root window's coordinate | |
239 * system. | |
240 * @param {!goog.math.Coordinate} point The point in the coordinate system | |
241 * of this view. | |
242 * @return {goog.math.Coordinate} The converted point. | |
243 */ | |
244 life.controllers.ViewController.prototype.convertPointToWindow = | |
245 function(point) { | |
246 var offset = goog.style.getFramedPageOffset(this.module_, window); | |
247 return goog.math.Coordinate.difference(point, offset); | |
248 } | |
249 | |
250 /** | |
251 * Format a method invocation and call postMessage with the formatted method | |
252 * string. This calls the NaCl module with the invocation string. Note that | |
253 * this is an asynchronous call into the NaCl module. | |
254 * @param {!string} methodName The name of the method. This must match a | |
255 * published method name in the NaCl module. | |
256 * @param {?Object} parameters A dictionary that maps parameter names to | |
257 * values. All parameter values are passed a strings. | |
258 */ | |
259 life.controllers.ViewController.prototype.invokeMethod_ = | |
260 function(methodName, opt_parameters) { | |
261 var method_invocation = methodName | |
262 if (opt_parameters) { | |
263 for (param in opt_parameters) { | |
264 method_invocation += ' ' + param + ':' + opt_parameters[param] | |
265 } | |
266 } | |
267 this.module_.postMessage(method_invocation); | |
268 } | |
269 | |
270 /** | |
271 * Handle the drag START event: add a cell at the event's coordinates. | |
272 * @param {!goog.fx.DragEvent} dragStartEvent The START event that | |
273 * triggered this handler. | |
274 * @private | |
275 */ | |
276 life.controllers.ViewController.prototype.handleStartDrag_ = | |
277 function(dragStartEvent) { | |
278 dragStartEvent.stopPropagation(); | |
279 var point = this.convertPointToWindow( | |
280 new goog.math.Coordinate(dragStartEvent.clientX, | |
281 dragStartEvent.clientY)); | |
282 this.invokeMethod_('putStampAtPoint', { x: point.x, y: point.y }); | |
283 }; | |
284 | |
285 /** | |
286 * Handle the DRAG event: add a cell at the event's coordinates. If the | |
287 * event goes outside of the drawing area, clip it. | |
288 * @param {!goog.fx.DragEvent} dragEvent The DRAG event that triggered this | |
289 * handler. | |
290 * @private | |
291 */ | |
292 life.controllers.ViewController.prototype.handleDrag_ = function(dragEvent) { | |
293 dragEvent.stopPropagation(); | |
294 var point = this.convertPointToWindow( | |
295 new goog.math.Coordinate(dragEvent.clientX, dragEvent.clientY)); | |
296 this.invokeMethod_('putStampAtPoint', { x: point.x, y: point.y }); | |
297 }; | |
298 | |
299 /** | |
300 * Handle the drag END event: stop propagating the event. | |
301 * @param {!goog.fx.DragEvent} dragEndEvent The END event that triggered this | |
302 * handler. | |
303 * @private | |
304 */ | |
305 life.controllers.ViewController.prototype.handleEndDrag_ = | |
306 function(dragEndEvent) { | |
307 dragEndEvent.stopPropagation(); | |
308 }; | |
OLD | NEW |