1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 sc_require('system/responder'); 8 9 /** @namespace 10 11 The root object for a responder chain. A responder context can dispatch 12 actions directly to a first responder; walking up the responder chain until 13 it finds a responder that can handle the action. 14 15 If no responder can be found to handle the action, it will attempt to send 16 the action to the defaultResponder. 17 18 You can have as many ResponderContext's as you want within your application. 19 Every SC.Pane and SC.Application automatically implements this mixin. 20 21 Note that to implement this, you should mix SC.ResponderContext into an 22 SC.Responder or SC.Responder subclass. 23 24 @since SproutCore 1.0 25 */ 26 SC.ResponderContext = { 27 28 //@if(debug) 29 /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */ 30 31 /** @property 32 33 When set to YES, logs tracing information about all actions sent and 34 responder changes. 35 */ 36 trace: NO, 37 38 /* END DEBUG ONLY PROPERTIES AND METHODS */ 39 //@endif 40 41 // .......................................................... 42 // PROPERTIES 43 // 44 45 isResponderContext: YES, 46 47 /** @property 48 The default responder. Set this to point to a responder object that can 49 respond to events when no other view in the hierarchy handles them. 50 51 @type SC.Responder 52 */ 53 defaultResponder: null, 54 55 /** @property 56 The next responder for an app is always its defaultResponder. 57 */ 58 nextResponder: function() { 59 return this.get('defaultResponder'); 60 }.property('defaultResponder').cacheable(), 61 62 /** @property 63 The first responder. This is the first responder that should receive 64 actions. 65 */ 66 firstResponder: null, 67 68 // .......................................................... 69 // METHODS 70 // 71 72 /** 73 Finds the next responder for the passed responder based on the responder's 74 nextResponder property. If the property is a string, then lookup the path 75 in the receiver. 76 */ 77 nextResponderFor: function(responder) { 78 var next = responder.get('nextResponder'); 79 if (typeof next === SC.T_STRING) { 80 next = SC.objectForPropertyPath(next, this); 81 } else if (!next && (responder !== this)) next = this ; 82 return next ; 83 }, 84 85 /** 86 Finds the responder name by searching the responders one time. 87 */ 88 responderNameFor: function(responder) { 89 if (!responder) return "(No Responder)"; 90 else if (responder._scrc_name) return responder._scrc_name; 91 92 // none found, let's go hunting...look three levels deep 93 var n = this.NAMESPACE; 94 this._findResponderNamesFor(this, 3, n ? [this.NAMESPACE] : []); 95 96 return responder._scrc_name || responder.toString(); // try again 97 }, 98 99 /** @private */ 100 _findResponderNamesFor: function(responder, level, path) { 101 var key, value; 102 103 for(key in responder) { 104 if (key === 'nextResponder') continue ; 105 value = responder[key]; 106 if (value && value.isResponder) { 107 if (value._scrc_name) continue ; 108 path.push(key); 109 value._scrc_name = path.join('.'); 110 if (level>0) this._findResponderNamesFor(value, level-1, path); 111 path.pop(); 112 } 113 } 114 }, 115 116 /** 117 Makes the passed responder into the new firstResponder for this 118 responder context. This will cause the current first responder to lose 119 its responder status and possibly keyResponder status as well. 120 121 When you change the first responder, this will send callbacks to 122 responders up the chain until you reach a shared responder, at which point 123 it will stop notifying. 124 125 @param {SC.Responder} responder 126 @param {Event} evt that cause this to become first responder 127 @returns {SC.ResponderContext} receiver 128 */ 129 makeFirstResponder: function(responder, evt) { 130 var current = this.get('firstResponder'), 131 last = this.get('nextResponder'), 132 //@if(debug) 133 trace = this.get('trace'), 134 //@endif 135 common ; 136 137 if (this._locked) { 138 //@if(debug) 139 if (trace) { 140 SC.Logger.log('%@: AFTER ACTION: makeFirstResponder => %@'.fmt(this, this.responderNameFor(responder))); 141 } 142 //@endif 143 144 this._pendingResponder = responder; 145 return ; 146 } 147 148 //@if(debug) 149 if (trace) { 150 SC.Logger.log('%@: makeFirstResponder => %@'.fmt(this, this.responderNameFor(responder))); 151 } 152 //@endif 153 154 if (responder) responder.set("becomingFirstResponder", YES); 155 156 this._locked = YES; 157 this._pendingResponder = null; 158 159 // Find the nearest common responder in the responder chain for the new 160 // responder. If there are no common responders, use last responder. 161 // Note: start at the responder itself: it could be the common responder. 162 common = responder ? responder : null; 163 while (common) { 164 if (common.get('hasFirstResponder')) break; 165 common = (common===last) ? null : this.nextResponderFor(common); 166 } 167 if (!common) common = last; 168 169 // Cleanup old first responder 170 this._notifyWillLoseFirstResponder(current, current, common, evt); 171 if (current) current.set('isFirstResponder', NO); 172 173 // Set new first responder. If new firstResponder does not have its 174 // responderContext property set, then set it. 175 176 // but, don't tell anyone until we have _also_ updated the hasFirstResponder state. 177 this.beginPropertyChanges(); 178 179 this.set('firstResponder', responder) ; 180 if (responder) responder.set('isFirstResponder', YES); 181 182 this._notifyDidBecomeFirstResponder(responder, responder, common); 183 184 // now, tell everyone the good news! 185 this.endPropertyChanges(); 186 187 this._locked = NO ; 188 if (this._pendingResponder) { 189 this.makeFirstResponder(this._pendingResponder); 190 this._pendingResponder = null; 191 } 192 193 if (responder) responder.set("becomingFirstResponder", NO); 194 195 return this ; 196 }, 197 198 _notifyWillLoseFirstResponder: function(responder, cur, root, evt) { 199 if (!cur || cur === root) return ; // nothing to do 200 201 cur.willLoseFirstResponder(responder, evt); 202 cur.set('hasFirstResponder', NO); 203 204 var next = this.nextResponderFor(cur); 205 if (next) this._notifyWillLoseFirstResponder(responder, next, root); 206 }, 207 208 _notifyDidBecomeFirstResponder: function(responder, cur, root) { 209 if (!cur || cur === root) return ; // nothing to do 210 211 var next = this.nextResponderFor(cur); 212 if (next) this._notifyDidBecomeFirstResponder(responder, next, root); 213 214 cur.set('hasFirstResponder', YES); 215 cur.didBecomeFirstResponder(responder); 216 }, 217 218 /** 219 Re-enters the current responder (calling willLoseFirstResponder and didBecomeFirstResponder). 220 */ 221 resetFirstResponder: function() { 222 var current = this.get('firstResponder'); 223 if (!current) return; 224 current.willLoseFirstResponder(); 225 current.didBecomeFirstResponder(); 226 }, 227 228 /** 229 Send the passed action down the responder chain, starting with the 230 current first responder. This will look for the first responder that 231 actually implements the action method and returns YES or no value when 232 called. 233 234 @param {String} action name of action 235 @param {Object} sender object sending the action 236 @param {Object} [context] additional context info 237 @returns {SC.Responder} the responder that handled it or null 238 */ 239 sendAction: function(action, sender, context) { 240 var working = this.get('firstResponder'), 241 last = this.get('nextResponder'), 242 //@if(debug) 243 trace = this.get('trace'), 244 //@endif 245 handled = NO, 246 responder; 247 248 this._locked = YES; 249 250 //@if(debug) 251 if (trace) { 252 SC.Logger.log("%@: begin action '%@' (%@, %@)".fmt(this, action, sender, context)); 253 } 254 //@endif 255 256 if (!handled && !working && this.tryToPerform) { 257 handled = this.tryToPerform(action, sender, context); 258 } 259 260 while (!handled && working) { 261 if (working.tryToPerform) { 262 handled = working.tryToPerform(action, sender, context); 263 } 264 265 if (!handled) { 266 working = (working===last) ? null : this.nextResponderFor(working); 267 } 268 } 269 270 //@if(debug) 271 if (trace) { 272 if (!handled) SC.Logger.log("%@: action '%@' NOT HANDLED".fmt(this,action)); 273 else SC.Logger.log("%@: action '%@' handled by %@".fmt(this, action, this.responderNameFor(working))); 274 } 275 //@endif 276 277 this._locked = NO ; 278 279 if (responder = this._pendingResponder) { 280 this._pendingResponder= null ; 281 this.makeFirstResponder(responder); 282 } 283 284 285 return working ; 286 } 287 288 }; 289