PageRenderTime 26ms CodeModel.GetById 20ms app.highlight 3ms RepoModel.GetById 1ms app.codeStats 0ms

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