API  0.9.8
 All Classes Files Functions Variables Typedefs Macros Groups Pages
CPTheme.j
Go to the documentation of this file.
1 /*
2  * CPTheme.j
3  * AppKit
4  *
5  * Created by Francisco Tolmasky.
6  * Copyright 2009, 280 North, Inc.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public
10  * License as published by the Free Software Foundation; either
11  * version 2.1 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21  */
22 
23 
24 
25 var CPThemesByName = { },
28 
29 
34 @implementation CPTheme : CPObject
35 {
36  CPString _name;
37  CPDictionary _attributes;
38 }
39 
40 + (void)setDefaultTheme:(CPTheme)aTheme
41 {
42  CPThemeDefaultTheme = aTheme;
43 }
44 
45 + (CPTheme)defaultTheme
46 {
47  return CPThemeDefaultTheme;
48 }
49 
54 + (void)setDefaultHudTheme:(CPTheme)aTheme
55 {
56  CPThemeDefaultHudTheme = aTheme;
57 }
58 
64 + (CPTheme)defaultHudTheme
65 {
69 }
70 
71 + (CPTheme)themeNamed:(CPString)aName
72 {
73  return CPThemesByName[aName];
74 }
75 
76 - (id)initWithName:(CPString)aName
77 {
78  self = [super init];
79 
80  if (self)
81  {
82  _name = aName;
83  _attributes = @{};
84 
85  CPThemesByName[_name] = self;
86  }
87 
88  return self;
89 }
90 
92 {
93  return _name;
94 }
95 
104 - (CPArray)classNames
105 {
106  return [_attributes allKeys];
107 }
108 
119 - (CPDictionary)attributesForClass:(id)aClass
120 {
121  if (!aClass)
122  return nil;
123 
124  var className = nil;
125 
126  if ([aClass isKindOfClass:[CPString class]])
127  {
128  // See if it is a class name
129  var theClass = CPClassFromString(aClass);
130 
131  if (theClass)
132  aClass = theClass;
133  else
134  className = aClass;
135  }
136 
137  if (!className)
138  {
139  if ([aClass isKindOfClass:[CPView class]])
140  {
141  if ([aClass respondsToSelector:@selector(defaultThemeClass)])
142  className = [aClass defaultThemeClass];
143  else if ([aClass respondsToSelector:@selector(themeClass)])
144  {
145  CPLog.warn(@"%@ themeClass is deprecated in favor of defaultThemeClass", CPStringFromClass(aClass));
146  className = [aClass themeClass];
147  }
148  else
149  return nil;
150  }
151  else
152  [CPException raise:CPInvalidArgumentException reason:@"aClass must be a class object or a string."];
153  }
154 
155  return [_attributes objectForKey:className];
156 }
157 
174 - (CPDictionary)attributeNamesForClass:(id)aClass
175 {
176  var attributes = [self attributesForClass:aClass];
177 
178  if (attributes)
179  return [attributes allKeys];
180  else
181  return [CPArray array];
182 }
183 
197 - (_CPThemeAttribute)attributeWithName:(CPString)aName forClass:(id)aClass
198 {
199  var attributes = [self attributesForClass:aClass];
200 
201  if (!attributes)
202  return nil;
203 
204  return [attributes objectForKey:aName];
205 }
206 
220 - (id)valueForAttributeWithName:(CPString)aName forClass:(id)aClass
221 {
222  return [self valueForAttributeWithName:aName inState:CPThemeStateNormal forClass:aClass];
223 }
224 
238 - (id)valueForAttributeWithName:(CPString)aName inState:(ThemeState)aState forClass:(id)aClass
239 {
240  var attribute = [self attributeWithName:aName forClass:aClass];
241 
242  if (!attribute)
243  return nil;
244 
245  return [attribute valueForState:aState];
246 }
247 
248 - (void)takeThemeFromObject:(id)anObject
249 {
250  var attributes = [anObject _themeAttributeDictionary],
251  attributeName = nil,
252  attributeNames = [attributes keyEnumerator],
253  objectThemeClass = [anObject themeClass];
254 
255  while ((attributeName = [attributeNames nextObject]) !== nil)
256  [self _recordAttribute:[attributes objectForKey:attributeName] forClass:objectThemeClass];
257 }
258 
259 - (void)_recordAttribute:(_CPThemeAttribute)anAttribute forClass:(CPString)aClass
260 {
261  if (![anAttribute hasValues])
262  return;
263 
264  var attributes = [_attributes objectForKey:aClass];
265 
266  if (!attributes)
267  {
268  attributes = @{};
269 
270  [_attributes setObject:attributes forKey:aClass];
271  }
272 
273  var name = [anAttribute name],
274  existingAttribute = [attributes objectForKey:name];
275 
276  if (existingAttribute)
277  [attributes setObject:[existingAttribute attributeMergedWithAttribute:anAttribute] forKey:name];
278  else
279  [attributes setObject:anAttribute forKey:name];
280 }
281 
282 @end
283 
284 var CPThemeNameKey = @"CPThemeNameKey",
285  CPThemeAttributesKey = @"CPThemeAttributesKey";
286 
287 @implementation CPTheme (CPCoding)
288 
289 - (id)initWithCoder:(CPCoder)aCoder
290 {
291  self = [super init];
292 
293  if (self)
294  {
295  _name = [aCoder decodeObjectForKey:CPThemeNameKey];
296  _attributes = [aCoder decodeObjectForKey:CPThemeAttributesKey];
297 
298  CPThemesByName[_name] = self;
299  }
300 
301  return self;
302 }
303 
304 - (void)encodeWithCoder:(CPCoder)aCoder
305 {
306  [aCoder encodeObject:_name forKey:CPThemeNameKey];
307  [aCoder encodeObject:_attributes forKey:CPThemeAttributesKey];
308 }
309 
310 @end
311 
316 function ThemeState(stateNames)
317 {
318  var stateNameKeys = [];
319  this._stateNames = {};
320 
321  for (key in stateNames)
322  {
323  if (!stateNames.hasOwnProperty(key))
324  continue;
325  if (key !== 'normal')
326  {
327  this._stateNames[key] = true;
328  stateNameKeys.push(key);
329  }
330  }
331 
332  if (stateNameKeys.length === 0)
333  {
334  stateNameKeys.push('normal');
335  this._stateNames['normal'] = true;
336  }
337 
338  stateNameKeys.sort();
339  this._stateNameString = stateNameKeys[0];
340 
341  var stateNameLength = stateNameKeys.length;
342  for (var stateIndex = 1; stateIndex < stateNameLength; stateIndex++)
343  this._stateNameString = this._stateNameString + "+" + stateNameKeys[stateIndex];
344  this._stateNameCount = stateNameLength;
345 }
346 
347 ThemeState.prototype.toString = function()
348 {
349  return this._stateNameString;
350 }
351 
352 ThemeState.prototype.hasThemeState = function(aState)
353 {
354  if (!aState || !aState._stateNames)
355  return false;
356 
357  // We can do this in O(n) because both states have their stateNames already sorted.
358  for (var stateName in aState._stateNames)
359  {
360  if (!aState._stateNames.hasOwnProperty(stateName))
361  continue;
362 
363  if (!this._stateNames[stateName])
364  return false;
365  }
366  return true;
367 }
368 
369 ThemeState.prototype.isSubsetOf = function(aState)
370 {
371  if (aState._stateNameCount < this._stateNameCount)
372  return false;
373 
374  for (var key in this._stateNames)
375  {
376  if (!this._stateNames.hasOwnProperty(key))
377  continue;
378 
379  if (!aState._stateNames[key])
380  return false;
381  }
382  return true;
383 }
384 
385 ThemeState.prototype.without = function(aState)
386 {
387  if (!aState || aState === [CPNull null])
388  return this;
389 
390  var newStates = {};
391  for (var stateName in this._stateNames)
392  {
393  if (!this._stateNames.hasOwnProperty(stateName))
394  continue;
395 
396  if (!aState._stateNames[stateName])
397  newStates[stateName] = true;
398  }
399 
400  return ThemeState._cacheThemeState(new ThemeState(newStates));
401 }
402 
403 ThemeState.prototype.and = function(aState)
404 {
405  return CPThemeState(this, aState);
406 }
407 
408 var CPThemeStates = {};
409 
410 ThemeState._cacheThemeState = function(aState)
411 {
412  // We do this caching so themeState equality works. Basically, doing CPThemeState('foo+bar') === CPThemeState('bar', 'foo') will return true.
413  var themeState = CPThemeStates[String(aState)];
414  if (themeState === undefined)
415  {
416  themeState = aState;
417  CPThemeStates[String(themeState)] = themeState;
418  }
419  return themeState;
420 }
421 
430 function CPThemeState()
431 {
432  if (arguments.length < 1)
433  throw "CPThemeState() must be called with at least one string argument";
434 
435  var themeState;
436  if (arguments.length === 1 && typeof arguments[0] === 'string')
437  {
438  themeState = CPThemeStates[arguments[0]];
439  if (themeState !== undefined)
440  return themeState;
441  }
442 
443  var stateNames = {};
444  for (var argIndex = 0; argIndex < arguments.length; argIndex++)
445  {
446  if (arguments[argIndex] === [CPNull null] || !arguments[argIndex])
447  continue;
448 
449  if (typeof arguments[argIndex] === 'object')
450  {
451  for (var stateName in arguments[argIndex]._stateNames)
452  {
453  if (!arguments[argIndex]._stateNames.hasOwnProperty(stateName))
454  continue;
455  stateNames[stateName] = true;
456  }
457  }
458  else
459  {
460  var allNames = arguments[argIndex].split('+');
461  for (var nameIndex = 0; nameIndex < allNames.length; nameIndex++)
462  stateNames[allNames[nameIndex]] = true;
463  }
464  }
465 
466  themeState = ThemeState._cacheThemeState(new ThemeState(stateNames));
467  return themeState;
468 }
469 
470 @implementation _CPThemeKeyedUnarchiver : CPKeyedUnarchiver
471 {
472  CPBundle _bundle;
473 }
474 
475 - (id)initForReadingWithData:(CPData)data bundle:(CPBundle)aBundle
476 {
477  self = [super initForReadingWithData:data];
478 
479  if (self)
480  _bundle = aBundle;
481 
482  return self;
483 }
484 
485 - (CPBundle)bundle
486 {
487  return _bundle;
488 }
489 
490 - (BOOL)awakenCustomResources
491 {
492  return YES;
493 }
494 
495 @end
496 
497 CPThemeStateNormal = CPThemeState("normal");
498 CPThemeStateDisabled = CPThemeState("disabled");
499 CPThemeStateHovered = CPThemeState("hovered");
500 CPThemeStateHighlighted = CPThemeState("highlighted");
501 CPThemeStateSelected = CPThemeState("selected");
502 CPThemeStateTableDataView = CPThemeState("tableDataView");
503 CPThemeStateSelectedDataView = CPThemeState("selectedTableDataView");
504 CPThemeStateGroupRow = CPThemeState("CPThemeStateGroupRow");
505 CPThemeStateBezeled = CPThemeState("bezeled");
506 CPThemeStateBordered = CPThemeState("bordered");
507 CPThemeStateEditable = CPThemeState("editable");
508 CPThemeStateEditing = CPThemeState("editing");
509 CPThemeStateVertical = CPThemeState("vertical");
510 CPThemeStateDefault = CPThemeState("default");
511 CPThemeStateCircular = CPThemeState("circular");
512 CPThemeStateAutocompleting = CPThemeState("autocompleting");
513 CPThemeStateFirstResponder = CPThemeState("firstResponder");
514 CPThemeStateMainWindow = CPThemeState("mainWindow");
515 CPThemeStateKeyWindow = CPThemeState("keyWindow");
516 CPThemeStateControlSizeRegular = CPThemeState("controlSizeRegular");
517 CPThemeStateControlSizeSmall = CPThemeState("controlSizeSmall");
518 CPThemeStateControlSizeMini = CPThemeState("controlSizeMini");
519 
520 @implementation _CPThemeAttribute : CPObject
521 {
522  CPString _name;
523  id _defaultValue;
524  CPDictionary _values;
525 
526  JSObject _cache;
527  _CPThemeAttribute _themeDefaultAttribute;
528 }
529 
530 - (id)initWithName:(CPString)aName defaultValue:(id)aDefaultValue
531 {
532  self = [super init];
533 
534  if (self)
535  {
536  _cache = { };
537  _name = aName;
538  _defaultValue = aDefaultValue;
539  _values = @{};
540  }
541 
542  return self;
543 }
544 
545 - (CPString)name
546 {
547  return _name;
548 }
549 
550 - (id)defaultValue
551 {
552  return _defaultValue;
553 }
554 
555 - (BOOL)hasValues
556 {
557  return [_values count] > 0;
558 }
559 
560 - (void)setValue:(id)aValue
561 {
562  _cache = {};
563 
564  if (aValue === undefined || aValue === nil)
565  _values = @{};
566  else
567  _values = @{ String(CPThemeStateNormal): aValue };
568 }
569 
570 - (void)setValue:(id)aValue forState:(ThemeState)aState
571 {
572  _cache = { };
573 
574  if ((aValue === undefined) || (aValue === nil))
575  [_values removeObjectForKey:String(aState)];
576  else
577  [_values setObject:aValue forKey:String(aState)];
578 }
579 
580 - (id)value
581 {
582  return [self valueForState:CPThemeStateNormal];
583 }
584 
585 - (id)valueForState:(ThemeState)aState
586 {
587  var stateName = String(aState),
588  value = _cache[stateName];
589 
590  // This can be nil.
591  if (value !== undefined)
592  return value;
593 
594  value = [_values objectForKey:stateName];
595 
596  if (value === undefined || value === nil)
597  {
598  // If this is a composite state, find the closest partial subset match.
599  if (aState._stateNameCount > 1)
600  {
601  var states = [_values allKeys],
602  count = states.length,
603  largestThemeState = 0;
604 
605  while (count--)
606  {
607  var stateObject = CPThemeState(states[count]);
608 
609  if (stateObject.isSubsetOf(aState) && stateObject._stateNameCount > largestThemeState)
610  {
611  value = [_values objectForKey:states[count]];
612  largestThemeState = stateObject._stateNameCount;
613  }
614  }
615  }
616 
617  // Still don't have a value? OK, let's use the normal value.
618  if (value === undefined || value === nil)
619  value = [_values objectForKey:String(CPThemeStateNormal)];
620  }
621 
622  if (value === undefined || value === nil)
623  value = [_themeDefaultAttribute valueForState:aState];
624 
625  if (value === undefined || value === nil)
626  {
627  value = _defaultValue;
628 
629  // Class theme attributes cannot use nil because it's a dictionary.
630  // So transform CPNull into nil.
631  if (value === [CPNull null])
632  value = nil;
633  }
634 
635  _cache[stateName] = value;
636 
637  return value;
638 }
639 
640 - (void)setParentAttribute:(_CPThemeAttribute)anAttribute
641 {
642  if (_themeDefaultAttribute === anAttribute)
643  return;
644 
645  _cache = { };
646  _themeDefaultAttribute = anAttribute;
647 }
648 
649 - (_CPThemeAttribute)attributeMergedWithAttribute:(_CPThemeAttribute)anAttribute
650 {
651  var mergedAttribute = [[_CPThemeAttribute alloc] initWithName:_name defaultValue:_defaultValue];
652 
653  mergedAttribute._values = [_values copy];
654  [mergedAttribute._values addEntriesFromDictionary:anAttribute._values];
655 
656  return mergedAttribute;
657 }
658 
659 @end
660 
661 @implementation _CPThemeAttribute (CPCoding)
662 
663 - (id)initWithCoder:(CPCoder)aCoder
664 {
665  self = [super init];
666 
667  if (self)
668  {
669  _cache = {};
670 
671  _name = [aCoder decodeObjectForKey:@"name"];
672  _defaultValue = [aCoder decodeObjectForKey:@"defaultValue"];
673  _values = @{};
674 
675  if ([aCoder containsValueForKey:@"value"])
676  {
677  var state = String(CPThemeStateNormal);
678 
679  if ([aCoder containsValueForKey:@"state"])
680  state = [aCoder decodeObjectForKey:@"state"];
681 
682  [_values setObject:[aCoder decodeObjectForKey:"value"] forKey:state];
683  }
684  else
685  {
686  var encodedValues = [aCoder decodeObjectForKey:@"values"],
687  keys = [encodedValues allKeys],
688  count = keys.length;
689 
690  while (count--)
691  {
692  var key = keys[count];
693 
694  [_values setObject:[encodedValues objectForKey:key] forKey:key];
695  }
696  }
697  }
698 
699  return self;
700 }
701 
702 - (void)encodeWithCoder:(CPCoder)aCoder
703 {
704  [aCoder encodeObject:_name forKey:@"name"];
705  [aCoder encodeObject:_defaultValue forKey:@"defaultValue"];
706 
707  var keys = [_values allKeys],
708  count = keys.length;
709 
710  if (count === 1)
711  {
712  var onlyKey = keys[0];
713 
714  if (onlyKey !== String(CPThemeStateNormal))
715  [aCoder encodeObject:onlyKey forKey:@"state"];
716 
717  [aCoder encodeObject:[_values objectForKey:onlyKey] forKey:@"value"];
718  }
719  else
720  {
721  var encodedValues = @{};
722 
723  while (count--)
724  {
725  var key = keys[count];
726 
727  [encodedValues setObject:[_values objectForKey:key] forKey:key];
728  }
729 
730  [aCoder encodeObject:encodedValues forKey:@"values"];
731  }
732 }
733 
734 @end
735 
736 function CPThemeAttributeEncode(aCoder, aThemeAttribute)
737 {
738  var values = aThemeAttribute._values,
739  count = [values count],
740  key = "$a" + [aThemeAttribute name];
741 
742  if (count === 1)
743  {
744  var state = [values allKeys][0];
745 
746  if (state === String(CPThemeStateNormal))
747  {
748  [aCoder encodeObject:[values objectForKey:state] forKey:key];
749 
750  return YES;
751  }
752  }
753 
754  if (count >= 1)
755  {
756  [aCoder encodeObject:aThemeAttribute forKey:key];
757 
758  return YES;
759  }
760 
761  return NO;
762 }
763 
764 function CPThemeAttributeDecode(aCoder, anAttributeName, aDefaultValue, aTheme, aClass)
765 {
766  var key = "$a" + anAttributeName;
767 
768  if (![aCoder containsValueForKey:key])
769  var attribute = [[_CPThemeAttribute alloc] initWithName:anAttributeName defaultValue:aDefaultValue];
770 
771  else
772  {
773  var attribute = [aCoder decodeObjectForKey:key];
774 
775  if (!attribute || !attribute.isa || ![attribute isKindOfClass:[_CPThemeAttribute class]])
776  {
777  var themeAttribute = [[_CPThemeAttribute alloc] initWithName:anAttributeName defaultValue:aDefaultValue];
778 
779  [themeAttribute setValue:attribute];
780 
781  attribute = themeAttribute;
782  }
783  }
784 
785  if (aTheme && aClass)
786  [attribute setParentAttribute:[aTheme attributeWithName:anAttributeName forClass:aClass]];
787 
788  return attribute;
789 }
790 
791 /* TO AUTO CREATE THESE:
792 function bit_count(bits)
793  {
794  var count = 0;
795 
796  while (bits)
797  {
798  ++count;
799  bits &= (bits - 1);
800  }
801 
802  return count ;
803  }
804 
805 zeros = "000000000";
806 
807 function pad(string, digits)
808 {
809  return zeros.substr(0, digits - string.length) + string;
810 }
811 
812 var str = ""
813 str += '[';
814 for (i = 0;i < Math.pow(2,6);++i)
815 {
816  str += bit_count(i) + " /*" + pad(i.toString(2),6) + "*" + "/, ";
817 }
818 print(str+']');
819 
820 */