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