PageRenderTime 27ms CodeModel.GetById 2ms app.highlight 12ms RepoModel.GetById 1ms app.codeStats 0ms

/AppKit/CPTokenField.j

http://github.com/cacaodev/cappuccino
Unknown | 1725 lines | 1365 code | 360 blank | 0 comment | 0 complexity | 8fb9209022f4c692a1df7f240223da6a MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/*
   2 * CPTokenField.j
   3 * AppKit
   4 *
   5 * Created by Klaas Pieter Annema.
   6 * Copyright 2008, 280 North, Inc.
   7 *
   8 * This library is free software; you can redistribute it and/or
   9 * modify it under the terms of the GNU Lesser General Public
  10 * License as published by the Free Software Foundation; either
  11 * version 2.1 of the License, or (at your option) any later version.
  12 *
  13 * This library is distributed in the hope that it will be useful,
  14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  16 * Lesser General Public License for more details.
  17 *
  18 * You should have received a copy of the GNU Lesser General Public
  19 * License along with this library; if not, write to the Free Software
  20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  21 */
  22
  23@import <Foundation/CPCharacterSet.j>
  24@import <Foundation/CPIndexSet.j>
  25@import <Foundation/CPTimer.j>
  26
  27@import "_CPAutocompleteMenu.j"
  28@import "CPButton.j"
  29@import "CPPopUpButton.j"
  30@import "CPScrollView.j"
  31@import "CPTableView.j"
  32@import "CPText.j"
  33@import "CPTextField.j"
  34@import "CPWindow_Constants.j"
  35
  36@class _CPTokenFieldTokenCloseButton
  37@class _CPTokenFieldTokenDisclosureButton
  38
  39@global CPApp
  40@global CPTextFieldDidFocusNotification
  41@global CPTextFieldDidBlurNotification
  42
  43
  44// TODO: should be conform to protocol CPTextFieldDelegate
  45@protocol CPTokenFieldDelegate <CPObject>
  46
  47@optional
  48- (BOOL)tokenField:(CPTokenField)tokenField hasMenuForRepresentedObject:(id)representedObject;
  49- (CPArray)tokenField:(CPTokenField)tokenField completionsForSubstring:(CPString)substring indexOfToken:(CPInteger)tokenIndex indexOfSelectedItem:(CPInteger)selectedIndex;
  50- (CPArray)tokenField:(CPTokenField)tokenField shouldAddObjects:(CPArray)tokens atIndex:(CPUInteger)index;
  51- (CPMenu)tokenField:(CPTokenField)tokenField menuForRepresentedObject:(id)representedObject;
  52- (CPString )tokenField:(CPTokenField)tokenField displayStringForRepresentedObject:(id)representedObject;
  53- (id)tokenField:(CPTokenField)tokenField representedObjectForEditingString:(CPString)editingString;
  54
  55@end
  56
  57var CPTokenFieldDelegate_tokenField_hasMenuForRepresentedObject_                                = 1 << 1,
  58    CPTokenFieldDelegate_tokenField_completionsForSubstring_indexOfToken_indexOfSelectedItem_   = 1 << 2,
  59    CPTokenFieldDelegate_tokenField_shouldAddObjects_atIndex_                                   = 1 << 3,
  60    CPTokenFieldDelegate_tokenField_menuForRepresentedObject_                                   = 1 << 4,
  61    CPTokenFieldDelegate_tokenField_displayStringForRepresentedObject_                          = 1 << 5,
  62    CPTokenFieldDelegate_tokenField_representedObjectForEditingString_                          = 1 << 6;
  63
  64
  65#if PLATFORM(DOM)
  66
  67var CPTokenFieldDOMInputElement = nil,
  68    CPTokenFieldDOMPasswordInputElement = nil,
  69    CPTokenFieldDOMStandardInputElement = nil,
  70    CPTokenFieldInputOwner = nil,
  71    CPTokenFieldTextDidChangeValue = nil,
  72    CPTokenFieldInputResigning = NO,
  73    CPTokenFieldInputDidBlur = NO,
  74    CPTokenFieldInputIsActive = NO,
  75    CPTokenFieldCachedSelectStartFunction = nil,
  76    CPTokenFieldCachedDragFunction = nil,
  77    CPTokenFieldFocusInput = NO,
  78
  79    CPTokenFieldBlurHandler = nil;
  80
  81#endif
  82
  83var CPScrollDestinationNone             = 0,
  84    CPScrollDestinationLeft             = 1,
  85    CPScrollDestinationRight            = 2;
  86
  87CPTokenFieldDisclosureButtonType = 0;
  88CPTokenFieldDeleteButtonType     = 1;
  89
  90@implementation CPTokenField : CPTextField
  91{
  92    CPScrollView                _tokenScrollView;
  93    int                         _shouldScrollTo;
  94
  95    CPRange                     _selectedRange;
  96
  97    _CPAutocompleteMenu         _autocompleteMenu;
  98    CGRect                      _inputFrame;
  99
 100    CPTimeInterval              _completionDelay;
 101
 102    CPCharacterSet              _tokenizingCharacterSet @accessors(property=tokenizingCharacterSet);
 103
 104    CPEvent                     _mouseDownEvent;
 105
 106    BOOL                        _shouldNotifyTarget;
 107
 108    int                         _buttonType @accessors(property=buttonType);
 109
 110    id <CPTokenFieldDelegate>   _tokenFieldDelegate;
 111    unsigned                    _implementedTokenFieldDelegateMethods;
 112}
 113
 114+ (CPCharacterSet)defaultTokenizingCharacterSet
 115{
 116    return [CPCharacterSet characterSetWithCharactersInString:@","];
 117}
 118
 119+ (CPTimeInterval)defaultCompletionDelay
 120{
 121    return 0.5;
 122}
 123
 124+ (CPString)defaultThemeClass
 125{
 126    return "tokenfield";
 127}
 128
 129+ (CPDictionary)themeAttributes
 130{
 131    return @{ @"editor-inset": CGInsetMakeZero() };
 132}
 133
 134- (id)initWithFrame:(CGRect)frame
 135{
 136    if (self = [super initWithFrame:frame])
 137    {
 138        _completionDelay = [[self class] defaultCompletionDelay];
 139        _tokenizingCharacterSet = [[self class] defaultTokenizingCharacterSet];
 140        _buttonType = CPTokenFieldDisclosureButtonType;
 141        [self setBezeled:YES];
 142
 143        [self _init];
 144
 145        [self setObjectValue:[]];
 146
 147        [self setNeedsLayout];
 148    }
 149
 150    return self;
 151}
 152
 153- (void)_init
 154{
 155    _selectedRange = CPMakeRange(0, 0);
 156
 157    var frame = [self frame];
 158
 159    _tokenScrollView = [[CPScrollView alloc] initWithFrame:CGRectMakeZero()];
 160    [_tokenScrollView setHasHorizontalScroller:NO];
 161    [_tokenScrollView setHasVerticalScroller:NO];
 162    [_tokenScrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
 163
 164    var contentView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
 165    [contentView setAutoresizingMask:CPViewWidthSizable];
 166    [_tokenScrollView setDocumentView:contentView];
 167
 168    [self addSubview:_tokenScrollView];
 169}
 170
 171#pragma mark -
 172#pragma mark Delegate methods
 173
 174/*!
 175    Set the delegate of the receiver
 176*/
 177- (void)setDelegate:(id <CPTokenFieldDelegate>)aDelegate
 178{
 179    if (_tokenFieldDelegate === aDelegate)
 180        return;
 181
 182    _tokenFieldDelegate = aDelegate;
 183    _implementedTokenFieldDelegateMethods = 0;
 184
 185    if ([_tokenFieldDelegate respondsToSelector:@selector(tokenField:hasMenuForRepresentedObject:)])
 186        _implementedTokenFieldDelegateMethods |= CPTokenFieldDelegate_tokenField_hasMenuForRepresentedObject_;
 187
 188    if ([_tokenFieldDelegate respondsToSelector:@selector(tokenField:completionsForSubstring:indexOfToken:indexOfSelectedItem:)])
 189        _implementedTokenFieldDelegateMethods |= CPTokenFieldDelegate_tokenField_completionsForSubstring_indexOfToken_indexOfSelectedItem_;
 190
 191    if ([_tokenFieldDelegate respondsToSelector:@selector(tokenField:shouldAddObjects:atIndex:)])
 192        _implementedTokenFieldDelegateMethods |= CPTokenFieldDelegate_tokenField_shouldAddObjects_atIndex_;
 193
 194    if ([_tokenFieldDelegate respondsToSelector:@selector(tokenField:menuForRepresentedObject:)])
 195        _implementedTokenFieldDelegateMethods |= CPTokenFieldDelegate_tokenField_menuForRepresentedObject_;
 196
 197    if ([_tokenFieldDelegate respondsToSelector:@selector(tokenField:displayStringForRepresentedObject:)])
 198        _implementedTokenFieldDelegateMethods |= CPTokenFieldDelegate_tokenField_displayStringForRepresentedObject_;
 199
 200    if ([_tokenFieldDelegate respondsToSelector:@selector(tokenField:representedObjectForEditingString:)])
 201        _implementedTokenFieldDelegateMethods |= CPTokenFieldDelegate_tokenField_representedObjectForEditingString_;
 202
 203    [super setDelegate:_tokenFieldDelegate];
 204}
 205
 206- (_CPAutocompleteMenu)_autocompleteMenu
 207{
 208    if (!_autocompleteMenu)
 209        _autocompleteMenu = [[_CPAutocompleteMenu alloc] initWithTextField:self];
 210    return _autocompleteMenu;
 211}
 212
 213- (void)_complete:(_CPAutocompleteMenu)anAutocompleteMenu
 214{
 215    [self _autocompleteWithEvent:nil];
 216}
 217
 218- (void)_autocompleteWithEvent:(CPEvent)anEvent
 219{
 220    if (![self _editorValue] && (![_autocompleteMenu contentArray] || ![self hasThemeState:CPThemeStateAutocompleting]))
 221        return;
 222
 223    [self _hideCompletions];
 224
 225    var token = [_autocompleteMenu selectedItem],
 226        shouldRemoveLastObject = token !== @"" && [self _editorValue] !== @"";
 227
 228    if (!token)
 229        token = [self _editorValue];
 230
 231    // Make sure the user typed an actual token to prevent the previous token from being emptied
 232    // If the input area is empty, we want to fall back to the normal behavior, resigning first
 233    // responder or selecting the next or previous key view.
 234    if (!token || token === @"")
 235    {
 236        var character = [anEvent charactersIgnoringModifiers],
 237            modifierFlags = [anEvent modifierFlags];
 238
 239        if (character === CPTabCharacter)
 240        {
 241            if (!(modifierFlags & CPShiftKeyMask))
 242                [[self window] selectNextKeyView:self];
 243            else
 244                [[self window] selectPreviousKeyView:self];
 245        }
 246        else
 247            [[self window] makeFirstResponder:nil];
 248        return;
 249    }
 250
 251    var objectValue = [self objectValue];
 252
 253    // Remove the uncompleted token and add the token string.
 254    // Explicitly remove the last object because the array contains strings and removeObject uses isEqual to compare objects
 255    if (shouldRemoveLastObject)
 256        [objectValue removeObjectAtIndex:_selectedRange.location];
 257
 258    // Convert typed text into a represented object.
 259    token = [self _representedObjectForEditingString:token];
 260
 261    // Give the delegate a chance to confirm, replace or add to the list of tokens being added.
 262    var delegateApprovedObjects = [self _shouldAddObjects:[CPArray arrayWithObject:token] atIndex:_selectedRange.location],
 263        delegateApprovedObjectsCount = [delegateApprovedObjects count];
 264
 265    if (delegateApprovedObjects)
 266    {
 267        for (var i = 0; i < delegateApprovedObjectsCount; i++)
 268        {
 269            [objectValue insertObject:[delegateApprovedObjects objectAtIndex:i] atIndex:_selectedRange.location + i];
 270        }
 271    }
 272
 273    // Put the cursor after the last inserted token.
 274    var location = _selectedRange.location;
 275
 276    [self setObjectValue:objectValue];
 277
 278    if (delegateApprovedObjectsCount)
 279        location += delegateApprovedObjectsCount;
 280    _selectedRange = CPMakeRange(location, 0);
 281
 282    [self _inputElement].value = @"";
 283    [self setNeedsLayout];
 284
 285    [self _controlTextDidChange];
 286}
 287
 288- (void)_autocomplete
 289{
 290    [self _autocompleteWithEvent:nil];
 291}
 292
 293- (void)_selectToken:(_CPTokenFieldToken)token byExtendingSelection:(BOOL)extend
 294{
 295    var indexOfToken = [[self _tokens] indexOfObject:token];
 296
 297    if (indexOfToken == CPNotFound)
 298    {
 299        if (!extend)
 300            _selectedRange = CPMakeRange([[self _tokens] count], 0);
 301    }
 302    else if (extend)
 303        _selectedRange = CPUnionRange(_selectedRange, CPMakeRange(indexOfToken, 1));
 304    else
 305        _selectedRange = CPMakeRange(indexOfToken, 1);
 306
 307    [self setNeedsLayout];
 308}
 309
 310- (void)_deselectToken:(_CPTokenFieldToken)token
 311{
 312    var indexOfToken = [[self _tokens] indexOfObject:token];
 313
 314    if (CPLocationInRange(indexOfToken, _selectedRange))
 315        _selectedRange = CPMakeRange(MAX(indexOfToken, _selectedRange.location), MIN(_selectedRange.length, indexOfToken - _selectedRange.location));
 316
 317    [self setNeedsLayout];
 318}
 319
 320- (void)_deleteToken:(_CPTokenFieldToken)token
 321{
 322    var indexOfToken = [[self _tokens] indexOfObject:token],
 323        objectValue = [self objectValue];
 324
 325    // If the selection was to the right of the deleted token, move it to the left. If the deleted token was
 326    // selected, deselect it.
 327    if (indexOfToken < _selectedRange.location)
 328        _selectedRange.location--;
 329    else
 330        [self _deselectToken:token];
 331
 332    // Preserve selection.
 333    var selection = CPMakeRangeCopy(_selectedRange);
 334
 335    [objectValue removeObjectAtIndex:indexOfToken];
 336    [self setObjectValue:objectValue];
 337    _selectedRange = selection;
 338
 339    [self setNeedsLayout];
 340    [self _controlTextDidChange];
 341}
 342
 343- (void)_controlTextDidChange
 344{
 345    var binderClass = [[self class] _binderClassForBinding:CPValueBinding],
 346        theBinding = [binderClass getBinding:CPValueBinding forObject:self];
 347
 348    if (theBinding)
 349        [theBinding reverseSetValueFor:@"objectValue"];
 350
 351    [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
 352
 353    _shouldNotifyTarget = YES;
 354}
 355
 356- (void)_removeSelectedTokens:(id)sender
 357{
 358    var tokens = [self objectValue];
 359
 360    for (var i = _selectedRange.length - 1; i >= 0; i--)
 361        [tokens removeObjectAtIndex:_selectedRange.location + i];
 362
 363    var collapsedSelection = _selectedRange.location;
 364
 365    [self setObjectValue:tokens];
 366    // setObjectValue moves the cursor to the end of the selection. We want it to stay
 367    // where the selected tokens were.
 368    _selectedRange = CPMakeRange(collapsedSelection, 0);
 369
 370    [self _controlTextDidChange];
 371}
 372
 373- (void)_updatePlaceholderState
 374{
 375    if (([[self _tokens] count] === 0) && ![self hasThemeState:CPThemeStateEditing])
 376        [self setThemeState:CPTextFieldStatePlaceholder];
 377    else
 378        [self unsetThemeState:CPTextFieldStatePlaceholder];
 379}
 380
 381// =============
 382// = RESPONDER =
 383// =============
 384
 385- (BOOL)becomeFirstResponder
 386{
 387    if (![super becomeFirstResponder])
 388        return NO;
 389
 390#if PLATFORM(DOM)
 391    if (CPTokenFieldInputOwner && [CPTokenFieldInputOwner window] !== [self window])
 392        [[CPTokenFieldInputOwner window] makeFirstResponder:nil];
 393#endif
 394
 395    // As long as we are the first responder we need to monitor the key status of our window.
 396    [self _setObserveWindowKeyNotifications:YES];
 397
 398    [self scrollRectToVisible:[self bounds]];
 399
 400    if ([[self window] isKeyWindow])
 401        return [self _becomeFirstKeyResponder];
 402
 403    return YES;
 404}
 405
 406- (BOOL)_becomeFirstKeyResponder
 407{
 408    // If the token field is still not completely on screen, refuse to become
 409    // first responder, because the browser will scroll it into view out of our control.
 410    if (![self _isWithinUsablePlatformRect])
 411        return NO;
 412
 413    [self setThemeState:CPThemeStateEditing];
 414
 415    [self _updatePlaceholderState];
 416
 417    [self setNeedsLayout];
 418
 419#if PLATFORM(DOM)
 420
 421    var string = [self stringValue],
 422        element = [self _inputElement],
 423        font = [self currentValueForThemeAttribute:@"font"];
 424
 425    element.value = nil;
 426    element.style.color = [[self currentValueForThemeAttribute:@"text-color"] cssString];
 427    element.style.font = [font cssString];
 428    element.style.zIndex = 1000;
 429
 430    switch ([self alignment])
 431    {
 432        case CPCenterTextAlignment:
 433            element.style.textAlign = "center";
 434            break;
 435
 436        case CPRightTextAlignment:
 437            element.style.textAlign = "right";
 438            break;
 439
 440        default:
 441            element.style.textAlign = "left";
 442    }
 443
 444    var contentRect = [self contentRectForBounds:[self bounds]];
 445
 446    element.style.top = CGRectGetMinY(contentRect) + "px";
 447    element.style.left = (CGRectGetMinX(contentRect) - 1) + "px"; // <input> element effectively imposes a 1px left margin
 448    element.style.width = CGRectGetWidth(contentRect) + "px";
 449    element.style.height = [font defaultLineHeightForFont] + "px";
 450
 451    window.setTimeout(function()
 452    {
 453        [_tokenScrollView documentView]._DOMElement.appendChild(element);
 454
 455        //post CPControlTextDidBeginEditingNotification
 456        [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
 457
 458        window.setTimeout(function()
 459        {
 460            element.focus();
 461            CPTokenFieldInputOwner = self;
 462        }, 0.0);
 463
 464        [self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]];
 465    }, 0.0);
 466
 467    [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
 468
 469    CPTokenFieldInputIsActive = YES;
 470
 471    if (document.attachEvent)
 472    {
 473        CPTokenFieldCachedSelectStartFunction = document.body.onselectstart;
 474        CPTokenFieldCachedDragFunction = document.body.ondrag;
 475
 476        document.body.ondrag = function () {};
 477        document.body.onselectstart = function () {};
 478    }
 479
 480#endif
 481
 482    return YES;
 483}
 484
 485- (BOOL)resignFirstResponder
 486{
 487    [self _autocomplete];
 488
 489    // From CPTextField superclass.
 490    [self _setObserveWindowKeyNotifications:NO];
 491
 492    [self _resignFirstKeyResponder];
 493
 494    if (_shouldNotifyTarget)
 495    {
 496        _shouldNotifyTarget = NO;
 497        [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidEndEditingNotification object:self userInfo:@{"CPTextMovement": [self _currentTextMovement]}]];
 498
 499        if ([self sendsActionOnEndEditing])
 500            [self sendAction:[self action] to:[self target]];
 501    }
 502
 503    [self textDidBlur:[CPNotification notificationWithName:CPTextFieldDidBlurNotification object:self userInfo:nil]];
 504
 505    return YES;
 506}
 507
 508- (void)_resignFirstKeyResponder
 509{
 510    [self unsetThemeState:CPThemeStateEditing];
 511
 512    [self _updatePlaceholderState];
 513    [self setNeedsLayout];
 514
 515#if PLATFORM(DOM)
 516
 517    var element = [self _inputElement];
 518
 519    CPTokenFieldInputResigning = YES;
 520    element.blur();
 521
 522    if (!CPTokenFieldInputDidBlur)
 523        CPTokenFieldBlurHandler();
 524
 525    CPTokenFieldInputDidBlur = NO;
 526    CPTokenFieldInputResigning = NO;
 527
 528    if (element.parentNode == [_tokenScrollView documentView]._DOMElement)
 529        element.parentNode.removeChild(element);
 530
 531    CPTokenFieldInputIsActive = NO;
 532
 533    if (document.attachEvent)
 534    {
 535        CPTokenFieldCachedSelectStartFunction = nil;
 536        CPTokenFieldCachedDragFunction = nil;
 537
 538        document.body.ondrag = CPTokenFieldCachedDragFunction
 539        document.body.onselectstart = CPTokenFieldCachedSelectStartFunction
 540    }
 541
 542#endif
 543}
 544
 545- (void)mouseDown:(CPEvent)anEvent
 546{
 547    _mouseDownEvent = anEvent;
 548
 549    [self _selectToken:nil byExtendingSelection:NO];
 550
 551    [super mouseDown:anEvent];
 552}
 553
 554- (void)mouseUp:(CPEvent)anEvent
 555{
 556    _mouseDownEvent = nil;
 557}
 558
 559- (void)_mouseDownOnToken:(_CPTokenFieldToken)aToken withEvent:(CPEvent)anEvent
 560{
 561    _mouseDownEvent = anEvent;
 562}
 563
 564- (void)_mouseUpOnToken:(_CPTokenFieldToken)aToken withEvent:(CPEvent)anEvent
 565{
 566    if (_mouseDownEvent && CGPointEqualToPoint([_mouseDownEvent locationInWindow], [anEvent locationInWindow]))
 567    {
 568        [self _selectToken:aToken byExtendingSelection:[anEvent modifierFlags] & CPShiftKeyMask];
 569        [[self window] makeFirstResponder:self];
 570        // Snap to the token if it's only half visible due to mouse wheel scrolling.
 571        _shouldScrollTo = aToken;
 572    }
 573}
 574
 575// ===========
 576// = CONTROL =
 577// ===========
 578- (CPArray)_tokens
 579{
 580    // We return super here because objectValue uses this method
 581    // If we called self we would loop infinitely
 582    return [super objectValue];
 583}
 584
 585- (CPString)stringValue
 586{
 587    return [[self objectValue] componentsJoinedByString:@","];
 588}
 589
 590- (id)objectValue
 591{
 592    var objectValue = [];
 593
 594    for (var i = 0, count = [[self _tokens] count]; i < count; i++)
 595    {
 596        var token = [[self _tokens] objectAtIndex:i];
 597
 598        if ([token isKindOfClass:[CPString class]])
 599            continue;
 600
 601        [objectValue addObject:[token representedObject]];
 602    }
 603
 604#if PLATFORM(DOM)
 605
 606    if ([self _editorValue])
 607    {
 608        var token = [self _representedObjectForEditingString:[self _editorValue]];
 609        [objectValue insertObject:token atIndex:_selectedRange.location];
 610    }
 611
 612#endif
 613
 614    return objectValue;
 615}
 616
 617- (void)setObjectValue:(id)aValue
 618{
 619    if (aValue !== nil && ![aValue isKindOfClass:[CPArray class]])
 620    {
 621        [super setObjectValue:nil];
 622        return;
 623    }
 624
 625    var superValue = [super objectValue];
 626    if (aValue === superValue || [aValue isEqualToArray:superValue])
 627        return;
 628
 629    var contentView = [_tokenScrollView documentView],
 630        oldTokens = [self _tokens],
 631        newTokens = [];
 632
 633    // Preserve as many existing tokens as possible to reduce redraw flickering.
 634    if (aValue !== nil)
 635    {
 636        for (var i = 0, count = [aValue count]; i < count; i++)
 637        {
 638            // Do we have this token among the old ones?
 639            var tokenObject = aValue[i],
 640                tokenValue = [self _displayStringForRepresentedObject:tokenObject],
 641                newToken = nil;
 642
 643            for (var j = 0, oldCount = [oldTokens count]; j < oldCount; j++)
 644            {
 645                var oldToken = oldTokens[j];
 646                if ([oldToken representedObject] == tokenObject)
 647                {
 648                    // Yep. Reuse it.
 649                    [oldTokens removeObjectAtIndex:j];
 650                    newToken = oldToken;
 651                    break;
 652                }
 653            }
 654
 655            if (newToken === nil)
 656            {
 657                newToken = [_CPTokenFieldToken new];
 658                [newToken setTokenField:self];
 659                [newToken setRepresentedObject:tokenObject];
 660                [newToken setStringValue:tokenValue];
 661                [newToken setEditable:[self isEditable]];
 662                [contentView addSubview:newToken];
 663            }
 664
 665            newTokens.push(newToken);
 666        }
 667    }
 668
 669    // Remove any now unused tokens.
 670    for (var j = 0, oldCount = [oldTokens count]; j < oldCount; j++)
 671        [oldTokens[j] removeFromSuperview];
 672
 673    /*
 674    [CPTextField setObjectValue] will try to set the _inputElement.value to
 675    the new objectValue, if the _inputElement exists. This is wrong for us
 676    since our objectValue is an array of tokens, so we can't use
 677    [super setObjectValue:objectValue];
 678
 679    Instead do what CPControl setObjectValue would.
 680    */
 681    _value = newTokens;
 682
 683    // Reset the selection.
 684    [self _selectToken:nil byExtendingSelection:NO];
 685
 686    [self _updatePlaceholderState];
 687
 688    _shouldScrollTo = CPScrollDestinationRight;
 689    [self setNeedsLayout];
 690    [self setNeedsDisplay:YES];
 691}
 692
 693- (void)setEnabled:(BOOL)shouldBeEnabled
 694{
 695    [super setEnabled:shouldBeEnabled];
 696
 697    // Set the enabled state of the tokens
 698    for (var i = 0, count = [[self _tokens] count]; i < count; i++)
 699    {
 700        var token = [[self _tokens] objectAtIndex:i];
 701
 702        if ([token respondsToSelector:@selector(setEnabled:)])
 703            [token setEnabled:shouldBeEnabled];
 704    }
 705}
 706
 707- (void)setEditable:(BOOL)shouldBeEditable
 708{
 709    [super setEditable:shouldBeEditable];
 710
 711    [[self _tokens] makeObjectsPerformSelector:@selector(setEditable:) withObject:shouldBeEditable];
 712}
 713
 714- (BOOL)sendAction:(SEL)anAction to:(id)anObject
 715{
 716    _shouldNotifyTarget = NO;
 717    [super sendAction:anAction to:anObject];
 718}
 719
 720// Incredible hack to disable supers implementation
 721// so it cannot change our object value and break the tokenfield
 722- (BOOL)_setStringValue:(CPString)aValue
 723{
 724}
 725
 726// =============
 727// = TEXTFIELD =
 728// =============
 729#if PLATFORM(DOM)
 730- (DOMElement)_inputElement
 731{
 732    if (!CPTokenFieldDOMInputElement)
 733    {
 734        CPTokenFieldDOMInputElement = document.createElement("input");
 735        CPTokenFieldDOMInputElement.style.position = "absolute";
 736        CPTokenFieldDOMInputElement.style.border = "0px";
 737        CPTokenFieldDOMInputElement.style.padding = "0px";
 738        CPTokenFieldDOMInputElement.style.margin = "0px";
 739        CPTokenFieldDOMInputElement.style.whiteSpace = "pre";
 740        CPTokenFieldDOMInputElement.style.background = "transparent";
 741        CPTokenFieldDOMInputElement.style.outline = "none";
 742
 743        CPTokenFieldBlurHandler = function(anEvent)
 744        {
 745            return CPTextFieldBlurFunction(
 746                        anEvent,
 747                        CPTokenFieldInputOwner,
 748                        CPTokenFieldInputOwner ? [CPTokenFieldInputOwner._tokenScrollView documentView]._DOMElement : nil,
 749                        CPTokenFieldDOMInputElement,
 750                        CPTokenFieldInputResigning,
 751                        @ref(CPTokenFieldInputDidBlur));
 752        };
 753
 754        // FIXME make this not onblur
 755        CPTokenFieldDOMInputElement.onblur = CPTokenFieldBlurHandler;
 756
 757        CPTokenFieldDOMStandardInputElement = CPTokenFieldDOMInputElement;
 758    }
 759
 760    if (CPFeatureIsCompatible(CPInputTypeCanBeChangedFeature))
 761    {
 762        if ([CPTokenFieldInputOwner isSecure])
 763            CPTokenFieldDOMInputElement.type = "password";
 764        else
 765            CPTokenFieldDOMInputElement.type = "text";
 766
 767        return CPTokenFieldDOMInputElement;
 768    }
 769
 770    return CPTokenFieldDOMInputElement;
 771}
 772#endif
 773
 774- (CPString)_editorValue
 775{
 776    if (![self hasThemeState:CPThemeStateEditing])
 777        return @"";
 778    return [self _inputElement].value;
 779}
 780
 781- (void)moveUp:(id)sender
 782{
 783    [[self _autocompleteMenu] selectPrevious];
 784    [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
 785}
 786
 787- (void)moveDown:(id)sender
 788{
 789    [[self _autocompleteMenu] selectNext];
 790    [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
 791}
 792
 793- (void)insertNewline:(id)sender
 794{
 795    if ([self hasThemeState:CPThemeStateAutocompleting])
 796    {
 797        [self _autocompleteWithEvent:[CPApp currentEvent]];
 798    }
 799    else
 800    {
 801        [self sendAction:[self action] to:[self target]];
 802        [[self window] makeFirstResponder:nil];
 803    }
 804}
 805
 806- (void)insertTab:(id)sender
 807{
 808    var anEvent = [CPApp currentEvent];
 809    if ([self hasThemeState:CPThemeStateAutocompleting])
 810    {
 811        [self _autocompleteWithEvent:anEvent];
 812    }
 813    else
 814    {
 815        // Default to standard tabbing behaviour.
 816        if (!([anEvent modifierFlags] & CPShiftKeyMask))
 817            [[self window] selectNextKeyView:self];
 818        else
 819            [[self window] selectPreviousKeyView:self];
 820    }
 821}
 822
 823- (void)insertText:(CPString)characters
 824{
 825    // Note that in Cocoa NStokenField uses a hidden input field not accessible to the user,
 826    // so insertText: is called on that field instead. That seems rather silly since it makes
 827    // it pretty much impossible to override insertText:. This version is better.
 828    if ([_tokenizingCharacterSet characterIsMember:[characters substringToIndex:1]])
 829    {
 830        [self _autocompleteWithEvent:[CPApp currentEvent]];
 831    }
 832    else
 833    {
 834        // If you type something while tokens are selected, overwrite them.
 835        if (_selectedRange.length)
 836        {
 837            [self _removeSelectedTokens:self];
 838            // Make sure the editor is placed so it can capture the characters we're overwriting with.
 839            [self layoutSubviews];
 840        }
 841
 842        // If we didn't handle it, allow _propagateCurrentDOMEvent the input field to receive
 843        // the new character.
 844
 845        // This method also allows a subclass to override insertText: to do nothing.
 846        // Unfortunately calling super with some different characters won't work since
 847        // the browser will see the original key event.
 848        [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
 849    }
 850}
 851
 852- (void)cancelOperation:(id)sender
 853{
 854    [self _hideCompletions];
 855}
 856
 857- (void)moveLeft:(id)sender
 858{
 859    // Left arrow
 860    if ((_selectedRange.location > 0 || _selectedRange.length) && [self _editorValue] == "")
 861    {
 862        if (_selectedRange.length)
 863            // Simply collapse the range.
 864            _selectedRange.length = 0;
 865        else
 866            _selectedRange.location--;
 867        [self setNeedsLayout];
 868        _shouldScrollTo = CPScrollDestinationLeft;
 869    }
 870    else
 871    {
 872        // Allow cursor movement within the text field.
 873        [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
 874    }
 875}
 876
 877- (void)moveLeftAndModifySelection:(id)sender
 878{
 879    if (_selectedRange.location > 0 && [self _editorValue] == "")
 880    {
 881        _selectedRange.location--;
 882        // When shift is depressed, select the next token backwards.
 883        _selectedRange.length++;
 884        [self setNeedsLayout];
 885        _shouldScrollTo = CPScrollDestinationLeft;
 886    }
 887    else
 888    {
 889        // Allow cursor movement within the text field.
 890        [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
 891    }
 892}
 893
 894- (void)moveRight:(id)sender
 895{
 896    // Right arrow
 897    if ((_selectedRange.location < [[self _tokens] count] || _selectedRange.length) && [self _editorValue] == "")
 898    {
 899        if (_selectedRange.length)
 900        {
 901            // Place the cursor at the end of the selection and collapse.
 902            _selectedRange.location = CPMaxRange(_selectedRange);
 903            _selectedRange.length = 0;
 904        }
 905        else
 906        {
 907            // Move the cursor forward one token if the input is empty and the right arrow key is pressed.
 908            _selectedRange.location = MIN([[self _tokens] count], _selectedRange.location + _selectedRange.length + 1);
 909        }
 910
 911        [self setNeedsLayout];
 912        _shouldScrollTo = CPScrollDestinationRight;
 913    }
 914    else
 915    {
 916        // Allow cursor movement within the text field.
 917        [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
 918    }
 919}
 920
 921- (void)moveRightAndModifySelection:(id)sender
 922{
 923    if (CPMaxRange(_selectedRange) < [[self _tokens] count] && [self _editorValue] == "")
 924    {
 925        // Leave the selection location in place but include the next token to the right.
 926        _selectedRange.length++;
 927        [self setNeedsLayout];
 928        _shouldScrollTo = CPScrollDestinationRight;
 929    }
 930    else
 931    {
 932        // Allow selection to happen within the text field.
 933        [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
 934    }
 935}
 936
 937- (void)deleteBackward:(id)sender
 938{
 939    // TODO Even if the editor isn't empty you should be able to delete the previous token by placing the cursor
 940    // at the beginning of the editor.
 941    if ([self _editorValue] == @"")
 942    {
 943        [self _hideCompletions];
 944
 945        if (CPEmptyRange(_selectedRange))
 946        {
 947            if (_selectedRange.location > 0)
 948            {
 949                var tokenView = [[self _tokens] objectAtIndex:(_selectedRange.location - 1)];
 950                [self _selectToken:tokenView byExtendingSelection:NO];
 951            }
 952        }
 953        else
 954            [self _removeSelectedTokens:nil];
 955    }
 956    else
 957    {
 958        // Allow deletion to happen within the text field.
 959        [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
 960    }
 961}
 962
 963- (void)deleteForward:(id)sender
 964{
 965    // TODO Even if the editor isn't empty you should be able to delete the next token by placing the cursor
 966    // at the end of the editor.
 967    if ([self _editorValue] == @"")
 968    {
 969        // Delete forward if nothing is selected, else delete all selected.
 970        [self _hideCompletions];
 971
 972        if (CPEmptyRange(_selectedRange))
 973        {
 974            if (_selectedRange.location < [[self _tokens] count])
 975                [self _deleteToken:[[self _tokens] objectAtIndex:[_selectedRange.location]]];
 976        }
 977        else
 978            [self _removeSelectedTokens:nil];
 979    }
 980    else
 981    {
 982        // Allow deletion to happen within the text field.
 983        [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
 984    }
 985}
 986
 987- (void)_selectText:(id)sender immediately:(BOOL)immediately
 988{
 989    // Override CPTextField's version. The correct behaviour is that the text currently being
 990    // edited is turned into a token if possible, or left as plain selected text if not.
 991    // Regardless of if there is on-going text entry, all existing tokens are also selected.
 992    // At this point we don't support having tokens and text selected at the same time (or
 993    // any situation where the cursor isn't within the text being edited) so we just finish
 994    // editing and select all tokens.
 995
 996    if (([self isEditable] || [self isSelectable]))
 997    {
 998        [super _selectText:sender immediately:immediately];
 999
1000        // Finish any editing.
1001        [self _autocomplete];
1002        _selectedRange = CPMakeRange(0, [[self _tokens] count]);
1003
1004        [self setNeedsLayout];
1005    }
1006}
1007
1008- (void)keyDown:(CPEvent)anEvent
1009{
1010#if PLATFORM(DOM)
1011    CPTokenFieldTextDidChangeValue = [self stringValue];
1012#endif
1013
1014    // Leave the default _propagateCurrentDOMEvent setting in place. This might be YES or NO depending
1015    // on if something that could be a browser shortcut was pressed or not, such as Cmd-R to reload.
1016    // If it was NO we want to leave it at NO however and only enable it in insertText:. This is what
1017    // allows a subclass to prevent characters from being inserted by overriding and not calling super.
1018
1019    [self interpretKeyEvents:[anEvent]];
1020
1021    [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
1022}
1023
1024- (void)keyUp:(CPEvent)anEvent
1025{
1026#if PLATFORM(DOM)
1027    if ([self stringValue] !== CPTokenFieldTextDidChangeValue)
1028    {
1029        [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
1030    }
1031#endif
1032
1033    [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
1034}
1035
1036- (void)textDidChange:(CPNotification)aNotification
1037{
1038    if ([aNotification object] !== self)
1039        return;
1040
1041    [super textDidChange:aNotification];
1042
1043    // For future reference: in Cocoa, textDidChange: appears to call [self complete:].
1044    [self _delayedShowCompletions];
1045    // If there was a selection, collapse it now since we're typing in a new token.
1046    _selectedRange.length = 0;
1047
1048    // Force immediate layout in case word wrapping is now necessary.
1049    [self setNeedsLayout];
1050}
1051
1052// - (void)setTokenStyle: (NSTokenStyle) style;
1053// - (NSTokenStyle)tokenStyle;
1054//
1055
1056// ====================
1057// = COMPLETION DELAY =
1058// ====================
1059- (void)setCompletionDelay:(CPTimeInterval)delay
1060{
1061    _completionDelay = delay;
1062}
1063
1064- (CPTimeInterval)completionDelay
1065{
1066    return _completionDelay;
1067}
1068
1069// ==========
1070// = LAYOUT =
1071// ==========
1072- (void)layoutSubviews
1073{
1074    [super layoutSubviews];
1075
1076    [_tokenScrollView setFrame:[self rectForEphemeralSubviewNamed:"content-view"]];
1077
1078    var textFieldContentView = [self layoutEphemeralSubviewNamed:@"content-view"
1079                                                      positioned:CPWindowAbove
1080                                 relativeToEphemeralSubviewNamed:@"bezel-view"];
1081
1082    if (textFieldContentView)
1083        [textFieldContentView setHidden:[self stringValue] !== @""];
1084
1085    var frame = [self frame],
1086        contentView = [_tokenScrollView documentView],
1087        tokens = [self _tokens];
1088
1089    // Hack to make sure we are handling an array
1090    if (![tokens isKindOfClass:[CPArray class]])
1091        return;
1092
1093    // Move each token into the right position.
1094    var contentRect = CGRectMakeCopy([contentView bounds]),
1095        contentOrigin = contentRect.origin,
1096        contentSize = contentRect.size,
1097        offset = CGPointMake(contentOrigin.x, contentOrigin.y),
1098        spaceBetweenTokens = CGSizeMake(2.0, 2.0),
1099        isEditing = [[self window] firstResponder] == self,
1100        tokenToken = [_CPTokenFieldToken new],
1101        font = [self currentValueForThemeAttribute:@"font"],
1102        lineHeight = [font defaultLineHeightForFont],
1103        editorInset = [self currentValueForThemeAttribute:@"editor-inset"];
1104
1105    // Put half a spacing above the tokens.
1106    offset.y += CEIL(spaceBetweenTokens.height / 2.0);
1107
1108    // Get the height of a typical token, or a token token if you will.
1109    [tokenToken sizeToFit];
1110
1111    var tokenHeight = CGRectGetHeight([tokenToken bounds]);
1112
1113    var fitAndFrame = function(width, height)
1114    {
1115        var r = CGRectMake(0, 0, width, height);
1116
1117        if (offset.x + width >= contentSize.width && offset.x > contentOrigin.x)
1118        {
1119            offset.x = contentOrigin.x;
1120            offset.y += height + spaceBetweenTokens.height;
1121        }
1122
1123        r.origin.x = offset.x;
1124        r.origin.y = offset.y;
1125
1126        // Make sure the frame fits.
1127        var scrollHeight = offset.y + tokenHeight + CEIL(spaceBetweenTokens.height / 2.0);
1128        if (CGRectGetHeight([contentView bounds]) < scrollHeight)
1129            [contentView setFrameSize:CGSizeMake(CGRectGetWidth([_tokenScrollView bounds]), scrollHeight)];
1130
1131        offset.x += width + spaceBetweenTokens.width;
1132
1133        return r;
1134    };
1135
1136    var placeEditor = function(useRemainingWidth)
1137    {
1138        var element = [self _inputElement],
1139            textWidth = 1;
1140
1141        if (_selectedRange.length === 0)
1142        {
1143            // XXX The "X" here is used to estimate the space needed to fit the next character
1144            // without clipping. Since different fonts might have different sizes of "X" this
1145            // solution is not ideal, but it works.
1146            textWidth = [(element.value || @"") + "X" sizeWithFont:font].width;
1147
1148            if (useRemainingWidth)
1149                textWidth = MAX(contentSize.width - offset.x - 1, textWidth);
1150        }
1151
1152        _inputFrame = fitAndFrame(textWidth, tokenHeight);
1153
1154        _inputFrame.size.height = lineHeight;
1155
1156        element.style.left = (_inputFrame.origin.x + editorInset.left) + "px";
1157        element.style.top = (_inputFrame.origin.y + editorInset.top) + "px";
1158        element.style.width = _inputFrame.size.width + "px";
1159        element.style.height = _inputFrame.size.height + "px";
1160
1161        // When editing, always scroll to the cursor.
1162        if (_selectedRange.length == 0)
1163            [[_tokenScrollView documentView] scrollPoint:CGPointMake(0, _inputFrame.origin.y)];
1164    };
1165
1166    for (var i = 0, count = [tokens count]; i < count; i++)
1167    {
1168        if (isEditing && !_selectedRange.length && i == CPMaxRange(_selectedRange))
1169            placeEditor(false);
1170
1171        var tokenView = [tokens objectAtIndex:i];
1172
1173        // Make sure we are only changing completed tokens
1174        if ([tokenView isKindOfClass:[CPString class]])
1175            continue;
1176
1177        [tokenView setHighlighted:CPLocationInRange(i, _selectedRange)];
1178        [tokenView sizeToFit];
1179
1180        var size = [contentView bounds].size,
1181            tokenViewSize = [tokenView bounds].size,
1182            tokenFrame = fitAndFrame(tokenViewSize.width, tokenViewSize.height);
1183
1184        [tokenView setFrame:tokenFrame];
1185
1186        [tokenView setButtonType:_buttonType];
1187    }
1188
1189    if (isEditing && !_selectedRange.length && CPMaxRange(_selectedRange) >= [tokens count])
1190        placeEditor(true);
1191
1192    // Hide the editor if there are selected tokens, but still keep it active
1193    // so we can continue using our standard keyboard handling events.
1194    if (isEditing && _selectedRange.length)
1195    {
1196        _inputFrame = nil;
1197        var inputElement = [self _inputElement];
1198        inputElement.style.display = "none";
1199    }
1200    else if (isEditing)
1201    {
1202        var inputElement = [self _inputElement];
1203        inputElement.style.display = "block";
1204        if (document.activeElement !== inputElement)
1205            inputElement.focus();
1206    }
1207
1208    // Trim off any excess height downwards (in case we shrank).
1209    var scrollHeight = offset.y + tokenHeight;
1210    if (CGRectGetHeight([contentView bounds]) > scrollHeight)
1211        [contentView setFrameSize:CGSizeMake(CGRectGetWidth([_tokenScrollView bounds]), scrollHeight)];
1212
1213    if (_shouldScrollTo !== CPScrollDestinationNone)
1214    {
1215        // Only carry out the scroll if the cursor isn't visible.
1216        if (!(isEditing && _selectedRange.length == 0))
1217        {
1218            var scrollToToken = _shouldScrollTo;
1219
1220            if (scrollToToken === CPScrollDestinationLeft)
1221                scrollToToken = tokens[_selectedRange.location]
1222            else if (scrollToToken === CPScrollDestinationRight)
1223                scrollToToken = tokens[MAX(0, CPMaxRange(_selectedRange) - 1)];
1224            [self _scrollTokenViewToVisible:scrollToToken];
1225        }
1226
1227        _shouldScrollTo = CPScrollDestinationNone;
1228    }
1229}
1230
1231- (BOOL)_scrollTokenViewToVisible:(_CPTokenFieldToken)aToken
1232{
1233    if (!aToken)
1234        return;
1235
1236    return [[_tokenScrollView documentView] scrollPoint:CGPointMake(0, [aToken frameOrigin].y)];
1237}
1238
1239@end
1240
1241@implementation CPTokenField (CPTokenFieldDelegate)
1242
1243/*!
1244    Private API to get the delegate tokenField:completionsForSubstring:indexOfToken:indexOfSelectedItem: result.
1245
1246    The delegate method should return an array of strings matching the provided substring for autocompletion.
1247    tokenIndex is the index of the token being completed. selectedIndex allows the selected autocompletion option
1248    to be indicated by reference.
1249
1250    @ignore
1251*/
1252- (CPArray)_completionsForSubstring:(CPString)substring indexOfToken:(int)tokenIndex indexOfSelectedItem:(int)selectedIndex
1253{
1254    if (!(_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_completionsForSubstring_indexOfToken_indexOfSelectedItem_))
1255        return [];
1256
1257    return [_tokenFieldDelegate tokenField:self completionsForSubstring:substring indexOfToken:tokenIndex indexOfSelectedItem:selectedIndex];
1258}
1259
1260/*!
1261    Private API used by the _CPAutocompleteMenu to determine where to place the menu in local coordinates.
1262*/
1263- (CGPoint)_completionOrigin:(_CPAutocompleteMenu)anAutocompleteMenu
1264{
1265    var relativeFrame = _inputFrame ? [[_tokenScrollView documentView] convertRect:_inputFrame toView:self ] : [self bounds];
1266
1267    return CGPointMake(CGRectGetMinX(relativeFrame), CGRectGetMaxY(relativeFrame));
1268}
1269
1270/*!
1271    Private API to get the delegate tokenField:displayStringForRepresentedObject: result.
1272
1273    The delegate method should return a string to be displayed for the given represtented object.
1274    If this delegate method is not implemented, the representedObject is displayed as a string.
1275
1276    @ignore
1277*/
1278- (CPString)_displayStringForRepresentedObject:(id)representedObject
1279{
1280    if (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_displayStringForRepresentedObject_)
1281    {
1282        var stringForRepresentedObject = [_tokenFieldDelegate tokenField:self displayStringForRepresentedObject:representedObject];
1283
1284        if (stringForRepresentedObject !== nil)
1285            return stringForRepresentedObject;
1286    }
1287
1288    return representedObject;
1289}
1290
1291/*!
1292    Private API to get the delegate tokenField:shouldAddObjects:atIndex: result.
1293
1294    The delegate should return an array of represented objects which should be added based on the
1295    suggested tokens to add and the insertion position specified by index. To add no tokens,
1296    return an empty array. Returning nil is an error.
1297
1298    @ignore
1299*/
1300- (CPArray)_shouldAddObjects:(CPArray)tokens atIndex:(int)index
1301{
1302    if (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_shouldAddObjects_atIndex_)
1303    {
1304        var approvedObjects = [_tokenFieldDelegate tokenField:self shouldAddObjects:tokens atIndex:index];
1305
1306        if (approvedObjects !== nil)
1307            return approvedObjects;
1308    }
1309
1310    return tokens;
1311}
1312
1313/*!
1314    Private API to get the delegate tokenField:representedObjectForEditingString: result.
1315
1316    The delegate method should return a represented object for the provided string which
1317    may have been typed by the user or selected from the completion menu. If the method is
1318    not implemented, or returns nil, the string is assumed to be the represented object.
1319
1320    @ignore
1321*/
1322- (id)_representedObjectForEditingString:(CPString)aString
1323{
1324    if (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_representedObjectForEditingString_)
1325    {
1326        var token = [_tokenFieldDelegate tokenField:self representedObjectForEditingString:aString];
1327
1328        if (token !== nil && token !== undefined)
1329            return token;
1330        // If nil was returned, assume the string is the represented object. The alternative would have been
1331        // to not add anything to the object value array for a nil response.
1332    }
1333
1334    return aString;
1335}
1336
1337- (BOOL)_hasMenuForRepresentedObject:(id)aRepresentedObject
1338{
1339    if ((_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_hasMenuForRepresentedObject_) &&
1340        (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_menuForRepresentedObject_))
1341        return [_tokenFieldDelegate tokenField:self hasMenuForRepresentedObject:aRepresentedObject];
1342
1343    return NO;
1344}
1345
1346- (CPMenu)_menuForRepresentedObject:(id)aRepresentedObject
1347{
1348    if ((_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_hasMenuForRepresentedObject_) &&
1349        (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_menuForRepresentedObject_))
1350    {
1351        var hasMenu = [_tokenFieldDelegate tokenField:self hasMenuForRepresentedObject:aRepresentedObject];
1352
1353        if (hasMenu)
1354            return [_tokenFieldDelegate tokenField:self menuForRepresentedObject:aRepresentedObject] || nil;
1355    }
1356
1357    return nil;
1358}
1359
1360// We put the string on the pasteboard before calling this delegate method.
1361// By default, we write the NSStringPboardType as well as an array of NSStrings.
1362// - (BOOL)tokenField:(NSTokenField *)tokenField writeRepresentedObjects:(NSArray *)objects toPasteboard:(NSPasteboard *)pboard;
1363//
1364// Return an array of represented objects to add to the token field.
1365// - (NSArray *)tokenField:(NSTokenField *)tokenField readFromPasteboard:(NSPasteboard *)pboard;
1366//
1367// By default the tokens have no menu.
1368// - (NSMenu *)tokenField:(NSTokenField *)tokenField menuForRepresentedObject:(id)representedObject;
1369// - (BOOL)tokenField:(NSTokenField *)tokenField hasMenuForRepresentedObject:(id)representedObject;
1370//
1371// This method allows you to change the style for individual tokens as well as have mixed text and tokens.
1372// - (NSTokenStyle)tokenField:(NSTokenField *)tokenField styleForRepresentedObject:(id)representedObject;
1373
1374- (void)_delayedShowCompletions
1375{
1376    [[self _autocompleteMenu] _delayedShowCompletions];
1377}
1378
1379- (void)_hideCompletions
1380{
1381    [_autocompleteMenu _hideCompletions];
1382}
1383
1384
1385- (void)setButtonType:(int)aButtonType
1386{
1387    if (_buttonType === aButtonType)
1388        return;
1389
1390    _buttonType = aButtonType;
1391    [self setNeedsLayout];
1392}
1393
1394@end
1395
1396@implementation _CPTokenFieldToken : CPTextField
1397{
1398    _CPTokenFieldTokenCloseButton       _deleteButton;
1399    _CPTokenFieldTokenDisclosureButton  _disclosureButton;
1400    CPTokenField                        _tokenField;
1401    id                                  _representedObject;
1402    int                                 _buttonType;
1403}
1404
1405+ (CPString)defaultThemeClass
1406{
1407    return "tokenfield-token";
1408}
1409
1410- (BOOL)acceptsFirstResponder
1411{
1412    return NO;
1413}
1414
1415- (id)initWithFrame:(CGRect)frame
1416{
1417    if (self = [super initWithFrame:frame])
1418    {
1419        [self setEditable:NO];
1420        [self setHighlighted:NO];
1421        [self setBezeled:YES];
1422        [self setButtonType:CPTokenFieldDisclosureButtonType];
1423    }
1424
1425    return self;
1426}
1427
1428- (CPTokenField)tokenField
1429{
1430    return _tokenField;
1431}
1432
1433- (void)setTokenField:(CPTokenField)tokenField
1434{
1435    _tokenField = tokenField;
1436}
1437
1438- (id)representedObject
1439{
1440    return _representedObject;
1441}
1442
1443- (void)setRepresentedObject:(id)representedObject
1444{
1445    _representedObject = representedObject;
1446    [self setNeedsLayout];
1447}
1448
1449- (void)setEditable:(BOOL)shouldBeEditable
1450{
1451    [super setEditable:shouldBeEditable];
1452    [self setNeedsLayout];
1453}
1454
1455- (BOOL)setThemeState:(ThemeState)aState
1456{
1457   if (aState.isa && [aState isKindOfClass:CPArray])
1458        aState = CPThemeState.apply(null, aState);
1459
1460    var r = [super setThemeState:aState];
1461
1462    // Share hover state with the disclosure and delete buttons.
1463    if (aState.hasThemeState(CPThemeStateHovered))
1464    {
1465        [_disclosureButton setThemeState:CPThemeStateHovered];
1466        [_deleteButton setThemeState:CPThemeStateHovered];
1467    }
1468
1469    return r;
1470}
1471
1472- (BOOL)unsetThemeState:(ThemeState)aState
1473{
1474   if (aState.isa && [aState isKindOfClass:CPArray])
1475        aState = CPThemeState.apply(null, aState);
1476
1477    var r = [super unsetThemeState:aState];
1478
1479    // Share hover state with the disclosure and delete button.
1480    if (aState.hasThemeState(CPThemeStateHovered))
1481    {
1482        [_disclosureButton unsetThemeState:CPThemeStateHovered];
1483        [_deleteButton unsetThemeState:CPThemeStateHovered];
1484    }
1485
1486    return r;
1487}
1488
1489- (CGSize)_minimumFrameSize
1490{
1491    var size = CGSizeMakeZero(),
1492        minSize = [self currentValueForThemeAttribute:@"min-size"],
1493        contentInset = [self currentValueForThemeAttribute:@"content-inset"];
1494
1495    // Tokens are fixed height, so we could as well have used max-size here.
1496    size.height = minSize.height;
1497    size.width = MAX(minSize.width, [([self stringValue] || @" ") sizeWithFont:[self font]].width + contentInset.left + contentInset.right);
1498
1499    return size;
1500}
1501
1502- (void)setButtonType:(int)aButtonType
1503{
1504    if (_buttonType === aButtonType)
1505        return;
1506
1507    _buttonType = aButtonType;
1508
1509    if (_buttonType === CPTokenFieldDisclosureButtonType)
1510    {
1511        if (_deleteButton)
1512        {
1513            [_deleteButton removeFromSuperview];
1514            _deleteButton = nil;
1515        }
1516
1517        if (!_disclosureButton)
1518        {
1519            _disclosureButton = [[_CPTokenFieldTokenDisclosureButton alloc] initWithFrame:CGRectMakeZero()];
1520            [self addSubview:_disclosureButton];
1521        }
1522    }
1523    else
1524    {
1525        if (_disclosureButton)
1526        {
1527            [_disclosureButton removeFromSuperview];
1528            _disclosureButton = nil;
1529        }
1530
1531        if (!_deleteButton)
1532        {
1533            _deleteButton = [[_CPTokenFieldTokenCloseButton alloc] initWithFrame:CGRectMakeZero()];
1534            [self addSubview:_deleteButton];
1535            [_deleteButton setTarget:self];
1536            [_deleteButton setAction:@selector(_delete:)];
1537        }
1538    }
1539
1540    [self setNeedsLayout];
1541}
1542
1543- (void)layoutSubviews
1544{
1545    [super layoutSubviews];
1546
1547    var bezelView = [self layoutEphemeralSubviewNamed:@"bezel-view"
1548                                           positioned:CPWindowBelow
1549                      relativeToEphemeralSubviewNamed:@"content-view"];
1550
1551    if (bezelView && _tokenField)
1552    {
1553        switch (_buttonType)
1554        {
1555            case CPTokenFieldDisclosureButtonType:
1556                var shouldBeEnabled = [self hasMenu];
1557                [_disclosureButton setHidden:!shouldBeEnabled];
1558
1559                if (shouldBeEnabled)
1560                    [_disclosureButton setMenu:[self menu]];
1561
1562                var frame = [bezelView frame],
1563                    buttonOffset = [_disclosureButton currentValueForThemeAttribute:@"offset"],
1564                    buttonSize = [_disclosureButton currentValueForThemeAttribute:@"min-size"];
1565
1566                [_disclosureButton setFrame:CGRectMake(CGRectGetMaxX(frame) - buttonOffset.x, CGRectGetMinY(frame) + buttonOffset.y, buttonSize.width, buttonSize.height)];
1567                break;
1568            case CPTokenFieldDeleteButtonType:
1569                [_deleteButton setEnabled:[self isEditable] && [self isEnabl…

Large files files are truncated, but you can click here to view the full file