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