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