API  0.9.6
 All Classes Files Functions Variables Macros Groups Pages
CPAccordionView.j
Go to the documentation of this 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 
23 
24 
25 
32 @implementation CPAccordionViewItem : CPObject
33 {
34  CPString _identifier;
35  CPView _view;
36  CPString _label;
37 }
38 
39 - (id)init
40 {
41  return [self initWithIdentifier:@""];
42 }
43 
48 - (id)initWithIdentifier:(CPString)anIdentifier
49 {
50  self = [super init];
51 
52  if (self)
53  [self setIdentifier:anIdentifier];
54 
55  return self;
56 }
57 
58 @end
59 
79 @implementation CPAccordionView : CPView
80 {
81  CPInteger _dirtyItemIndex;
82  CPView _itemHeaderPrototype;
83 
84  CPMutableArray _items;
85  CPMutableArray _itemViews;
86  CPIndexSet _expandedItemIndexes;
87 }
92 - (id)initWithFrame:(CGRect)aFrame
93 {
94  self = [super initWithFrame:aFrame];
95 
96  if (self)
97  {
98  _items = [];
99  _itemViews = [];
100  _expandedItemIndexes = [CPIndexSet indexSet];
101 
102  [self setItemHeaderPrototype:[[CPButton alloc] initWithFrame:_CGRectMake(0.0, 0.0, 100.0, 24.0)]];
103  }
104 
105  return self;
106 }
107 
108 - (void)setItemHeaderPrototype:(CPView)aView
109 {
110  _itemHeaderPrototype = aView;
111 }
112 
113 - (CPView)itemHeaderPrototype
114 {
115  return _itemHeaderPrototype;
116 }
117 
118 - (CPArray)items
119 {
120  return _items;
121 }
127 - (void)addItem:(CPAccordionViewItem)anItem
128 {
129  [self insertItem:anItem atIndex:_items.length];
130 }
131 
132 - (void)insertItem:(CPAccordionViewItem)anItem atIndex:(CPInteger)anIndex
133 {
134  // FIXME: SHIFT ITEMS RIGHT
135  [_expandedItemIndexes addIndex:anIndex];
136 
137  var itemView = [[_CPAccordionItemView alloc] initWithAccordionView:self];
138 
139  [itemView setIndex:anIndex];
140  [itemView setLabel:[anItem label]];
141  [itemView setContentView:[anItem view]];
142 
143  [self addSubview:itemView];
144 
145  [_items insertObject:anItem atIndex:anIndex];
146  [_itemViews insertObject:itemView atIndex:anIndex];
147 
148  [self _invalidateItemsStartingAtIndex:anIndex];
149 
150  [self setNeedsLayout];
151 }
152 
153 - (void)removeItem:(CPAccordionViewItem)anItem
154 {
155  [self removeItemAtIndex:[_items indexOfObjectIdenticalTo:anItem]];
156 }
157 
158 - (void)removeItemAtIndex:(CPInteger)anIndex
159 {
160  // SHIFT ITEMS LEFT
161  [_expandedItemIndexes removeIndex:anIndex];
162 
163  [_itemViews[anIndex] removeFromSuperview];
164 
165  [_items removeObjectAtIndex:anIndex];
166  [_itemViews removeObjectAtIndex:anIndex];
167 
168  [self _invalidateItemsStartingAtIndex:anIndex];
169 
170  [self setNeedsLayout];
171 }
172 
173 - (void)removeAllItems
174 {
175  var count = _items.length;
176 
177  while (count--)
178  [self removeItemAtIndex:count];
179 }
180 
181 - (void)expandItemAtIndex:(CPInteger)anIndex
182 {
183  if (![_itemViews[anIndex] isCollapsed])
184  return;
185 
186  [_expandedItemIndexes addIndex:anIndex];
187  [_itemViews[anIndex] setCollapsed:NO];
188 
189  [self _invalidateItemsStartingAtIndex:anIndex];
190 }
191 
192 - (void)collapseItemAtIndex:(CPInteger)anIndex
193 {
194  if ([_itemViews[anIndex] isCollapsed])
195  return;
196 
197  [_expandedItemIndexes removeIndex:anIndex];
198  [_itemViews[anIndex] setCollapsed:YES];
199 
200  [self _invalidateItemsStartingAtIndex:anIndex];
201 }
202 
203 - (void)toggleItemAtIndex:(CPInteger)anIndex
204 {
205  var itemView = _itemViews[anIndex];
206 
207  if ([itemView isCollapsed])
208  [self expandItemAtIndex:anIndex];
209 
210  else
211  [self collapseItemAtIndex:anIndex];
212 }
213 
214 - (CPIndexSet)expandedItemIndexes
215 {
216  return _expandedItemIndexes;
217 }
218 
219 - (CPIndexSet)collapsedItemIndexes
220 {
221  var indexSet = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, _items.length)];
222 
223  [indexSet removeIndexes:_expandedIndexes];
224 
225  return indexSet;
226 }
227 
228 - (void)setEnabled:(BOOL)isEnabled forItemAtIndex:(CPInteger)anIndex
229 {
230  var itemView = _itemViews[anIndex];
231  if (!itemView)
232  return;
233 
234  if (!isEnabled)
235  [self collapseItemAtIndex:anIndex];
236  else
237  [self expandItemAtIndex:anIndex];
238 
239  [itemView setEnabled:isEnabled];
240 }
241 
242 - (void)_invalidateItemsStartingAtIndex:(CPInteger)anIndex
243 {
244  if (_dirtyItemIndex === CPNotFound)
245  _dirtyItemIndex = anIndex;
246 
247  _dirtyItemIndex = MIN(_dirtyItemIndex, anIndex);
248 
249  [self setNeedsLayout];
250 }
251 
252 - (void)setFrameSize:(CGSize)aSize
253 {
254  var width = _CGRectGetWidth([self frame]);
255 
256  [super setFrameSize:aSize];
257 
258  if (width !== _CGRectGetWidth([self frame]))
259  [self _invalidateItemsStartingAtIndex:0];
260 }
261 
262 - (void)layoutSubviews
263 {
264  if (_items.length <= 0)
265  return [self setFrameSize:_CGSizeMake(_CGRectGetWidth([self frame]), 0.0)];
266 
267  if (_dirtyItemIndex === CPNotFound)
268  return;
269 
270  _dirtyItemIndex = MIN(_dirtyItemIndex, _items.length - 1);
271 
272  var index = _dirtyItemIndex,
273  count = _itemViews.length,
274  width = _CGRectGetWidth([self bounds]),
275  y = index > 0 ? CGRectGetMaxY([_itemViews[index - 1] frame]) : 0.0;
276 
277  // 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.
278  _dirtyItemIndex = CPNotFound;
279 
280  for (; index < count; ++index)
281  {
282  var itemView = _itemViews[index];
283 
284  [itemView setFrameY:y width:width];
285 
286  y = CGRectGetMaxY([itemView frame]);
287  }
288 
289  [self setFrameSize:_CGSizeMake(_CGRectGetWidth([self frame]), y)];
290 }
291 
292 @end
293 
294 @implementation _CPAccordionItemView : CPView
295 {
296  CPAccordionView _accordionView;
297 
298  BOOL _isCollapsed;
299  CPInteger _index;
300  CPView _headerView;
301  CPView _contentView;
302 }
303 
304 - (id)initWithAccordionView:(CPAccordionView)anAccordionView
305 {
306  self = [super initWithFrame:_CGRectMakeZero()];
307 
308  if (self)
309  {
310  _accordionView = anAccordionView;
311  _isCollapsed = NO;
312 
313  var bounds = [self bounds];
314 
315  _headerView = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:[_accordionView itemHeaderPrototype]]];
316 
317  if ([_headerView respondsToSelector:@selector(setTarget:)] && [_headerView respondsToSelector:@selector(setAction:)])
318  {
319  [_headerView setTarget:self];
320  [_headerView setAction:@selector(toggle:)];
321  }
322 
323  [self addSubview:_headerView];
324  }
325 
326  return self;
327 }
328 
329 - (void)toggle:(id)aSender
330 {
331  [_accordionView toggleItemAtIndex:[self index]];
332 }
333 
334 - (void)setLabel:(CPString)aLabel
335 {
336  if ([_headerView respondsToSelector:@selector(setTitle:)])
337  [_headerView setTitle:aLabel];
338 
339  else if ([_headerView respondsToSelector:@selector(setLabel:)])
340  [_headerView setLabel:aLabel];
341 
342  else if ([_headerView respondsToSelector:@selector(setStringValue:)])
343  [_headerView setStringValue:aLabel];
344 }
345 
346 - (void)setEnabled:(BOOL)isEnabled
347 {
348  if ([_headerView respondsToSelector:@selector(setEnabled:)])
349  [_headerView setEnabled:isEnabled];
350 }
351 
352 - (void)setContentView:(CPView)aView
353 {
354  if (_contentView === aView)
355  return;
356 
357  [_contentView removeObserver:self forKeyPath:@"frame"];
358 
359  [_contentView removeFromSuperview];
360 
361  _contentView = aView;
362 
363  [_contentView addObserver:self forKeyPath:@"frame" options:CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew context:NULL];
364 
365  [self addSubview:_contentView];
366 
367  [_accordionView _invalidateItemsStartingAtIndex:[self index]];
368 }
369 
370 - (void)setFrameY:(float)aY width:(float)aWidth
371 {
372  var headerHeight = _CGRectGetHeight([_headerView frame]);
373 
374  // Size to fit or something?
375  [_headerView setFrameSize:_CGSizeMake(aWidth, headerHeight)];
376  [_contentView setFrameOrigin:_CGPointMake(0.0, headerHeight)];
377 
378  if ([self isCollapsed])
379  [self setFrame:_CGRectMake(0.0, aY, aWidth, headerHeight)];
380 
381  else
382  {
383  var contentHeight = _CGRectGetHeight([_contentView frame]);
384 
385  [_contentView setFrameSize:_CGSizeMake(aWidth, contentHeight)];
386  [self setFrame:_CGRectMake(0.0, aY, aWidth, contentHeight + headerHeight)];
387  }
388 }
389 
390 - (void)resizeSubviewsWithOldSize:(CGSize)aSize
391 {
392 }
393 
394 - (void)observeValueForKeyPath:(CPString)aKeyPath
395  ofObject:(id)anObject
396  change:(CPDictionary)aChange
397  context:(id)aContext
398 {
399  if (aKeyPath === "frame" && !CGRectEqualToRect([aChange objectForKey:CPKeyValueChangeOldKey], [aChange objectForKey:CPKeyValueChangeNewKey]))
400  [_accordionView _invalidateItemsStartingAtIndex:[self index]];
401 /*
402  else if (aKeyPath === "itemHeaderPrototype")
403  {
404 
405  }
406 */
407 }
408 
409 @end
410 
412 
416 - (CPString)identifier
417 {
418  return _identifier;
419 }
420 
424 - (void)setIdentifier:(CPString)aValue
425 {
426  _identifier = aValue;
427 }
428 
432 - (CPView)view
433 {
434  return _view;
435 }
436 
440 - (void)setView:(CPView)aValue
441 {
442  _view = aValue;
443 }
444 
449 {
450  return _label;
451 }
452 
456 - (void)setLabel:(CPString)aValue
457 {
458  _label = aValue;
459 }
460 
461 @end