![]() |
API 0.9.5
|
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