API  0.9.7
 All Classes Files Functions Variables Macros Groups Pages
CPTableColumn.j
Go to the documentation of this file.
1 /*
2  * CPTableColumn.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 @global CPTableViewColumnDidResizeNotification
26 
27 @class _CPTableColumnHeaderView
28 
29 CPTableColumnNoResizing = 0;
30 CPTableColumnAutoresizingMask = 1 << 0;
31 CPTableColumnUserResizingMask = 1 << 1;
32 
44 @implementation CPTableColumn : CPObject
45 {
46  CPTableView _tableView;
47  CPView _headerView;
48  CPView _dataView;
49  CPData _dataViewData;
50 
51  float _width;
52  float _minWidth;
53  float _maxWidth;
54  unsigned _resizingMask;
55 
56  id _identifier;
57  BOOL _isEditable;
58  CPSortDescriptor _sortDescriptorPrototype;
59  BOOL _isHidden;
60  CPString _headerToolTip;
61 
62  BOOL _disableResizingPosting;
63 }
64 
68 - (id)init
69 {
70  return [self initWithIdentifier:@""];
71 }
72 
77 - (id)initWithIdentifier:(id)anIdentifier
78 {
79  self = [super init];
80 
81  if (self)
82  {
83  _dataViewData = nil;
84 
85  _width = 100.0;
86  _minWidth = 10.0;
87  _maxWidth = 1000000.0;
88  _resizingMask = CPTableColumnAutoresizingMask | CPTableColumnUserResizingMask;
89  _disableResizingPosting = NO;
90 
91  [self setIdentifier:anIdentifier];
92 
93  var header = [[_CPTableColumnHeaderView alloc] initWithFrame:CGRectMakeZero()];
94  [self setHeaderView:header];
95 
96  [self setDataView:[CPTextField new]];
97  }
98 
99  return self;
100 }
101 
105 - (void)setTableView:(CPTableView)aTableView
106 {
107  _tableView = aTableView;
108 }
109 
113 - (CPTableView)tableView
114 {
115  return _tableView;
116 }
117 
128 - (int)_tryToResizeToWidth:(int)width
129 {
130  var min = [self minWidth],
131  max = [self maxWidth],
132  newWidth = ROUND(MIN(MAX(width, min), max));
133 
134  [self setWidth:newWidth];
135 
136  return newWidth - width;
137 }
138 
146 - (void)setWidth:(float)aWidth
147 {
148  aWidth = +aWidth;
149 
150  if (_width === aWidth)
151  return;
152 
153  var newWidth = MIN(MAX(aWidth, [self minWidth]), [self maxWidth]);
154 
155  if (_width === newWidth)
156  return;
157 
158  var oldWidth = _width;
159 
160  _width = newWidth;
161 
162  var tableView = [self tableView];
163 
164  if (tableView)
165  {
166  var index = [[tableView tableColumns] indexOfObjectIdenticalTo:self],
167  dirtyTableColumnRangeIndex = tableView._dirtyTableColumnRangeIndex;
168 
169  if (dirtyTableColumnRangeIndex < 0)
170  tableView._dirtyTableColumnRangeIndex = index;
171  else
172  tableView._dirtyTableColumnRangeIndex = MIN(index, tableView._dirtyTableColumnRangeIndex);
173 
174  var rows = tableView._exposedRows,
175  columns = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(index, [tableView._exposedColumns lastIndex] - index + 1)];
176 
177  // FIXME: Would be faster with some sort of -setNeedsDisplayInColumns: that updates a dirtyTableColumnForDisplay cache; then marked columns would relayout their data views at display time.
178  [tableView _layoutDataViewsInRows:rows columns:columns];
179  [tableView tile];
180 
181  if (!_disableResizingPosting)
182  [self _postDidResizeNotificationWithOldWidth:oldWidth];
183  }
184 }
185 
189 - (float)width
190 {
191  return _width;
192 }
193 
198 - (void)setMinWidth:(float)aMinWidth
199 {
200  aMinWidth = +aMinWidth;
201 
202  if (_minWidth === aMinWidth)
203  return;
204 
205  _minWidth = aMinWidth;
206 
207  var width = [self width],
208  newWidth = MAX(width, [self minWidth]);
209 
210  if (width !== newWidth)
211  [self setWidth:newWidth];
212 }
213 
217 - (float)minWidth
218 {
219  return _minWidth;
220 }
221 
226 - (void)setMaxWidth:(float)aMaxWidth
227 {
228  aMaxWidth = +aMaxWidth;
229 
230  if (_maxWidth === aMaxWidth)
231  return;
232 
233  _maxWidth = aMaxWidth;
234 
235  var width = [self width],
236  newWidth = MIN(width, [self maxWidth]);
237 
238  if (width !== newWidth)
239  [self setWidth:newWidth];
240 }
241 
245 - (float)maxWidth
246 {
247  return _maxWidth;
248 }
249 
261 - (void)setResizingMask:(unsigned)aResizingMask
262 {
263  _resizingMask = aResizingMask;
264 }
265 
266 
270 - (unsigned)resizingMask
271 {
272  return _resizingMask;
273 }
274 
278 - (void)sizeToFit
279 {
280  var width = CGRectGetWidth([_headerView frame]);
281 
282  if (width < [self minWidth])
283  [self setMinWidth:width];
284  else if (width > [self maxWidth])
285  [self setMaxWidth:width]
286 
287  if (_width !== width)
288  [self setWidth:width];
289 }
290 
291 
301 - (void)setHeaderView:(CPView)aView
302 {
303  if (!aView)
304  [CPException raise:CPInvalidArgumentException reason:@"Attempt to set nil header view on " + [self description]];
305 
306  _headerView = aView;
307 
308  var tableHeaderView = [_tableView headerView];
309 
310  [tableHeaderView setNeedsLayout];
311  [tableHeaderView setNeedsDisplay:YES];
312 }
313 
320 - (CPView)headerView
321 {
322  return _headerView;
323 }
324 
389 - (void)setDataView:(CPView)aView
390 {
391  if (_dataView)
392  _dataViewData = nil;
393 
394  [aView setThemeState:CPThemeStateTableDataView];
395 
396  _dataView = aView;
397  _dataViewData = [CPKeyedArchiver archivedDataWithRootObject:aView];
398 }
399 
400 - (CPView)dataView
401 {
402  return _dataView;
403 }
404 
405 /*
406  Returns the CPView object used by the CPTableView to draw values for the receiver.
407 
408  By default, this method just calls dataView. Subclassers can override if they need to
409  potentially use different "cells" or dataViews for different rows. Subclasses should expect this method
410  to be invoked with row equal to -1 in cases where no actual row is involved but the table
411  view needs to get some generic cell info.
412 */
413 - (id)dataViewForRow:(CPInteger)aRowIndex
414 {
415  return [self dataView];
416 }
417 
421 - (id)_newDataView
422 {
423  if (!_dataViewData)
424  return nil;
425 
426  var newDataView = [CPKeyedUnarchiver unarchiveObjectWithData:_dataViewData];
427  [newDataView setAutoresizingMask:CPViewNotSizable];
428 
429  return newDataView;
430 }
431 
432 //Setting the Identifier
433 
437 - (void)setIdentifier:(id)anIdentifier
438 {
439  _identifier = anIdentifier;
440 }
441 
445 - (id)identifier
446 {
447  return _identifier;
448 }
449 
450 //Controlling Editability
451 
455 - (void)setEditable:(BOOL)shouldBeEditable
456 {
457  _isEditable = shouldBeEditable;
458 }
459 
464 - (BOOL)isEditable
465 {
466  return _isEditable;
467 }
468 
472 - (void)setSortDescriptorPrototype:(CPSortDescriptor)aSortDescriptor
473 {
474  _sortDescriptorPrototype = aSortDescriptor;
475 }
476 
480 - (CPSortDescriptor)sortDescriptorPrototype
481 {
482  if (_sortDescriptorPrototype)
483  return _sortDescriptorPrototype;
484 
485  var binderClass = [[self class] _binderClassForBinding:CPValueBinding],
486  binding = [binderClass getBinding:CPValueBinding forObject:self];
487 
488  return [binding _defaultSortDescriptorPrototype];
489 }
490 
495 - (void)setHidden:(BOOL)shouldBeHidden
496 {
497  shouldBeHidden = !!shouldBeHidden
498 
499  if (_isHidden === shouldBeHidden)
500  return;
501 
502  _isHidden = shouldBeHidden;
503 
504  [[self headerView] setHidden:shouldBeHidden];
505  [[self tableView] _tableColumnVisibilityDidChange:self];
506 }
507 
511 - (BOOL)isHidden
512 {
513  return _isHidden;
514 }
515 
516 //Setting Tool Tips
517 
522 - (void)setHeaderToolTip:(CPString)aToolTip
523 {
524  _headerToolTip = aToolTip;
525 }
526 
530 - (CPString)headerToolTip
531 {
532  return _headerToolTip;
533 }
534 
538 - (void)_postDidResizeNotificationWithOldWidth:(float)oldWidth
539 {
540  [[self tableView] _didResizeTableColumn:self];
541 
543  postNotificationName:CPTableViewColumnDidResizeNotification
544  object:[self tableView]
545  userInfo:@{ @"CPTableColumn": self, @"CPOldWidth": oldWidth }];
546 }
547 
548 @end
549 @implementation CPTableColumnValueBinder : CPBinder
550 {
551  id __doxygen__;
552 }
553 
554 - (void)setValueFor:(CPString)aBinding
555 {
556  var tableView = [_source tableView],
557  column = [[tableView tableColumns] indexOfObjectIdenticalTo:_source],
558  rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [tableView numberOfRows])],
559  columnIndexes = [CPIndexSet indexSetWithIndex:column];
560 
561  [tableView reloadDataForRowIndexes:rowIndexes columnIndexes:columnIndexes];
562 }
563 
564 - (CPSortDescriptor)_defaultSortDescriptorPrototype
565 {
566  if (![self createsSortDescriptor])
567  return nil;
568 
569  var keyPath = [_info objectForKey:CPObservedKeyPathKey],
570  dotIndex = keyPath.indexOf(".");
571 
572  if (dotIndex === CPNotFound)
573  return nil;
574 
575  var firstPart = keyPath.substring(0, dotIndex),
576  key = keyPath.substring(dotIndex + 1);
577 
578  return [CPSortDescriptor sortDescriptorWithKey:key ascending:YES];
579 }
580 
581 - (BOOL)createsSortDescriptor
582 {
583  var options = [_info objectForKey:CPOptionsKey],
584  optionValue = [options objectForKey:CPCreatesSortDescriptorBindingOption];
585  return optionValue === nil ? YES : [optionValue boolValue];
586 }
587 
588 @end
589 
590 @implementation CPTableColumn (Bindings)
591 
592 + (Class)_binderClassForBinding:(CPString)aBinding
593 {
594  if (aBinding == CPValueBinding)
595  return [CPTableColumnValueBinder class];
596 
597  return [super _binderClassForBinding:aBinding];
598 }
599 
608 - (void)bind:(CPString)aBinding toObject:(id)anObject withKeyPath:(CPString)aKeyPath options:(CPDictionary)options
609 {
610  [super bind:aBinding toObject:anObject withKeyPath:aKeyPath options:options];
611 
612  if (![aBinding isEqual:@"someListOfExceptedBindings(notAcceptedBindings)"])
613  {
614  // Bind the table to the array controller this column is bound to.
615  // Note that anObject might not be the array controller. E.g. the keypath could be something like
616  // somePathTo.anArrayController.arrangedObjects.aKey. Cocoa doesn't support this but it is consistent
617  // and it makes sense.
618  var acIndex = aKeyPath.lastIndexOf("arrangedObjects."),
619  arrayController = anObject;
620 
621  if (acIndex > 1)
622  {
623  var firstPart = aKeyPath.substring(0, acIndex - 1);
624  arrayController = [anObject valueForKeyPath:firstPart];
625  }
626 
627  [[self tableView] _establishBindingsIfUnbound:arrayController];
628  }
629 }
630 
634 - (void)_prepareDataView:(CPView)aDataView forRow:(unsigned)aRow
635 {
636  var bindingsDictionary = [CPBinder allBindingsForObject:self],
637  keys = [bindingsDictionary allKeys];
638 
639  for (var i = 0, count = [keys count]; i < count; i++)
640  {
641  var bindingName = keys[i],
642  bindingPath = [aDataView _replacementKeyPathForBinding:bindingName],
643  binding = [bindingsDictionary objectForKey:bindingName],
644  bindingInfo = binding._info,
645  destination = [bindingInfo objectForKey:CPObservedObjectKey],
646  keyPath = [bindingInfo objectForKey:CPObservedKeyPathKey],
647  dotIndex = keyPath.lastIndexOf("."),
648  value;
649 
650  if (dotIndex === CPNotFound)
651  value = [[destination valueForKeyPath:keyPath] objectAtIndex:aRow];
652  else
653  {
654  /*
655  Optimize the prototypical use case where the key path describes a value
656  in an array. Without this optimization, we call CPArray's valueForKey
657  which generates as many values as objects in the array, of which we then
658  pick one and throw away the rest.
659 
660  The optimization is to get the array and access the value directly. This
661  turns the operation into a single access regardless of how long the model
662  array is.
663  */
664 
665  var firstPart = keyPath.substring(0, dotIndex),
666  secondPart = keyPath.substring(dotIndex + 1),
667  firstValue = [destination valueForKeyPath:firstPart];
668 
669  if ([firstValue isKindOfClass:CPArray])
670  value = [[firstValue objectAtIndex:aRow] valueForKeyPath:secondPart];
671  else
672  value = [[firstValue valueForKeyPath:secondPart] objectAtIndex:aRow];
673  }
674 
675  value = [binding transformValue:value withOptions:[bindingInfo objectForKey:CPOptionsKey]];
676  [aDataView setValue:value forKey:@"objectValue"];
677  }
678 }
679 
683 - (void)_reverseSetDataView:(CPView)aDataView forRow:(unsigned)aRow
684 {
685  var bindingsDictionary = [CPBinder allBindingsForObject:self],
686  keys = [bindingsDictionary allKeys],
687  newValue = [aDataView valueForKey:@"objectValue"];
688 
689  for (var i = 0, count = [keys count]; i < count; i++)
690  {
691  var bindingName = keys[i],
692  bindingPath = [aDataView _replacementKeyPathForBinding:bindingName],
693  binding = [bindingsDictionary objectForKey:bindingName],
694  bindingInfo = binding._info,
695  destination = [bindingInfo objectForKey:CPObservedObjectKey],
696  keyPath = [bindingInfo objectForKey:CPObservedKeyPathKey],
697  options = [bindingInfo objectForKey:CPOptionsKey],
698  dotIndex = keyPath.lastIndexOf(".");
699 
700  newValue = [binding reverseTransformValue:newValue withOptions:options];
701 
702  if (dotIndex === CPNotFound)
703  [[destination valueForKeyPath:keyPath] replaceObjectAtIndex:aRow withObject:newValue];
704  else
705  {
706  var firstPart = keyPath.substring(0, dotIndex),
707  secondPart = keyPath.substring(dotIndex + 1),
708  firstValue = [destination valueForKeyPath:firstPart];
709 
710  if ([firstValue isKindOfClass:CPArray])
711  [[firstValue objectAtIndex:aRow] setValue:newValue forKeyPath:secondPart];
712  else
713  [[firstValue valueForKeyPath:secondPart] replaceObjectAtIndex:aRow withObject:newValue];
714  }
715  }
716 }
717 
718 @end
719 
720 var CPTableColumnIdentifierKey = @"CPTableColumnIdentifierKey",
721  CPTableColumnHeaderViewKey = @"CPTableColumnHeaderViewKey",
722  CPTableColumnDataViewKey = @"CPTableColumnDataViewKey",
723  CPTableColumnWidthKey = @"CPTableColumnWidthKey",
724  CPTableColumnMinWidthKey = @"CPTableColumnMinWidthKey",
725  CPTableColumnMaxWidthKey = @"CPTableColumnMaxWidthKey",
726  CPTableColumnResizingMaskKey = @"CPTableColumnResizingMaskKey",
727  CPTableColumnIsHiddenKey = @"CPTableColumnIsHiddenKey",
728  CPSortDescriptorPrototypeKey = @"CPSortDescriptorPrototypeKey",
729  CPTableColumnIsEditableKey = @"CPTableColumnIsEditableKey";
730 
731 @implementation CPTableColumn (CPCoding)
732 
736 - (id)initWithCoder:(CPCoder)aCoder
737 {
738  self = [super init];
739 
740  if (self)
741  {
742  _dataViewData = nil;
743 
744  _width = [aCoder decodeFloatForKey:CPTableColumnWidthKey];
745  _minWidth = [aCoder decodeFloatForKey:CPTableColumnMinWidthKey];
746  _maxWidth = [aCoder decodeFloatForKey:CPTableColumnMaxWidthKey];
747 
748  [self setIdentifier:[aCoder decodeObjectForKey:CPTableColumnIdentifierKey]];
749  [self setHeaderView:[aCoder decodeObjectForKey:CPTableColumnHeaderViewKey]];
750  [self setDataView:[aCoder decodeObjectForKey:CPTableColumnDataViewKey]];
751 
752  _resizingMask = [aCoder decodeIntForKey:CPTableColumnResizingMaskKey];
753  _isHidden = [aCoder decodeBoolForKey:CPTableColumnIsHiddenKey];
754  _isEditable = [aCoder decodeBoolForKey:CPTableColumnIsEditableKey];
755 
756  _sortDescriptorPrototype = [aCoder decodeObjectForKey:CPSortDescriptorPrototypeKey];
757  }
758 
759  return self;
760 }
761 
765 - (void)encodeWithCoder:(CPCoder)aCoder
766 {
767  [aCoder encodeObject:_identifier forKey:CPTableColumnIdentifierKey];
768 
769  [aCoder encodeFloat:_width forKey:CPTableColumnWidthKey];
770  [aCoder encodeFloat:_minWidth forKey:CPTableColumnMinWidthKey];
771  [aCoder encodeFloat:_maxWidth forKey:CPTableColumnMaxWidthKey];
772 
773  [aCoder encodeObject:_headerView forKey:CPTableColumnHeaderViewKey];
774  [aCoder encodeObject:_dataView forKey:CPTableColumnDataViewKey];
775 
776  [aCoder encodeObject:_resizingMask forKey:CPTableColumnResizingMaskKey];
777  [aCoder encodeBool:_isHidden forKey:CPTableColumnIsHiddenKey];
778  [aCoder encodeBool:_isEditable forKey:CPTableColumnIsEditableKey];
779 
780  [aCoder encodeObject:_sortDescriptorPrototype forKey:CPSortDescriptorPrototypeKey];
781 }
782 
783 @end
784 
785 @implementation CPTableColumn (NSInCompatibility)
789 - (void)setHeaderCell:(CPView)aView
790 {
791  [CPException raise:CPUnsupportedMethodException
792  reason:@"setHeaderCell: is not supported. Use -setHeaderView:aView instead."];
793 }
794 
798 - (CPView)headerCell
799 {
800  [CPException raise:CPUnsupportedMethodException
801  reason:@"headCell is not supported. Use -headerView instead."];
802 }
803 
807 - (void)setDataCell:(CPView)aView
808 {
809  [CPException raise:CPUnsupportedMethodException
810  reason:@"setDataCell: is not supported. Use -setDataView:aView instead."];
811 }
812 
816 - (CPView)dataCell
817 {
818  [CPException raise:CPUnsupportedMethodException
819  reason:@"dataCell is not supported. Use -dataView instead."];
820 }
821 
825 - (id)dataCellForRow:(CPInteger)row
826 {
827  [CPException raise:CPUnsupportedMethodException
828  reason:@"dataCellForRow: is not supported. Use -dataViewForRow:row instead."];
829 }
830 
831 @end
832 
833 @implementation CPTableColumn (CPSynthesizedAccessors)
834 
838 - (BOOL)disableResizingPosting
839 {
840  return _disableResizingPosting;
841 }
842 
846 - (void)setDisableResizingPosting:(BOOL)aValue
847 {
848  _disableResizingPosting = aValue;
849 }
850 
851 @end