/AppKit/CPAccordionView.j

http://github.com/cacaodev/cappuccino · Unknown · 416 lines · 319 code · 97 blank · 0 comment · 0 complexity · 0dbe7184be51d639873b21ccce9e8107 MD5 · raw file

  1. /*
  2. * CPAccordionView.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. @import <Foundation/CPArray.j>
  23. @import <Foundation/CPObject.j>
  24. @import <Foundation/CPKeyValueObserving.j>
  25. @import <Foundation/CPIndexSet.j>
  26. @import <Foundation/CPString.j>
  27. @import "CPView.j"
  28. @import "CPButton.j"
  29. /*!
  30. @ingroup appkit
  31. @class CPAccordionViewItem
  32. <p>A CPAccordionViewItem represents a single section of a CPAccordionView.</p>
  33. */
  34. @implementation CPAccordionViewItem : CPObject
  35. {
  36. CPString _identifier @accessors(property=identifier);
  37. CPView _view @accessors(property=view);
  38. CPString _label @accessors(property=label);
  39. }
  40. - (id)init
  41. {
  42. return [self initWithIdentifier:@""];
  43. }
  44. /*!
  45. Initializes a new CPAccordionViewItem with the specified identifier. This identifier may
  46. then be used in subsequent methods to provide abstract introspection of the CPAccordionView.
  47. */
  48. - (id)initWithIdentifier:(CPString)anIdentifier
  49. {
  50. self = [super init];
  51. if (self)
  52. [self setIdentifier:anIdentifier];
  53. return self;
  54. }
  55. @end
  56. /*!
  57. @ingroup appkit
  58. @class CPAccordionView
  59. <p>CPAccordionView provides a container for CPAccordionViewItem objects and manages layout state
  60. for all sub-layout items.</p>
  61. <strong>Example</strong><br />
  62. <pre>
  63. var myAccordionView = [[CPAccordionView alloc] initWithFrame:CGRectMakeZero()];
  64. var firstItem = [[CPAccordionViewItem alloc] initWithIdentifier:@"firstSection"]];
  65. [firstItem setView:[[CPView alloc] initWithFrame:CGRectMakeZero()]];
  66. var secondItem = [[CPAccordionViewItem alloc] initWithIdentifier:@"secondSection"]];
  67. [secondItem setView:[[CPView alloc] initWithFrame:CGRectMakeZero()]];
  68. [myAccordionView addItem:firstItem];
  69. [myAccordionView addItem:secondItem];
  70. [myAccordionView setAutoresizingMask: CPViewWidthSizable | CPViewHeightSizable];
  71. </pre>
  72. */
  73. @implementation CPAccordionView : CPView
  74. {
  75. CPInteger _dirtyItemIndex;
  76. CPView _itemHeaderPrototype;
  77. CPMutableArray _items;
  78. CPMutableArray _itemViews;
  79. CPIndexSet _expandedItemIndexes;
  80. }
  81. /*!
  82. Initializes the receiver for usage with the specified bounding rectangle
  83. @return the initialized view
  84. */
  85. - (id)initWithFrame:(CGRect)aFrame
  86. {
  87. self = [super initWithFrame:aFrame];
  88. if (self)
  89. {
  90. _items = [];
  91. _itemViews = [];
  92. _expandedItemIndexes = [CPIndexSet indexSet];
  93. [self setItemHeaderPrototype:[[CPButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 24.0)]];
  94. }
  95. return self;
  96. }
  97. - (void)setItemHeaderPrototype:(CPView)aView
  98. {
  99. _itemHeaderPrototype = aView;
  100. }
  101. - (CPView)itemHeaderPrototype
  102. {
  103. return _itemHeaderPrototype;
  104. }
  105. - (CPArray)items
  106. {
  107. return _items;
  108. }
  109. /*!
  110. Append a CPAccordionViewItem to the receiver. Note that the CPAccordionViewItem must contain
  111. a valid CPView derived component or a TypeError will be generated when the contents of the
  112. ViewItem are disclosed.
  113. */
  114. - (void)addItem:(CPAccordionViewItem)anItem
  115. {
  116. [self insertItem:anItem atIndex:_items.length];
  117. }
  118. - (void)insertItem:(CPAccordionViewItem)anItem atIndex:(CPInteger)anIndex
  119. {
  120. // FIXME: SHIFT ITEMS RIGHT
  121. [_expandedItemIndexes addIndex:anIndex];
  122. var itemView = [[_CPAccordionItemView alloc] initWithAccordionView:self];
  123. [itemView setIndex:anIndex];
  124. [itemView setLabel:[anItem label]];
  125. [itemView setContentView:[anItem view]];
  126. [self addSubview:itemView];
  127. [_items insertObject:anItem atIndex:anIndex];
  128. [_itemViews insertObject:itemView atIndex:anIndex];
  129. [self _invalidateItemsStartingAtIndex:anIndex];
  130. [self setNeedsLayout];
  131. }
  132. - (void)removeItem:(CPAccordionViewItem)anItem
  133. {
  134. [self removeItemAtIndex:[_items indexOfObjectIdenticalTo:anItem]];
  135. }
  136. - (void)removeItemAtIndex:(CPInteger)anIndex
  137. {
  138. // SHIFT ITEMS LEFT
  139. [_expandedItemIndexes removeIndex:anIndex];
  140. [_itemViews[anIndex] removeFromSuperview];
  141. [_items removeObjectAtIndex:anIndex];
  142. [_itemViews removeObjectAtIndex:anIndex];
  143. [self _invalidateItemsStartingAtIndex:anIndex];
  144. [self setNeedsLayout];
  145. }
  146. - (void)removeAllItems
  147. {
  148. var count = _items.length;
  149. while (count--)
  150. [self removeItemAtIndex:count];
  151. }
  152. - (void)expandItemAtIndex:(CPInteger)anIndex
  153. {
  154. if (![_itemViews[anIndex] isCollapsed])
  155. return;
  156. [_expandedItemIndexes addIndex:anIndex];
  157. [_itemViews[anIndex] setCollapsed:NO];
  158. [self _invalidateItemsStartingAtIndex:anIndex];
  159. }
  160. - (void)collapseItemAtIndex:(CPInteger)anIndex
  161. {
  162. if ([_itemViews[anIndex] isCollapsed])
  163. return;
  164. [_expandedItemIndexes removeIndex:anIndex];
  165. [_itemViews[anIndex] setCollapsed:YES];
  166. [self _invalidateItemsStartingAtIndex:anIndex];
  167. }
  168. - (void)toggleItemAtIndex:(CPInteger)anIndex
  169. {
  170. var itemView = _itemViews[anIndex];
  171. if ([itemView isCollapsed])
  172. [self expandItemAtIndex:anIndex];
  173. else
  174. [self collapseItemAtIndex:anIndex];
  175. }
  176. - (CPIndexSet)expandedItemIndexes
  177. {
  178. return _expandedItemIndexes;
  179. }
  180. - (CPIndexSet)collapsedItemIndexes
  181. {
  182. var indexSet = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, _items.length)];
  183. [indexSet removeIndexes:_expandedItemIndexes];
  184. return indexSet;
  185. }
  186. - (void)setEnabled:(BOOL)isEnabled forItemAtIndex:(CPInteger)anIndex
  187. {
  188. var itemView = _itemViews[anIndex];
  189. if (!itemView)
  190. return;
  191. if (!isEnabled)
  192. [self collapseItemAtIndex:anIndex];
  193. else
  194. [self expandItemAtIndex:anIndex];
  195. [itemView setEnabled:isEnabled];
  196. }
  197. - (void)_invalidateItemsStartingAtIndex:(CPInteger)anIndex
  198. {
  199. if (_dirtyItemIndex === CPNotFound)
  200. _dirtyItemIndex = anIndex;
  201. _dirtyItemIndex = MIN(_dirtyItemIndex, anIndex);
  202. [self setNeedsLayout];
  203. }
  204. - (void)setFrameSize:(CGSize)aSize
  205. {
  206. var width = CGRectGetWidth([self frame]);
  207. [super setFrameSize:aSize];
  208. if (width !== CGRectGetWidth([self frame]))
  209. [self _invalidateItemsStartingAtIndex:0];
  210. }
  211. - (void)layoutSubviews
  212. {
  213. if (_items.length <= 0)
  214. return [self setFrameSize:CGSizeMake(CGRectGetWidth([self frame]), 0.0)];
  215. if (_dirtyItemIndex === CPNotFound)
  216. return;
  217. _dirtyItemIndex = MIN(_dirtyItemIndex, _items.length - 1);
  218. var index = _dirtyItemIndex,
  219. count = _itemViews.length,
  220. width = CGRectGetWidth([self bounds]),
  221. y = index > 0 ? CGRectGetMaxY([_itemViews[index - 1] frame]) : 0.0;
  222. // Do this now (instead of after looping), so that if we are made dirty again in the middle we don't blow this value away.
  223. _dirtyItemIndex = CPNotFound;
  224. for (; index < count; ++index)
  225. {
  226. var itemView = _itemViews[index];
  227. [itemView setFrameY:y width:width];
  228. y = CGRectGetMaxY([itemView frame]);
  229. }
  230. [self setFrameSize:CGSizeMake(CGRectGetWidth([self frame]), y)];
  231. }
  232. @end
  233. @implementation _CPAccordionItemView : CPView
  234. {
  235. CPAccordionView _accordionView;
  236. BOOL _isCollapsed @accessors(getter=isCollapsed, setter=setCollapsed:);
  237. CPInteger _index @accessors(property=index);
  238. CPView _headerView;
  239. CPView _contentView;
  240. }
  241. - (id)initWithAccordionView:(CPAccordionView)anAccordionView
  242. {
  243. self = [super initWithFrame:CGRectMakeZero()];
  244. if (self)
  245. {
  246. _accordionView = anAccordionView;
  247. _isCollapsed = NO;
  248. var bounds = [self bounds];
  249. _headerView = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:[_accordionView itemHeaderPrototype]]];
  250. if ([_headerView respondsToSelector:@selector(setTarget:)] && [_headerView respondsToSelector:@selector(setAction:)])
  251. {
  252. [_headerView setTarget:self];
  253. [_headerView setAction:@selector(toggle:)];
  254. }
  255. [self addSubview:_headerView];
  256. }
  257. return self;
  258. }
  259. - (void)toggle:(id)aSender
  260. {
  261. [_accordionView toggleItemAtIndex:[self index]];
  262. }
  263. - (void)setLabel:(CPString)aLabel
  264. {
  265. if ([_headerView respondsToSelector:@selector(setTitle:)])
  266. [_headerView setTitle:aLabel];
  267. else if ([_headerView respondsToSelector:@selector(setLabel:)])
  268. [_headerView setLabel:aLabel];
  269. else if ([_headerView respondsToSelector:@selector(setStringValue:)])
  270. [_headerView setStringValue:aLabel];
  271. }
  272. - (void)setEnabled:(BOOL)isEnabled
  273. {
  274. if ([_headerView respondsToSelector:@selector(setEnabled:)])
  275. [_headerView setEnabled:isEnabled];
  276. }
  277. - (void)setContentView:(CPView)aView
  278. {
  279. if (_contentView === aView)
  280. return;
  281. [_contentView removeObserver:self forKeyPath:@"frame"];
  282. [_contentView removeFromSuperview];
  283. _contentView = aView;
  284. [_contentView addObserver:self forKeyPath:@"frame" options:CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew context:NULL];
  285. [self addSubview:_contentView];
  286. [_accordionView _invalidateItemsStartingAtIndex:[self index]];
  287. }
  288. - (void)setFrameY:(float)aY width:(float)aWidth
  289. {
  290. var headerHeight = CGRectGetHeight([_headerView frame]);
  291. // Size to fit or something?
  292. [_headerView setFrameSize:CGSizeMake(aWidth, headerHeight)];
  293. [_contentView setFrameOrigin:CGPointMake(0.0, headerHeight)];
  294. if ([self isCollapsed])
  295. [self setFrame:CGRectMake(0.0, aY, aWidth, headerHeight)];
  296. else
  297. {
  298. var contentHeight = CGRectGetHeight([_contentView frame]);
  299. [_contentView setFrameSize:CGSizeMake(aWidth, contentHeight)];
  300. [self setFrame:CGRectMake(0.0, aY, aWidth, contentHeight + headerHeight)];
  301. }
  302. }
  303. - (void)resizeSubviewsWithOldSize:(CGSize)aSize
  304. {
  305. }
  306. - (void)observeValueForKeyPath:(CPString)aKeyPath
  307. ofObject:(id)anObject
  308. change:(CPDictionary)aChange
  309. context:(id)aContext
  310. {
  311. if (aKeyPath === "frame" && !CGRectEqualToRect([aChange objectForKey:CPKeyValueChangeOldKey], [aChange objectForKey:CPKeyValueChangeNewKey]))
  312. [_accordionView _invalidateItemsStartingAtIndex:[self index]];
  313. /*
  314. else if (aKeyPath === "itemHeaderPrototype")
  315. {
  316. }
  317. */
  318. }
  319. @end