1 sc_require("views/view");
  2 
  3 SC.View.reopen(
  4   /** @scope SC.View.prototype */ {
  5 
  6   // ..........................................................
  7   // THEME SUPPORT
  8   //
  9 
 10   /**
 11     Names which theme this view should use; the theme named by this property
 12     will be set to the view's 'theme' property.
 13 
 14     Themes are identified by their name. In addition to looking for the
 15     theme globally, SproutCore will look for the theme inside 'baseTheme',
 16     which is almost always the parent view's theme.
 17 
 18     If null (the default), the view will set its 'theme' property to
 19     be equal to 'baseTheme'.
 20 
 21     Example: themeName: 'ace'
 22 
 23     @type String
 24   */
 25   themeName: null,
 26 
 27   /**
 28     Selects which theme to use as a 'base theme'. If null, the 'baseTheme'
 29     property will be set to the parent's theme. If there is no parent, the theme
 30     named by SC.defaultTheme is used.
 31 
 32     This property is private for the time being.
 33 
 34     @private
 35     @type String
 36   */
 37   baseThemeName: null,
 38 
 39   /**
 40     The SC.Theme instance which this view should use to render.
 41 
 42     Note: the actual code for this function is in _themeProperty for backwards-compatibility:
 43     some older views specify a string value for 'theme', which would override this property,
 44     breaking it.
 45 
 46     @property {SC.Theme}
 47   */
 48   theme: function() {
 49     var base = this.get('baseTheme'), themeName = this.get('themeName');
 50 
 51     // find theme, if possible
 52     if (themeName) {
 53       // Note: theme instance "find" function will search every parent
 54       // _except_ global (which is not a parent)
 55       var theme;
 56       if (base) {
 57         theme = base.find(themeName);
 58         if (theme) { return theme; }
 59       }
 60 
 61       theme = SC.Theme.find(themeName);
 62       if (theme) { return theme; }
 63 
 64       // Create a new invisible subtheme. This will cause the themeName to
 65       // be applied as a class name.
 66       return base.invisibleSubtheme(themeName);
 67     }
 68 
 69     // can't find anything, return base.
 70     return base;
 71   }.property('baseTheme', 'themeName').cacheable(),
 72 
 73   /**
 74     Detects when the theme changes. Replaces the layer if necessary.
 75 
 76     Also, because
 77   */
 78   _sc_view_themeDidChange: function() {
 79     var curTheme = this.get('theme');
 80     if (this._lastTheme === curTheme) { return; }
 81     this._lastTheme = curTheme;
 82 
 83     // invalidate child view base themes, if present
 84     var childViews = this.childViews, len = childViews.length, idx;
 85     for (idx = 0; idx < len; idx++) {
 86       childViews[idx].notifyPropertyChange('baseTheme');
 87     }
 88 
 89     if (this.get('layer')) { this.replaceLayer(); }
 90   }.observes('theme'),
 91 
 92   /**
 93     The SC.Theme instance in which the 'theme' property should look for the theme
 94     named by 'themeName'.
 95 
 96     For example, if 'baseTheme' is SC.AceTheme, and 'themeName' is 'popover',
 97     it will look to see if SC.AceTheme has a child theme named 'popover',
 98     and _then_, if it is not found, look globally.
 99 
100     @private
101     @property {SC.Theme}
102   */
103   baseTheme: function() {
104     var parent;
105     var baseThemeName = this.get('baseThemeName');
106     if (baseThemeName) {
107       return SC.Theme.find(baseThemeName);
108     } else {
109       parent = this.get('parentView');
110       var theme  = parent && parent.get('theme');
111       return   theme || SC.Theme.find(SC.defaultTheme);
112     }
113   }.property('baseThemeName', 'parentView').cacheable(),
114 
115   /**
116     The object to which rendering and updating the HTML representation of this
117     view should be delegated.
118 
119     By default, views are responsible for creating their own HTML
120     representation. In some cases, however, you may want to create an object
121     that is responsible for rendering all views of a certain type. For example,
122     you may want rendering of SC.ButtonView to be controlled by an object that
123     is specific to the current theme.
124 
125     By setting a render delegate, the render and update methods will be called
126     on that object instead of the view itself.
127 
128     For your convenience, the view will provide its displayProperties to the
129     RenderDelegate. In some cases, you may have a conflict between the RenderDelegate's
130     API and your view's. For instance, you may have a 'value' property that is
131     any number, but the render delegate expects a percentage. Make a 'displayValue'
132     property, add _it_ to displayProperties instead of 'value', and the Render Delegate
133     will automatically use that when it wants to find 'value.'
134 
135     You can also set the render delegate by using the 'renderDelegateName' property.
136 
137     @type Object
138   */
139   renderDelegate: function(key, value) {
140     if (value) { this._setRenderDelegate = value; }
141     if (this._setRenderDelegate) { return this._setRenderDelegate; }
142 
143     // If this view does not have a render delegate but has
144     // renderDelegateName set, try to retrieve the render delegate from the
145     // theme.
146     var renderDelegateName = this.get('renderDelegateName'), renderDelegate;
147 
148     if (renderDelegateName) {
149       renderDelegate = this.get('theme')[renderDelegateName];
150       if (!renderDelegate) {
151         throw new Error("%@: Unable to locate render delegate \"%@\" in theme.".fmt(this, renderDelegateName));
152       }
153 
154       return renderDelegate;
155     }
156 
157     return null;
158   }.property('renderDelegateName', 'theme'),
159 
160   /**
161     The name of the property of the current theme that contains the render
162     delegate to use for this view.
163 
164     By default, views are responsible for creating their own HTML
165     representation. You can tell the view to instead delegate rendering to the
166     theme by setting this property to the name of the corresponding property
167     of the theme.
168 
169     For example, to tell the view that it should render using the
170     SC.ButtonView render delegate, set this property to
171     'buttonRenderDelegate'. When the view is created, it will retrieve the
172     buttonRenderDelegate property from its theme and set the renderDelegate
173     property to that object.
174   */
175   renderDelegateName: null,
176 
177   /**
178     [RO] Pass this object as the data source for render delegates. This proxy object
179     for the view relays requests for properties like 'title' to 'displayTitle'
180     as necessary.
181 
182     If you ever communicate with your view's render delegate, you should pass this
183     object as the data source.
184 
185     The proxy that forwards RenderDelegate requests for properties to the view,
186     handling display*, keeps track of the delegate's state, etc.
187   */
188   renderDelegateProxy: function() {
189     return SC.View._RenderDelegateProxy.createForView(this);
190   }.property('renderDelegate').cacheable(),
191 
192   /**
193     Invoked whenever your view needs to create its HTML representation.
194 
195     You will normally override this method in your subclassed views to
196     provide whatever drawing functionality you will need in order to
197     render your content.
198 
199     This method is usually only called once per view. After that, the update
200     method will be called to allow you to update the existing HTML
201     representation.
202 
203 
204     The default implementation of this method calls renderChildViews().
205 
206     For backwards compatibility, this method will also call the appropriate
207     method on a render delegate object, if your view has one.
208 
209     @param {SC.RenderContext} context the render context
210     @returns {void}
211   */
212   render: function(context, firstTime) {
213     var renderDelegate = this.get('renderDelegate');
214 
215     if (renderDelegate) {
216       if (firstTime) {
217         renderDelegate.render(this.get('renderDelegateProxy'), context);
218       } else {
219         renderDelegate.update(this.get('renderDelegateProxy'), context.$());
220       }
221     }
222   },
223 
224   /**
225     Invokes a method on the render delegate, if one is present and it implements
226     that method.
227 
228     @param {String} method The name of the method to call.
229     @param arg One or more arguments.
230   */
231   invokeRenderDelegateMethod: function(method, args) {
232     var renderDelegate = this.get('renderDelegate');
233     if (!renderDelegate) return undefined;
234 
235     if (SC.typeOf(renderDelegate[method]) !== SC.T_FUNCTION) return undefined;
236 
237     args = SC.$A(arguments);
238     args.shift();
239     args.unshift(this.get('renderDelegateProxy'));
240     return renderDelegate[method].apply(renderDelegate, args);
241   }
242 });
243 
244 /**
245   @class
246   @private
247   View Render Delegate Proxies are tool SC.Views use to:
248 
249   - look up 'display*' ('displayTitle' instead of 'title') to help deal with
250     differences between the render delegate's API and the view's.
251 
252   RenderDelegateProxies are fully valid data sources for render delegates. They
253   act as proxies to the view, interpreting the .get and .didChangeFor commands
254   based on the view's displayProperties.
255 
256   This tool is not useful outside of SC.View itself, and as such, is private.
257 */
258 SC.View._RenderDelegateProxy = {
259 
260   //@if(debug)
261   // for testing:
262   isViewRenderDelegateProxy: YES,
263   //@endif
264 
265   /**
266     Creates a View Render Delegate Proxy for the specified view.
267 
268     Implementation note: this creates a hash of the view's displayProperties
269     array so that the proxy may quickly determine whether a property is a
270     displayProperty or not. This could cause issues if the view's displayProperties
271     array is modified after instantiation.
272 
273     @param {SC.View} view The view this proxy should proxy to.
274     @returns SC.View._RenderDelegateProxy
275   */
276   createForView: function(view) {
277     var ret = SC.beget(this);
278 
279     // set up displayProperty lookup for performance
280     var dp = view.get('displayProperties'), lookup = {};
281     for (var idx = 0, len = dp.length; idx < len; idx++) {
282       lookup[dp[idx]] = YES;
283     }
284 
285     // also allow the few special properties through
286     lookup.theme = YES;
287 
288     ret._displayPropertiesLookup = lookup;
289     ret.renderState = {};
290 
291     ret._view = view;
292     return ret;
293   },
294 
295 
296   /**
297     Provides the render delegate with any property it needs.
298 
299     This first looks up whether the property exists in the view's
300     displayProperties, and whether it exists prefixed with 'display';
301     for instance, if the render delegate asks for 'title', this will
302     look for 'displayTitle' in the view's displayProperties array.
303 
304    @param {String} property The name of the property the render delegate needs.
305    @returns The value.
306   */
307   get: function(property) {
308     if (this[property] !== undefined) { return this[property]; }
309 
310     var displayProperty = 'display' + property.capitalize();
311 
312     if (this._displayPropertiesLookup[displayProperty]) {
313       return this._view.get(displayProperty);
314     } else {
315       return this._view.get(property);
316     }
317   },
318 
319   /**
320    Checks if any of the specified properties have changed.
321 
322    For each property passed, this first determines whether to use the
323    'display' prefix. Then, it calls view.didChangeFor with context and that
324    property name.
325   */
326   didChangeFor: function(context) {
327     var len = arguments.length, idx;
328     for (idx = 1; idx < len; idx++) {
329       var property = arguments[idx],
330           displayProperty = 'display' + property.capitalize();
331 
332       if (this._displayPropertiesLookup[displayProperty]) {
333         if (this._view.didChangeFor(context, displayProperty)) { return YES; }
334       } else {
335         if (this._view.didChangeFor(context, property)) { return YES; }
336       }
337     }
338 
339     return NO;
340   }
341 };
342 
343 /**
344   Generates a computed property that will look up the specified property from
345   the view's render delegate, if present. You may specify a default value to
346   return if there is no such property or is no render delegate.
347 
348   The generated property is read+write, so it may be overridden.
349 
350   @param {String} propertyName The name of the property to get from the render delegate..
351   @param {Value} def The default value to use if the property is not present.
352 */
353 SC.propertyFromRenderDelegate = function(propertyName, def) {
354   return function(key, value) {
355     // first, handle set() case
356     if (value !== undefined) {
357       this['_set_rd_' + key] = value;
358     }
359 
360     // use any value set manually via set()  -- two lines ago.
361     var ret = this['_set_rd_' + key];
362     if (ret !== undefined) return ret;
363 
364     // finally, try to get it from the render delegate
365     var renderDelegate = this.get('renderDelegate');
366     if (renderDelegate && renderDelegate.get) {
367       var proxy = this.get('renderDelegateProxy');
368       ret = renderDelegate.getPropertyFor(proxy, propertyName);
369     }
370 
371     if (ret !== undefined) return ret;
372 
373     return def;
374   }.property('renderDelegate').cacheable();
375 };
376 
377 
378