API 0.9.5
AppKit/CPTokenField.j
Go to the documentation of this file.
00001 /*
00002  * CPTokenField.j
00003  * AppKit
00004  *
00005  * Created by Klaas Pieter Annema.
00006  * Copyright 2008, 280 North, Inc.
00007  *
00008  * This library is free software; you can redistribute it and/or
00009  * modify it under the terms of the GNU Lesser General Public
00010  * License as published by the Free Software Foundation; either
00011  * version 2.1 of the License, or (at your option) any later version.
00012  *
00013  * This library is distributed in the hope that it will be useful,
00014  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00015  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
00016  * Lesser General Public License for more details.
00017  *
00018  * You should have received a copy of the GNU Lesser General Public
00019  * License along with this library; if not, write to the Free Software
00020  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
00021  */
00022 
00023 
00024 
00025 
00026 #if PLATFORM(DOM)
00027 
00028 var CPTokenFieldDOMInputElement = nil,
00029     CPTokenFieldDOMPasswordInputElement = nil,
00030     CPTokenFieldDOMStandardInputElement = nil,
00031     CPTokenFieldInputOwner = nil,
00032     CPTokenFieldTextDidChangeValue = nil,
00033     CPTokenFieldInputResigning = NO,
00034     CPTokenFieldInputDidBlur = NO,
00035     CPTokenFieldInputIsActive = NO,
00036     CPTokenFieldCachedSelectStartFunction = nil,
00037     CPTokenFieldCachedDragFunction = nil,
00038     CPTokenFieldFocusInput = NO,
00039 
00040     CPTokenFieldBlurFunction = nil,
00041     CPTokenFieldKeyUpFunction = nil,
00042     CPTokenFieldKeyPressFunction = nil,
00043     CPTokenFieldKeyDownFunction = nil;
00044 
00045 #endif
00046 
00047 var CPThemeStateAutoCompleting          = @"CPThemeStateAutoCompleting",
00048     CPTokenFieldTableColumnIdentifier   = @"CPTokenFieldTableColumnIdentifier",
00049 
00050     CPScrollDestinationNone             = 0,
00051     CPScrollDestinationLeft             = 1,
00052     CPScrollDestinationRight            = 2;
00053 
00054 @implementation CPTokenField : CPTextField
00055 {
00056     CPScrollView        _tokenScrollView;
00057     int                 _shouldScrollTo;
00058 
00059     CPRange             _selectedRange;
00060 
00061     CPView              _autocompleteContainer;
00062     CPScrollView        _autocompleteScrollView;
00063     CPTableView         _autocompleteView;
00064     CPTimeInterval      _completionDelay;
00065     CPTimer             _showCompletionsTimer;
00066 
00067     CPArray             _cachedCompletions;
00068 
00069     CPCharacterSet      _tokenizingCharacterSet;
00070 
00071     CPEvent             _mouseDownEvent;
00072 
00073     BOOL                _preventResign;
00074     BOOL                _shouldNotifyTarget;
00075 }
00076 
00077 + (CPCharacterSet)defaultTokenizingCharacterSet
00078 {
00079     return [CPCharacterSet characterSetWithCharactersInString:@","];
00080 }
00081 
00082 + (CPString)defaultThemeClass
00083 {
00084     return "tokenfield";
00085 }
00086 
00087 - (id)initWithFrame:(CPRect)frame
00088 {
00089     if (self = [super initWithFrame:frame])
00090     {
00091         _completionDelay = [CPTokenField defaultCompletionDelay];
00092         _tokenizingCharacterSet = [[self class] defaultTokenizingCharacterSet];
00093         [self setBezeled:YES];
00094 
00095         [self _init];
00096 
00097         [self setObjectValue:[]];
00098 
00099         [self setNeedsLayout];
00100     }
00101 
00102     return self;
00103 }
00104 
00105 - (void)_init
00106 {
00107     _selectedRange = CPMakeRange(0, 0);
00108 
00109     var frame = [self frame];
00110 
00111     _tokenScrollView = [[CPScrollView alloc] initWithFrame:CGRectMakeZero()];
00112     [_tokenScrollView setHasHorizontalScroller:NO];
00113     [_tokenScrollView setHasVerticalScroller:NO];
00114     [_tokenScrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
00115 
00116     var contentView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
00117     [contentView setAutoresizingMask:CPViewWidthSizable];
00118     [_tokenScrollView setDocumentView:contentView];
00119 
00120     [self addSubview:_tokenScrollView];
00121 
00122     _cachedCompletions = [];
00123 
00124     _autocompleteContainer = [[CPView alloc] initWithFrame:CPRectMake(0.0, 0.0, frame.size.width, 92.0)];
00125     [_autocompleteContainer setBackgroundColor:[_CPMenuWindow backgroundColorForBackgroundStyle:_CPMenuWindowPopUpBackgroundStyle]];
00126 
00127     _autocompleteScrollView = [[CPScrollView alloc] initWithFrame:CPRectMake(1.0, 1.0, frame.size.width - 2.0, 90.0)];
00128     [_autocompleteScrollView setAutohidesScrollers:YES];
00129     [_autocompleteScrollView setHasHorizontalScroller:NO];
00130     [_autocompleteContainer addSubview:_autocompleteScrollView];
00131 
00132     _autocompleteView = [[CPTableView alloc] initWithFrame:CPRectMakeZero()];
00133 
00134     var tableColumn = [[CPTableColumn alloc] initWithIdentifier:CPTokenFieldTableColumnIdentifier];
00135     [tableColumn setResizingMask:CPTableColumnAutoresizingMask];
00136     [_autocompleteView addTableColumn:tableColumn];
00137 
00138     [_autocompleteView setDataSource:self];
00139     [_autocompleteView setDelegate:self];
00140     [_autocompleteView setAllowsMultipleSelection:NO];
00141     [_autocompleteView setHeaderView:nil];
00142     [_autocompleteView setCornerView:nil];
00143     [_autocompleteView setRowHeight:30.0];
00144     [_autocompleteView setGridStyleMask:CPTableViewSolidHorizontalGridLineMask];
00145     [_autocompleteView setBackgroundColor:[CPColor clearColor]];
00146     [_autocompleteView setGridColor:[CPColor colorWithRed:242.0 / 255.0 green:243.0 / 255.0 blue:245.0 / 255.0 alpha:1.0]];
00147 
00148     [_autocompleteScrollView setDocumentView:_autocompleteView];
00149 }
00150 
00151 // ===============
00152 // = CONVENIENCE =
00153 // ===============
00154 - (void)_retrieveCompletions
00155 {
00156     var indexOfSelectedItem = 0;
00157 
00158     _cachedCompletions = [self tokenField:self completionsForSubstring:[self _inputElement].value indexOfToken:0 indexOfSelectedItem:indexOfSelectedItem];
00159 
00160     [_autocompleteView selectRowIndexes:[CPIndexSet indexSetWithIndex:indexOfSelectedItem] byExtendingSelection:NO];
00161     [_autocompleteView reloadData];
00162 }
00163 
00164 - (void)_autocompleteWithDOMEvent:(JSObject)DOMEvent
00165 {
00166     if (![self _inputElement].value && (!_cachedCompletions || ![self hasThemeState:CPThemeStateAutoCompleting]))
00167         return;
00168 
00169     [self _hideCompletions];
00170 
00171     var token = _cachedCompletions ? _cachedCompletions[[_autocompleteView selectedRow]] : nil,
00172         shouldRemoveLastObject = token !== @"" && [self _inputElement].value !== @"";
00173 
00174     if (!token)
00175         token = [self _inputElement].value;
00176 
00177     // Make sure the user typed an actual token to prevent the previous token from being emptied
00178     // If the input area is empty, we want to fall back to the normal behavior, resigning first
00179     // responder or selecting the next or previous key view.
00180     if (!token || token === @"")
00181     {
00182         if (DOMEvent && DOMEvent.keyCode === CPTabKeyCode)
00183         {
00184             if (!DOMEvent.shiftKey)
00185                 [[self window] selectNextKeyView:self];
00186             else
00187                 [[self window] selectPreviousKeyView:self];
00188         }
00189         else
00190             [[self window] makeFirstResponder:nil];
00191         return;
00192     }
00193 
00194     var objectValue = [self objectValue];
00195 
00196     // Remove the uncompleted token and add the token string.
00197     // Explicitly remove the last object because the array contains strings and removeObject uses isEqual to compare objects
00198     if (shouldRemoveLastObject)
00199         [objectValue removeObjectAtIndex:_selectedRange.location];
00200 
00201     // Give the delegate a chance to confirm, replace or add to the list of tokens being added.
00202     var delegateApprovedObjects = [self tokenField:self shouldAddObjects:[CPArray arrayWithObject:token] atIndex:_selectedRange.location],
00203         delegateApprovedObjectsCount = [delegateApprovedObjects count];
00204     if (delegateApprovedObjects)
00205     {
00206         for (var i = 0; i < delegateApprovedObjectsCount; i++)
00207         {
00208             [objectValue insertObject:[delegateApprovedObjects objectAtIndex:i] atIndex:_selectedRange.location + i];
00209         }
00210     }
00211 
00212     // Put the cursor after the last inserted token.
00213     var location = _selectedRange.location;
00214 
00215     [self setObjectValue:objectValue];
00216 
00217     if (delegateApprovedObjectsCount)
00218         location += delegateApprovedObjectsCount;
00219     _selectedRange = CPMakeRange(location, 0);
00220 
00221     [self _inputElement].value = @"";
00222     [self setNeedsLayout];
00223 
00224     [self _controlTextDidChange];
00225 }
00226 
00227 - (void)_autocomplete
00228 {
00229     [self _autocompleteWithDOMEvent:nil];
00230 }
00231 
00232 - (void)_selectToken:(_CPTokenFieldToken)token byExtendingSelection:(BOOL)extend
00233 {
00234     var indexOfToken = [[self _tokens] indexOfObject:token];
00235 
00236     if (indexOfToken == CPNotFound)
00237     {
00238         if (!extend)
00239             _selectedRange = CPMakeRange([[self _tokens] count], 0);
00240     }
00241     else if (extend)
00242         _selectedRange = CPUnionRange(_selectedRange, CPMakeRange(indexOfToken, 1));
00243     else
00244         _selectedRange = CPMakeRange(indexOfToken, 1);
00245 
00246     [self setNeedsLayout];
00247 }
00248 
00249 - (void)_deselectToken:(_CPTokenFieldToken)token
00250 {
00251     var indexOfToken = [[self _tokens] indexOfObject:token];
00252 
00253     if (CPLocationInRange(indexOfToken, _selectedRange))
00254         _selectedRange = CPMakeRange(MAX(indexOfToken, _selectedRange.location), MIN(_selectedRange.length, indexOfToken - _selectedRange.location));
00255 
00256     [self setNeedsLayout];
00257 }
00258 
00259 - (void)_deleteToken:(_CPTokenFieldToken)token
00260 {
00261     var indexOfToken = [[self _tokens] indexOfObject:token],
00262         objectValue = [self objectValue];
00263 
00264     // If the selection was to the right of the deleted token, move it to the left. If the deleted token was
00265     // selected, deselect it.
00266     if (indexOfToken < _selectedRange.location)
00267         _selectedRange.location--;
00268     else
00269         [self _deselectToken:token];
00270 
00271     // Preserve selection.
00272     var selection = CPCopyRange(_selectedRange);
00273     [objectValue removeObjectAtIndex:indexOfToken];
00274     [self setObjectValue:objectValue];
00275     _selectedRange = selection;
00276 
00277     [self setNeedsLayout];
00278     [self _controlTextDidChange];
00279 }
00280 
00281 - (void)_controlTextDidChange
00282 {
00283     var binderClass = [[self class] _binderClassForBinding:CPValueBinding],
00284         theBinding = [binderClass getBinding:CPValueBinding forObject:self];
00285 
00286     if (theBinding)
00287         [theBinding reverseSetValueFor:@"objectValue"];
00288 
00289     [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
00290 
00291     _shouldNotifyTarget = YES;
00292 }
00293 
00294 - (void)_removeSelectedTokens:(id)sender
00295 {
00296     var tokens = [self objectValue];
00297 
00298     for (var i = _selectedRange.length - 1; i >= 0; i--)
00299         [tokens removeObjectAtIndex:_selectedRange.location + i];
00300 
00301     var collapsedSelection = _selectedRange.location;
00302     [self setObjectValue:tokens];
00303     // setObjectValue moves the cursor to the end of the selection. We want it to stay
00304     // where the selected tokens were.
00305     _selectedRange = CPMakeRange(collapsedSelection, 0);
00306 
00307     [self _controlTextDidChange];
00308 }
00309 
00310 - (void)_updatePlaceholderState
00311 {
00312     if (([[self _tokens] count] === 0) && ![self hasThemeState:CPThemeStateEditing])
00313         [self setThemeState:CPTextFieldStatePlaceholder];
00314     else
00315         [self unsetThemeState:CPTextFieldStatePlaceholder];
00316 }
00317 
00318 // =============
00319 // = RESPONDER =
00320 // =============
00321 
00322 - (BOOL)becomeFirstResponder
00323 {
00324     if (CPTokenFieldInputOwner && [CPTokenFieldInputOwner window] !== [self window])
00325         [[CPTokenFieldInputOwner window] makeFirstResponder:nil];
00326 
00327     [self setThemeState:CPThemeStateEditing];
00328 
00329     [self _updatePlaceholderState];
00330 
00331     [self setNeedsLayout];
00332 
00333 #if PLATFORM(DOM)
00334 
00335     var string = [self stringValue],
00336         element = [self _inputElement];
00337 
00338     element.value = nil;
00339     element.style.color = [[self currentValueForThemeAttribute:@"text-color"] cssString];
00340     element.style.font = [[self currentValueForThemeAttribute:@"font"] cssString];
00341     element.style.zIndex = 1000;
00342 
00343     switch ([self alignment])
00344     {
00345         case CPCenterTextAlignment: element.style.textAlign = "center";
00346                                     break;
00347         case CPRightTextAlignment:  element.style.textAlign = "right";
00348                                     break;
00349         default:                    element.style.textAlign = "left";
00350     }
00351 
00352     var contentRect = [self contentRectForBounds:[self bounds]];
00353 
00354     element.style.top = CGRectGetMinY(contentRect) + "px";
00355     element.style.left = (CGRectGetMinX(contentRect) - 1) + "px"; // why -1?
00356     element.style.width = CGRectGetWidth(contentRect) + "px";
00357     element.style.height = CGRectGetHeight(contentRect) + "px";
00358 
00359     [_tokenScrollView documentView]._DOMElement.appendChild(element);
00360 
00361     window.setTimeout(function()
00362     {
00363         element.focus();
00364         CPTokenFieldInputOwner = self;
00365     }, 0.0);
00366 
00367     //post CPControlTextDidBeginEditingNotification
00368     [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
00369 
00370     [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
00371 
00372     CPTokenFieldInputIsActive = YES;
00373 
00374     if (document.attachEvent)
00375     {
00376         CPTokenFieldCachedSelectStartFunction = document.body.onselectstart;
00377         CPTokenFieldCachedDragFunction = document.body.ondrag;
00378 
00379         document.body.ondrag = function () {};
00380         document.body.onselectstart = function () {};
00381     }
00382 
00383 #endif
00384 
00385     return YES;
00386 }
00387 
00388 - (BOOL)resignFirstResponder
00389 {
00390     if (_preventResign)
00391         return NO;
00392 
00393     [self unsetThemeState:CPThemeStateEditing];
00394 
00395     [self _autocomplete];
00396 
00397 #if PLATFORM(DOM)
00398 
00399     var element = [self _inputElement];
00400 
00401     CPTokenFieldInputResigning = YES;
00402     element.blur();
00403 
00404     if (!CPTokenFieldInputDidBlur)
00405         CPTokenFieldBlurFunction();
00406 
00407     CPTokenFieldInputDidBlur = NO;
00408     CPTokenFieldInputResigning = NO;
00409 
00410     if (element.parentNode == [_tokenScrollView documentView]._DOMElement)
00411         element.parentNode.removeChild(element);
00412 
00413     CPTokenFieldInputIsActive = NO;
00414 
00415     if (document.attachEvent)
00416     {
00417         CPTokenFieldCachedSelectStartFunction = nil;
00418         CPTokenFieldCachedDragFunction = nil;
00419 
00420         document.body.ondrag = CPTokenFieldCachedDragFunction
00421         document.body.onselectstart = CPTokenFieldCachedSelectStartFunction
00422     }
00423 
00424 #endif
00425 
00426     [self _updatePlaceholderState];
00427 
00428     [self setNeedsLayout];
00429 
00430     if (_shouldNotifyTarget)
00431     {
00432         _shouldNotifyTarget = NO;
00433         [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidEndEditingNotification object:self userInfo:nil]];
00434 
00435         if ([self sendsActionOnEndEditing])
00436             [self sendAction:[self action] to:[self target]];
00437     }
00438 
00439     return YES;
00440 }
00441 
00442 - (void)mouseDown:(CPEvent)anEvent
00443 {
00444     _preventResign = YES;
00445     _mouseDownEvent = anEvent;
00446 
00447     [self _selectToken:nil byExtendingSelection:NO];
00448 
00449     [super mouseDown:anEvent];
00450 }
00451 
00452 - (void)mouseUp:(CPEvent)anEvent
00453 {
00454     _preventResign = NO;
00455     _mouseDownEvent = nil;
00456 }
00457 
00458 - (void)mouseDownOnToken:(_CPTokenFieldToken)aToken withEvent:(CPEvent)anEvent
00459 {
00460     _preventResign = YES;
00461     _mouseDownEvent = anEvent;
00462 }
00463 
00464 - (void)mouseUpOnToken:(_CPTokenFieldToken)aToken withEvent:(CPEvent)anEvent
00465 {
00466     if (_mouseDownEvent && CGPointEqualToPoint([_mouseDownEvent locationInWindow], [anEvent locationInWindow]))
00467     {
00468         [self _selectToken:aToken byExtendingSelection:[anEvent modifierFlags] & CPShiftKeyMask];
00469         [[self window] makeFirstResponder:self];
00470         // Snap to the token if it's only half visible due to mouse wheel scrolling.
00471         _shouldScrollTo = aToken;
00472     }
00473     _preventResign = NO;
00474 }
00475 
00476 // ===========
00477 // = CONTROL =
00478 // ===========
00479 - (CPArray)_tokens
00480 {
00481     // We return super here because objectValue uses this method
00482     // If we called self we would loop infinitely
00483     return [super objectValue];
00484 }
00485 
00486 - (CPString)stringValue
00487 {
00488     return [[self objectValue] componentsJoinedByString:@","];
00489 }
00490 
00491 - (id)objectValue
00492 {
00493     var objectValue = [];
00494     for (var i = 0, count = [[self _tokens] count]; i < count; i++)
00495     {
00496         var token = [[self _tokens] objectAtIndex:i];
00497 
00498         if ([token isKindOfClass:[CPString class]])
00499             continue;
00500 
00501         [objectValue addObject:[token representedObject]];
00502     }
00503 
00504 #if PLATFORM(DOM)
00505 
00506     if ([self _inputElement].value != @"")
00507         [objectValue insertObject:[self _inputElement].value atIndex:_selectedRange.location];
00508 
00509 #endif
00510 
00511     return objectValue;
00512 }
00513 
00514 - (void)setObjectValue:(id)aValue
00515 {
00516     if (aValue !== nil && ![aValue isKindOfClass:[CPArray class]])
00517     {
00518         [super setObjectValue:nil];
00519         return;
00520     }
00521 
00522     var superValue = [super objectValue];
00523     if (aValue === superValue || [aValue isEqualToArray:superValue])
00524         return;
00525 
00526     var contentView = [_tokenScrollView documentView];
00527 
00528     // Preserve as many existing tokens as possible to reduce redraw flickering.
00529     var oldTokens = [self _tokens],
00530         newTokens = [];
00531 
00532     if (aValue !== nil)
00533     {
00534         for (var i = 0, count = [aValue count]; i < count; i++)
00535         {
00536             // Do we have this token among the old ones?
00537             var tokenObject = aValue[i],
00538                 tokenValue = [self tokenField:self displayStringForRepresentedObject:tokenObject],
00539                 newToken = nil;
00540 
00541             for (var j = 0, oldCount = [oldTokens count]; j < oldCount; j++)
00542             {
00543                 var oldToken = oldTokens[j];
00544                 if ([oldToken representedObject] == tokenObject)
00545                 {
00546                     // Yep. Reuse it.
00547                     [oldTokens removeObjectAtIndex:j];
00548                     newToken = oldToken;
00549                     break;
00550                 }
00551             }
00552 
00553             if (newToken === nil)
00554             {
00555                 newToken = [[_CPTokenFieldToken alloc] init];
00556                 [newToken setTokenField:self];
00557                 [newToken setRepresentedObject:tokenObject];
00558                 [newToken setStringValue:tokenValue];
00559                 [contentView addSubview:newToken];
00560             }
00561 
00562             newTokens.push(newToken);
00563         }
00564     }
00565 
00566     // Remove any now unused tokens.
00567     for (var j = 0, oldCount = [oldTokens count]; j < oldCount; j++)
00568         [oldTokens[j] removeFromSuperview];
00569 
00570     /*
00571     [CPTextField setObjectValue] will try to set the _inputElement.value to
00572     the new objectValue, if the _inputElement exists. This is wrong for us
00573     since our objectValue is an array of tokens, so we can't use
00574     [super setObjectValue:objectValue];
00575 
00576     Instead do what CPControl setObjectValue would.
00577     */
00578     _value = newTokens;
00579 
00580     // Reset the selection.
00581     [self _selectToken:nil byExtendingSelection:NO];
00582 
00583     [self _updatePlaceholderState];
00584 
00585     _shouldScrollTo = CPScrollDestinationRight;
00586     [self setNeedsLayout];
00587     [self setNeedsDisplay:YES];
00588 }
00589 
00590 - (void)sendAction:(SEL)anAction to:(id)anObject
00591 {
00592     _shouldNotifyTarget = NO;
00593     [super sendAction:anAction to:anObject];
00594 }
00595 
00596 // Incredible hack to disable supers implementation
00597 // so it cannot change our object value and break the tokenfield
00598 - (void)_setStringValue:(id)aValue
00599 {
00600 }
00601 
00602 // ========
00603 // = VIEW =
00604 // ========
00605 - (void)viewDidMoveToWindow
00606 {
00607     [[[self window] contentView] addSubview:_autocompleteContainer];
00608 
00609 #if PLATFORM(DOM)
00610     _autocompleteContainer._DOMElement.style.zIndex = 1000; // Anything else doesn't seem to work
00611 #endif
00612 }
00613 
00614 - (void)removeFromSuperview
00615 {
00616     [_autocompleteContainer removeFromSuperview];
00617 }
00618 
00619 // =============
00620 // = TEXTFIELD =
00621 // =============
00622 #if PLATFORM(DOM)
00623 - (DOMElement)_inputElement
00624 {
00625     if (!CPTokenFieldDOMInputElement)
00626     {
00627         CPTokenFieldDOMInputElement = document.createElement("input");
00628         CPTokenFieldDOMInputElement.style.position = "absolute";
00629         CPTokenFieldDOMInputElement.style.border = "0px";
00630         CPTokenFieldDOMInputElement.style.padding = "0px";
00631         CPTokenFieldDOMInputElement.style.margin = "0px";
00632         CPTokenFieldDOMInputElement.style.whiteSpace = "pre";
00633         CPTokenFieldDOMInputElement.style.background = "transparent";
00634         CPTokenFieldDOMInputElement.style.outline = "none";
00635 
00636         CPTokenFieldBlurFunction = function(anEvent)
00637         {
00638             if (CPTokenFieldInputOwner && [CPTokenFieldInputOwner._tokenScrollView documentView]._DOMElement != CPTokenFieldDOMInputElement.parentNode)
00639                 return;
00640 
00641             if (CPTokenFieldInputOwner && CPTokenFieldInputOwner._preventResign)
00642                 return false;
00643 
00644             if (!CPTokenFieldInputResigning && !CPTokenFieldFocusInput)
00645             {
00646                 [[CPTokenFieldInputOwner window] makeFirstResponder:nil];
00647                 return;
00648             }
00649 
00650             CPTokenFieldHandleBlur(anEvent, CPTokenFieldDOMInputElement);
00651             CPTokenFieldInputDidBlur = YES;
00652 
00653             return true;
00654         }
00655 
00656         CPTokenFieldKeyDownFunction = function(aDOMEvent)
00657         {
00658             aDOMEvent = aDOMEvent || window.event
00659 
00660             CPTokenFieldTextDidChangeValue = [CPTokenFieldInputOwner stringValue];
00661 
00662             // Update the selectedIndex if necessary
00663             var index = [[CPTokenFieldInputOwner autocompleteView] selectedRow];
00664 
00665             if (aDOMEvent.keyCode === CPUpArrowKeyCode)
00666                 index -= 1;
00667             else if (aDOMEvent.keyCode === CPDownArrowKeyCode)
00668                 index += 1;
00669 
00670             if (index > [[CPTokenFieldInputOwner autocompleteView] numberOfRows] - 1)
00671                 index = [[CPTokenFieldInputOwner autocompleteView] numberOfRows] - 1;
00672 
00673             if (index < 0)
00674                 index = 0;
00675 
00676             [[CPTokenFieldInputOwner autocompleteView] selectRowIndexes:[CPIndexSet indexSetWithIndex:index] byExtendingSelection:NO];
00677 
00678             var autocompleteView = [CPTokenFieldInputOwner autocompleteView],
00679                 clipView = [[autocompleteView enclosingScrollView] contentView],
00680                 rowRect = [autocompleteView rectOfRow:index],
00681                 owner = CPTokenFieldInputOwner;
00682 
00683             if (rowRect && !CPRectContainsRect([clipView bounds], rowRect))
00684                 [clipView scrollToPoint:[autocompleteView rectOfRow:index].origin];
00685 
00686             if (aDOMEvent.keyCode === CPReturnKeyCode || aDOMEvent.keyCode === CPTabKeyCode)
00687             {
00688                 if (aDOMEvent.preventDefault)
00689                     aDOMEvent.preventDefault();
00690                 if (aDOMEvent.stopPropagation)
00691                     aDOMEvent.stopPropagation();
00692                 aDOMEvent.cancelBubble = true;
00693 
00694                 // Only resign first responder if we weren't auto-completing
00695                 if (![CPTokenFieldInputOwner hasThemeState:CPThemeStateAutoCompleting])
00696                 {
00697                     if (aDOMEvent && aDOMEvent.keyCode === CPReturnKeyCode)
00698                     {
00699                         [owner sendAction:[owner action] to:[owner target]];
00700                         [[owner window] makeFirstResponder:nil];
00701                     }
00702                     else if (aDOMEvent && aDOMEvent.keyCode === CPTabKeyCode)
00703                     {
00704                         if (!aDOMEvent.shiftKey)
00705                             [[owner window] selectNextKeyView:owner];
00706                         else
00707                             [[owner window] selectPreviousKeyView:owner];
00708                     }
00709                 }
00710 
00711                 [owner _autocompleteWithDOMEvent:aDOMEvent];
00712                 [owner setNeedsLayout];
00713             }
00714             else if (aDOMEvent.keyCode === CPEscapeKeyCode)
00715             {
00716                 [CPTokenFieldInputOwner _hideCompletions];
00717             }
00718             else if (aDOMEvent.keyCode === CPUpArrowKeyCode || aDOMEvent.keyCode === CPDownArrowKeyCode)
00719             {
00720                 if (aDOMEvent.preventDefault)
00721                     aDOMEvent.preventDefault();
00722                 if (aDOMEvent.stopPropagation)
00723                     aDOMEvent.stopPropagation();
00724                 aDOMEvent.cancelBubble = true;
00725             }
00726             else if (aDOMEvent.keyCode == CPLeftArrowKeyCode && owner._selectedRange.location > 0 && CPTokenFieldDOMInputElement.value == "")
00727             {
00728                 // Move the cursor back one token if the input is empty and the left arrow key is pressed.
00729                 if (!aDOMEvent.shiftKey)
00730                 {
00731                     if (owner._selectedRange.length)
00732                         // Simply collapse the range.
00733                         owner._selectedRange.length = 0;
00734                     else
00735                         owner._selectedRange.location--;
00736                 }
00737                 else
00738                 {
00739                     owner._selectedRange.location--;
00740                     // When shift is depressed, select the next token backwards.
00741                     owner._selectedRange.length++;
00742                 }
00743                 owner._shouldScrollTo = CPScrollDestinationLeft;
00744                 [owner setNeedsLayout];
00745             }
00746             else if (aDOMEvent.keyCode == CPRightArrowKeyCode && owner._selectedRange.location < [[owner _tokens] count] && CPTokenFieldDOMInputElement.value == "")
00747             {
00748                 if (!aDOMEvent.shiftKey)
00749                 {
00750                     if (owner._selectedRange.length)
00751                     {
00752                         // Place the cursor at the end of the selection and collapse.
00753                         owner._selectedRange.location = CPMaxRange(owner._selectedRange);
00754                         owner._selectedRange.length = 0;
00755                     }
00756                     else
00757                     {
00758                         // Move the cursor forward one token if the input is empty and the right arrow key is pressed.
00759                         owner._selectedRange.location = MIN([[owner _tokens] count], owner._selectedRange.location + owner._selectedRange.length + 1);
00760                     }
00761                 }
00762                 else
00763                 {
00764                     // Leave the selection location in place but include the next token to the right.
00765                     owner._selectedRange.length++;
00766                 }
00767                 owner._shouldScrollTo = CPScrollDestinationRight;
00768                 [owner setNeedsLayout];
00769             }
00770             else if (aDOMEvent.keyCode === CPDeleteKeyCode)
00771             {
00772                 // Highlight the previous token if backspace was pressed in an empty input element or re-show the completions view
00773                 if (CPTokenFieldDOMInputElement.value == @"")
00774                 {
00775                     [self _hideCompletions];
00776 
00777                     if (CPEmptyRange(CPTokenFieldInputOwner._selectedRange))
00778                     {
00779                         if (CPTokenFieldInputOwner._selectedRange.location > 0)
00780                         {
00781                             var tokens = [CPTokenFieldInputOwner _tokens],
00782                                 tokenView = [tokens objectAtIndex:(CPTokenFieldInputOwner._selectedRange.location - 1)];
00783                             [CPTokenFieldInputOwner _selectToken:tokenView byExtendingSelection:NO];
00784                         }
00785                     }
00786                     else
00787                         [CPTokenFieldInputOwner _removeSelectedTokens:nil];
00788                 }
00789                 else
00790                     [CPTokenFieldInputOwner _delayedShowCompletions];
00791             }
00792             else if (aDOMEvent.keyCode === CPDeleteForwardKeyCode && CPTokenFieldDOMInputElement.value == @"")
00793             {
00794                 // Delete forward if nothing is selected, else delete all selected.
00795                 [self _hideCompletions];
00796 
00797                 if (CPEmptyRange(CPTokenFieldInputOwner._selectedRange))
00798                 {
00799                     var tokens = [CPTokenFieldInputOwner _tokens];
00800                     if (CPTokenFieldInputOwner._selectedRange.location < [tokens count])
00801                         [CPTokenFieldInputOwner _deleteToken:tokens[CPTokenFieldInputOwner._selectedRange.location]];
00802                 }
00803                 else
00804                     [CPTokenFieldInputOwner _removeSelectedTokens:nil];
00805             }
00806 
00807             return true;
00808         }
00809 
00810         CPTokenFieldKeyPressFunction = function(aDOMEvent)
00811         {
00812             aDOMEvent = aDOMEvent || window.event;
00813 
00814             var character = String.fromCharCode(aDOMEvent.keyCode || aDOMEvent.which),
00815                 owner = CPTokenFieldInputOwner;
00816 
00817             if ([[owner tokenizingCharacterSet] characterIsMember:character])
00818             {
00819                 if (aDOMEvent.preventDefault)
00820                     aDOMEvent.preventDefault();
00821                 if (aDOMEvent.stopPropagation)
00822                     aDOMEvent.stopPropagation();
00823                 aDOMEvent.cancelBubble = true;
00824 
00825                 [owner _autocompleteWithDOMEvent:aDOMEvent];
00826                 [owner setNeedsLayout];
00827 
00828                 return true;
00829             }
00830 
00831             [CPTokenFieldInputOwner _delayedShowCompletions];
00832             // If there was a selection, collapse it now since we're typing in a new token.
00833             owner._selectedRange.length = 0;
00834 
00835             // Force immediate layout in case word wrapping is now necessary.
00836             [owner setNeedsLayout];
00837             [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
00838         }
00839 
00840         CPTokenFieldKeyUpFunction = function()
00841         {
00842             if ([CPTokenFieldInputOwner stringValue] !== CPTokenFieldTextDidChangeValue)
00843             {
00844                 CPTokenFieldTextDidChangeValue = [CPTokenFieldInputOwner stringValue];
00845                 [CPTokenFieldInputOwner textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:CPTokenFieldInputOwner userInfo:nil]];
00846             }
00847 
00848             [self setNeedsLayout];
00849 
00850             [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
00851         }
00852 
00853         CPTokenFieldHandleBlur = function(anEvent)
00854         {
00855             CPTokenFieldInputOwner = nil;
00856 
00857             [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
00858         }
00859 
00860         if (document.attachEvent)
00861         {
00862             CPTokenFieldDOMInputElement.attachEvent("on" + CPDOMEventKeyUp, CPTokenFieldKeyUpFunction);
00863             CPTokenFieldDOMInputElement.attachEvent("on" + CPDOMEventKeyDown, CPTokenFieldKeyDownFunction);
00864             CPTokenFieldDOMInputElement.attachEvent("on" + CPDOMEventKeyPress, CPTokenFieldKeyPressFunction);
00865         }
00866         else
00867         {
00868             CPTokenFieldDOMInputElement.addEventListener(CPDOMEventKeyUp, CPTokenFieldKeyUpFunction, NO);
00869             CPTokenFieldDOMInputElement.addEventListener(CPDOMEventKeyDown, CPTokenFieldKeyDownFunction, NO);
00870             CPTokenFieldDOMInputElement.addEventListener(CPDOMEventKeyPress, CPTokenFieldKeyPressFunction, NO);
00871         }
00872 
00873         //FIXME make this not onblur
00874         CPTokenFieldDOMInputElement.onblur = CPTokenFieldBlurFunction;
00875 
00876         CPTokenFieldDOMStandardInputElement = CPTokenFieldDOMInputElement;
00877     }
00878 
00879     if (CPFeatureIsCompatible(CPInputTypeCanBeChangedFeature))
00880     {
00881         if ([CPTokenFieldInputOwner isSecure])
00882             CPTokenFieldDOMInputElement.type = "password";
00883         else
00884             CPTokenFieldDOMInputElement.type = "text";
00885 
00886         return CPTokenFieldDOMInputElement;
00887     }
00888 
00889     return CPTokenFieldDOMInputElement;
00890 }
00891 #endif
00892 
00893 // - (void)setTokenStyle: (NSTokenStyle) style;
00894 // - (NSTokenStyle)tokenStyle;
00895 //
00896 
00897 // ====================
00898 // = COMPLETION DELAY =
00899 // ====================
00900 - (void)setCompletionDelay:(CPTimeInterval)delay
00901 {
00902     _completionDelay = delay;
00903 }
00904 
00905 - (NSTimeInterval)completionDelay
00906 {
00907     return _completionDelay;
00908 }
00909 
00910 + (NSTimeInterval)defaultCompletionDelay
00911 {
00912     return 0.5;
00913 }
00914 
00915 // ===========================
00916 // = SHOW / HIDE COMPLETIONS =
00917 // ===========================
00918 - (void)_showCompletions:(CPTimer)timer
00919 {
00920     [self _retrieveCompletions]
00921     [self setThemeState:CPThemeStateAutoCompleting];
00922 
00923     [self setNeedsLayout];
00924 }
00925 
00926 - (void)_delayedShowCompletions
00927 {
00928     _showCompletionsTimer = [CPTimer scheduledTimerWithTimeInterval:[self completionDelay] target:self
00929                                                            selector:@selector(_showCompletions:) userInfo:nil repeats:NO];
00930 }
00931 
00932 - (void)_cancelShowCompletions
00933 {
00934     if ([_showCompletionsTimer isValid])
00935         [_showCompletionsTimer invalidate];
00936 }
00937 
00938 - (void)_hideCompletions
00939 {
00940     [self _cancelShowCompletions];
00941 
00942     [self unsetThemeState:CPThemeStateAutoCompleting];
00943     [self setNeedsLayout];
00944 }
00945 
00946 // ==========
00947 // = LAYOUT =
00948 // ==========
00949 - (void)layoutSubviews
00950 {
00951     [super layoutSubviews];
00952 
00953     [_tokenScrollView setFrame:[self rectForEphemeralSubviewNamed:"content-view"]];
00954 
00955     var textFieldContentView = [self layoutEphemeralSubviewNamed:@"content-view"
00956                                                       positioned:CPWindowAbove
00957                                  relativeToEphemeralSubviewNamed:@"bezel-view"];
00958 
00959     if (textFieldContentView)
00960         [textFieldContentView setHidden:[self stringValue] !== @""];
00961 
00962     var frame = [self frame],
00963         contentView = [_tokenScrollView documentView],
00964         tokens = [self _tokens];
00965 
00966     // Correctly size the tableview
00967     // FIXME Horizontal scrolling will not work because we are not actually looking at the content to set the width for the table column
00968     [[_autocompleteView tableColumnWithIdentifier:CPTokenFieldTableColumnIdentifier] setWidth:[[_autocompleteScrollView contentView] frame].size.width];
00969 
00970     if ([self hasThemeState:CPThemeStateAutoCompleting] && [_cachedCompletions count])
00971     {
00972         // Manually sizeToFit because CPTableView's sizeToFit doesn't work properly
00973         [_autocompleteContainer setHidden:NO];
00974         var frameOrigin = [self convertPoint:[self bounds].origin toView:[_autocompleteContainer superview]];
00975         [_autocompleteContainer setFrameOrigin:CPPointMake(frameOrigin.x, frameOrigin.y + frame.size.height)];
00976         [_autocompleteContainer setFrameSize:CPSizeMake(CPRectGetWidth([self bounds]), 92.0)];
00977         [_autocompleteScrollView setFrameSize:CPSizeMake([_autocompleteContainer frame].size.width - 2.0, 90.0)];
00978     }
00979     else
00980         [_autocompleteContainer setHidden:YES];
00981 
00982     // Hack to make sure we are handling an array
00983     if (![tokens isKindOfClass:[CPArray class]])
00984         return;
00985 
00986     // Move each token into the right position.
00987     var contentRect = CGRectMakeCopy([contentView bounds]),
00988         contentOrigin = contentRect.origin,
00989         contentSize = contentRect.size,
00990         offset = CPPointMake(contentOrigin.x, contentOrigin.y),
00991         spaceBetweenTokens = CPSizeMake(2.0, 2.0),
00992         isEditing = [[self window] firstResponder] == self,
00993         tokenToken = [_CPTokenFieldToken new];
00994 
00995     // Get the height of a typical token, or a token token if you will.
00996     [tokenToken sizeToFit];
00997 
00998     var tokenHeight = CGRectGetHeight([tokenToken bounds]);
00999 
01000     var fitAndFrame = function(width, height)
01001     {
01002         var r = CGRectMake(0, 0, width, height);
01003 
01004         if (offset.x + width >= contentSize.width && offset.x > contentOrigin.x)
01005         {
01006             offset.x = contentOrigin.x;
01007             offset.y += height + spaceBetweenTokens.height;
01008         }
01009 
01010         r.origin.x = offset.x;
01011         r.origin.y = offset.y;
01012 
01013         // Make sure the frame fits.
01014         if (CGRectGetHeight([contentView bounds]) < offset.y + height)
01015             [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + height)];
01016 
01017         offset.x += width + spaceBetweenTokens.width;
01018 
01019         return r;
01020     }
01021 
01022     var placeEditor = function(useRemainingWidth)
01023     {
01024         var element = [self _inputElement],
01025             textWidth = 1;
01026 
01027         if (_selectedRange.length === 0)
01028         {
01029             // XXX The "X" here is used to estimate the space needed to fit the next character
01030             // without clipping. Since different fonts might have different sizes of "X" this
01031             // solution is not ideal, but it works.
01032             textWidth = [(element.value || @"") + "X" sizeWithFont:[self font]].width;
01033             if (useRemainingWidth)
01034                 textWidth = MAX(contentSize.width - offset.x - 1, textWidth);
01035         }
01036 
01037         var inputFrame = fitAndFrame(textWidth, tokenHeight);
01038 
01039         element.style.left = inputFrame.origin.x + "px";
01040         element.style.top = inputFrame.origin.y + "px";
01041         element.style.width = inputFrame.size.width + "px";
01042         element.style.height = inputFrame.size.height + "px";
01043 
01044         // When editing, always scroll to the cursor.
01045         if (_selectedRange.length == 0)
01046             [[_tokenScrollView documentView] scrollRectToVisible:inputFrame];
01047     }
01048 
01049     for (var i = 0, count = [tokens count]; i < count; i++)
01050     {
01051         if (isEditing && i == CPMaxRange(_selectedRange))
01052             placeEditor(false);
01053 
01054         var tokenView = [tokens objectAtIndex:i];
01055 
01056         // Make sure we are only changing completed tokens
01057         if ([tokenView isKindOfClass:[CPString class]])
01058             continue;
01059 
01060         [tokenView setHighlighted:CPLocationInRange(i, _selectedRange)];
01061         [tokenView sizeToFit];
01062 
01063         var size = [contentView bounds].size,
01064             tokenViewSize = [tokenView bounds].size,
01065             tokenFrame = fitAndFrame(tokenViewSize.width, tokenViewSize.height);
01066 
01067         [tokenView setFrame:tokenFrame];
01068     }
01069 
01070     if (isEditing && CPMaxRange(_selectedRange) >= [tokens count])
01071         placeEditor(true);
01072 
01073     // Hide the editor if there are selected tokens, but still keep it active
01074     // so we can continue using our standard keyboard handling events.
01075     if (isEditing && _selectedRange.length)
01076     {
01077         [self _inputElement].style.left = "-10000px";
01078         [self _inputElement].focus();
01079     }
01080 
01081     // Trim off any excess height downwards.
01082     if (CGRectGetHeight([contentView bounds]) > offset.y + tokenHeight)
01083         [contentView setFrame:CGRectMake(0, 0, CGRectGetWidth([_tokenScrollView bounds]), offset.y + tokenHeight)];
01084 
01085     if (_shouldScrollTo !== CPScrollDestinationNone)
01086     {
01087         // Only carry out the scroll if the cursor isn't visible.
01088         if (!(isEditing && _selectedRange.length == 0))
01089         {
01090 
01091             var scrollToToken = _shouldScrollTo;
01092             if (scrollToToken === CPScrollDestinationLeft)
01093                 scrollToToken = tokens[_selectedRange.location]
01094             else if (scrollToToken === CPScrollDestinationRight)
01095                 scrollToToken = tokens[MAX(0, CPMaxRange(_selectedRange) - 1)];
01096             [self _scrollTokenViewToVisible:scrollToToken];
01097         }
01098         _shouldScrollTo = CPScrollDestinationNone;
01099     }
01100 }
01101 
01102 - (BOOL)_scrollTokenViewToVisible:(_CPTokenFieldToken)aToken
01103 {
01104     if (!aToken)
01105         return;
01106 
01107     return [[_tokenScrollView documentView] scrollRectToVisible:[aToken frame]];
01108 }
01109 
01110 // ======================
01111 // = TABLEVIEW DATSOURCE / DELEGATE =
01112 // ======================
01113 - (int)numberOfRowsInTableView:(CPTableView)tableView
01114 {
01115     return [_cachedCompletions count];
01116 }
01117 
01118 - (void)tableView:(CPTableView)tableView objectValueForTableColumn:(CPTableColumn)tableColumn row:(int)row
01119 {
01120     return [self tokenField:self displayStringForRepresentedObject:[_cachedCompletions objectAtIndex:row]];
01121 }
01122 
01123 - (void)tableViewSelectionDidChange:(CPNotification)notification
01124 {
01125     // make sure a mouse click in the tableview doesn't steal first responder state
01126     window.setTimeout(function()
01127     {
01128         [[self window] makeFirstResponder:self];
01129     }, 2.0);
01130 }
01131 
01132 // =============
01133 // = ACCESSORS =
01134 // =============
01135 - (CPTableView)autocompleteView
01136 {
01137     return _autocompleteView;
01138 }
01139 
01140 @end
01141 
01142 @implementation CPTokenField (CPTokenFieldDelegate)
01143 
01144 // // Each element in the array should be an NSString or an array of NSStrings.
01145 // // substring is the partial string that is being completed.  tokenIndex is the index of the token being completed.
01146 // // selectedIndex allows you to return by reference an index specifying which of the completions should be selected initially.
01147 // // The default behavior is not to have any completions.
01148 - (CPArray)tokenField:(CPTokenField)tokenField completionsForSubstring:(CPString)substring indexOfToken:(int)tokenIndex indexOfSelectedItem:(int)selectedIndex
01149 {
01150     if ([[self delegate] respondsToSelector:@selector(tokenField:completionsForSubstring:indexOfToken:indexOfSelectedItem:)])
01151     {
01152         return [[self delegate] tokenField:tokenField completionsForSubstring:substring indexOfToken:tokenIndex indexOfSelectedItem:selectedIndex];
01153     }
01154 
01155     return [];
01156 }
01157 
01158 // // Allows the delegate to provide a string to be displayed as a proxy for the given represented object.
01159 // // If you return nil or do not implement this method, then representedObject is displayed as the string.
01160 - (CPString)tokenField:(CPTokenField)tokenField displayStringForRepresentedObject:(id)representedObject
01161 {
01162     if ([[self delegate] respondsToSelector:@selector(tokenField:displayStringForRepresentedObject:)])
01163     {
01164         var stringForRepresentedObject = [[self delegate] tokenField:tokenField displayStringForRepresentedObject:representedObject];
01165         if (stringForRepresentedObject !== nil)
01166         {
01167             return stringForRepresentedObject;
01168         }
01169     }
01170 
01171     return representedObject;
01172 }
01173 
01174 //
01175 // // return an array of represented objects you want to add.
01176 // // If you want to reject the add, return an empty array.
01177 // // returning nil will cause an error.
01178 - (CPArray)tokenField:(CPTokenField)tokenField shouldAddObjects:(CPArray)tokens atIndex:(int)index
01179 {
01180     var  delegate = [self delegate];
01181     if ([delegate respondsToSelector:@selector(tokenField:shouldAddObjects:atIndex:)])
01182     {
01183         var approvedObjects = [delegate tokenField:tokenField shouldAddObjects:tokens atIndex:index];
01184         if (approvedObjects !== nil)
01185             return approvedObjects;
01186     }
01187 
01188     return tokens;
01189 }
01190 
01191 //
01192 // // If you return nil or don't implement these delegate methods, we will assume
01193 // // editing string = display string = represented object
01194 // - (NSString *)tokenField:(NSTokenField *)tokenField editingStringForRepresentedObject:(id)representedObject;
01195 // - (id)tokenField:(NSTokenField *)tokenField representedObjectForEditingString: (NSString *)editingString;
01196 //
01197 // // We put the string on the pasteboard before calling this delegate method.
01198 // // By default, we write the NSStringPboardType as well as an array of NSStrings.
01199 // - (BOOL)tokenField:(NSTokenField *)tokenField writeRepresentedObjects:(NSArray *)objects toPasteboard:(NSPasteboard *)pboard;
01200 //
01201 // // Return an array of represented objects to add to the token field.
01202 // - (NSArray *)tokenField:(NSTokenField *)tokenField readFromPasteboard:(NSPasteboard *)pboard;
01203 //
01204 // // By default the tokens have no menu.
01205 // - (NSMenu *)tokenField:(NSTokenField *)tokenField menuForRepresentedObject:(id)representedObject;
01206 // - (BOOL)tokenField:(NSTokenField *)tokenField hasMenuForRepresentedObject:(id)representedObject;
01207 //
01208 // // This method allows you to change the style for individual tokens as well as have mixed text and tokens.
01209 // - (NSTokenStyle)tokenField:(NSTokenField *)tokenField styleForRepresentedObject:(id)representedObject;
01210 
01211 @end
01212 
01213 @implementation _CPTokenFieldToken : CPTextField
01214 {
01215     _CPTokenFieldTokenCloseButton   _deleteButton;
01216     CPTokenField                    _tokenField;
01217     id                              _representedObject;
01218 }
01219 
01220 + (CPString)defaultThemeClass
01221 {
01222     return "tokenfield-token";
01223 }
01224 
01225 - (id)initWithFrame:(CPRect)frame
01226 {
01227     if (self = [super initWithFrame:frame])
01228     {
01229         _deleteButton = [[_CPTokenFieldTokenCloseButton alloc] initWithFrame:CPRectMakeZero()];
01230         [self addSubview:_deleteButton];
01231 
01232         [self setEditable:NO];
01233         [self setHighlighted:NO];
01234         [self setBezeled:YES];
01235     }
01236 
01237     return self;
01238 }
01239 
01240 - (CPTokenField)tokenField
01241 {
01242     return _tokenField;
01243 }
01244 
01245 - (void)setTokenField:(CPTokenField)tokenField
01246 {
01247     _tokenField = tokenField;
01248 }
01249 
01250 - (id)representedObject
01251 {
01252     return _representedObject;
01253 }
01254 
01255 - (void)setRepresentedObject:(id)representedObject
01256 {
01257     _representedObject = representedObject;
01258 }
01259 
01260 - (CGSize)_minimumFrameSize
01261 {
01262     var size = CGSizeMakeZero(),
01263         minSize = [self currentValueForThemeAttribute:@"min-size"],
01264         contentInset = [self currentValueForThemeAttribute:@"content-inset"];
01265 
01266     // Tokens are fixed height, so we could as well have used max-size here.
01267     size.height = minSize.height;
01268     size.width = MAX(minSize.width, [([self stringValue] || @" ") sizeWithFont:[self font]].width + contentInset.left + contentInset.right);
01269 
01270     return size;
01271 }
01272 
01273 - (void)layoutSubviews
01274 {
01275     [super layoutSubviews];
01276 
01277     var bezelView = [self layoutEphemeralSubviewNamed:@"bezel-view"
01278                                            positioned:CPWindowBelow
01279                       relativeToEphemeralSubviewNamed:@"content-view"];
01280 
01281     if (bezelView)
01282     {
01283         [_deleteButton setTarget:self];
01284         [_deleteButton setAction:@selector(_delete:)];
01285 
01286         var frame = [bezelView frame],
01287             buttonOffset = [_deleteButton currentValueForThemeAttribute:@"offset"],
01288             buttonSize = [_deleteButton currentValueForThemeAttribute:@"min-size"];
01289 
01290         [_deleteButton setFrame:CPRectMake(CPRectGetMaxX(frame) - buttonOffset.x, CPRectGetMinY(frame) + buttonOffset.y, buttonSize.width, buttonSize.height)];
01291     }
01292 }
01293 
01294 - (void)mouseDown:(CPEvent)anEvent
01295 {
01296     [_tokenField mouseDownOnToken:self withEvent:anEvent];
01297 }
01298 
01299 - (void)mouseUp:(CPEvent)anEvent
01300 {
01301     [_tokenField mouseUpOnToken:self withEvent:anEvent];
01302 }
01303 
01304 - (void)_delete:(id)sender
01305 {
01306     [_tokenField _deleteToken:self];
01307 }
01308 
01309 @end
01310 
01311 /*
01312     Theming hook.
01313 */
01314 @implementation _CPTokenFieldTokenCloseButton : CPButton
01315 {
01316     id __doxygen__;
01317 }
01318 
01319 + (id)themeAttributes
01320 {
01321     var attributes = [CPButton themeAttributes];
01322 
01323     [attributes setObject:CGPointMake(15, 5) forKey:@"offset"];
01324 
01325     return attributes;
01326 }
01327 
01328 + (CPString)defaultThemeClass
01329 {
01330     return "tokenfield-token-close-button";
01331 }
01332 
01333 @end
01334 
01335 
01336 var CPTokenFieldTokenizingCharacterSetKey   = "CPTokenFieldTokenizingCharacterSetKey",
01337     CPTokenFieldCompletionDelayKey          = "CPTokenFieldCompletionDelay";
01338 
01339 @implementation CPTokenField (CPCoding)
01340 
01341 - (id)initWithCoder:(CPCoder)aCoder
01342 {
01343     self = [super initWithCoder:aCoder];
01344 
01345     if (self)
01346     {
01347         _tokenizingCharacterSet = [aCoder decodeObjectForKey:CPTokenFieldTokenizingCharacterSetKey] || [[self class] defaultTokenizingCharacterSet];
01348         _completionDelay = [aCoder decodeDoubleForKey:CPTokenFieldCompletionDelayKey] || [[self class] defaultCompletionDelay];
01349 
01350         [self _init];
01351 
01352         [self setNeedsLayout];
01353         [self setNeedsDisplay:YES];
01354     }
01355 
01356     return self;
01357 }
01358 
01359 - (void)encodeWithCoder:(CPCoder)aCoder
01360 {
01361     [super encodeWithCoder:aCoder];
01362 
01363     [aCoder encodeInt:_tokenizingCharacterSet forKey:CPTokenFieldTokenizingCharacterSetKey];
01364     [aCoder encodeDouble:_completionDelay forKey:CPTokenFieldCompletionDelayKey];
01365 }
01366 
01367 @end
01368 
01369 @implementation CPTokenField (CPSynthesizedAccessors)
01370 
01374 - (CPCharacterSet)tokenizingCharacterSet
01375 {
01376     return _tokenizingCharacterSet;
01377 }
01378 
01382 - (void)setTokenizingCharacterSet:(CPCharacterSet)aValue
01383 {
01384     _tokenizingCharacterSet = aValue;
01385 }
01386 
01387 @end
 All Classes Files Functions Variables Defines