API  1.0.0
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
32 @protocol CPTokenFieldDelegate <CPObject>
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  [[CPRunLoop mainRunLoop] performBlock:function()
439  {
440  [_tokenScrollView documentView]._DOMElement.appendChild(element);
441 
442  //post CPControlTextDidBeginEditingNotification
443  [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
444 
445  [[CPRunLoop mainRunLoop] performBlock:function()
446  {
447  // This will prevent to jump to the focused element
448  var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin];
449 
450  element.focus();
451 
452  [self _restorePreviousScrollingOrigin:previousScrollingOrigin];
453 
454  CPTokenFieldInputOwner = self;
455  } argument:nil order:0 modes:[CPDefaultRunLoopMode]];
456 
457  [self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]];
458  } argument:nil order:0 modes:[CPDefaultRunLoopMode]];
459 
460  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
461 
462  CPTokenFieldInputIsActive = YES;
463 
464  if (document.attachEvent)
465  {
466  CPTokenFieldCachedSelectStartFunction = document.body.onselectstart;
467  CPTokenFieldCachedDragFunction = document.body.ondrag;
468 
469  document.body.ondrag = function () {};
470  document.body.onselectstart = function () {};
471  }
472 
473 #endif
474 
475  return YES;
476 }
477 
478 - (BOOL)resignFirstResponder
479 {
480  [self _autocomplete];
481 
482  // From CPTextField superclass.
483  [self _setObserveWindowKeyNotifications:NO];
484 
485  [self _resignFirstKeyResponder];
486 
487  if (_shouldNotifyTarget)
488  {
489  _shouldNotifyTarget = NO;
490  [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidEndEditingNotification object:self userInfo:@{"CPTextMovement": [self _currentTextMovement]}]];
491 
492  if ([self sendsActionOnEndEditing])
493  [self sendAction:[self action] to:[self target]];
494  }
495 
496  [self textDidBlur:[CPNotification notificationWithName:CPTextFieldDidBlurNotification object:self userInfo:nil]];
497 
498  return YES;
499 }
500 
501 - (void)_resignFirstKeyResponder
502 {
503  [self unsetThemeState:CPThemeStateEditing];
504 
505  [self _updatePlaceholderState];
506  [self setNeedsLayout];
507 
508 #if PLATFORM(DOM)
509 
510  var element = [self _inputElement];
511 
512  CPTokenFieldInputResigning = YES;
513 
514  if (CPTokenFieldInputIsActive)
515  element.blur();
516 
517  if (!CPTokenFieldInputDidBlur)
518  CPTokenFieldBlurHandler();
519 
520  if (element.parentNode == [_tokenScrollView documentView]._DOMElement)
521  element.parentNode.removeChild(element);
522 
523  // Previosly, we unflagged CPTokenFieldInputDidBlur and CPTokenFieldInputResigning before
524  // the call to removeChild. This may result in DOM exceptions in Chrome under certain conditions.
525  // See https://stackoverflow.com/questions/21926083/failed-to-execute-removechild-on-node
526  // for why we need to unflag CPTokenFieldInputDidBlur and CPTokenFieldInputResigning
527  // only after removing the element.
528 
529  CPTokenFieldInputDidBlur = NO;
530  CPTokenFieldInputResigning = NO;
531 
532  CPTokenFieldInputIsActive = NO;
533 
534  if (document.attachEvent)
535  {
536  CPTokenFieldCachedSelectStartFunction = nil;
537  CPTokenFieldCachedDragFunction = nil;
538 
539  document.body.ondrag = CPTokenFieldCachedDragFunction;
540  document.body.onselectstart = CPTokenFieldCachedSelectStartFunction
541  }
542 
543 #endif
544 }
545 
546 - (void)mouseDown:(CPEvent)anEvent
547 {
548  _mouseDownEvent = anEvent;
549 
550  [self _selectToken:nil byExtendingSelection:NO];
551 
552  [super mouseDown:anEvent];
553 }
554 
555 - (void)mouseUp:(CPEvent)anEvent
556 {
557  _mouseDownEvent = nil;
558 }
559 
560 - (void)_mouseDownOnToken:(_CPTokenFieldToken)aToken withEvent:(CPEvent)anEvent
561 {
562  _mouseDownEvent = anEvent;
563 }
564 
565 - (void)_mouseUpOnToken:(_CPTokenFieldToken)aToken withEvent:(CPEvent)anEvent
566 {
567  if (_mouseDownEvent && CGPointEqualToPoint([_mouseDownEvent locationInWindow], [anEvent locationInWindow]))
568  {
569  [self _selectToken:aToken byExtendingSelection:[anEvent modifierFlags] & CPShiftKeyMask];
570  [[self window] makeFirstResponder:self];
571  // Snap to the token if it's only half visible due to mouse wheel scrolling.
572  _shouldScrollTo = aToken;
573  }
574 }
575 
576 // ===========
577 // = CONTROL =
578 // ===========
579 - (CPArray)_tokens
580 {
581  // We return super here because objectValue uses this method
582  // If we called self we would loop infinitely
583  return [super objectValue];
584 }
585 
586 - (CPString)stringValue
587 {
588  return [[self objectValue] componentsJoinedByString:@","];
589 }
590 
591 - (id)objectValue
592 {
593  var objectValue = [];
594 
595  for (var i = 0, count = [[self _tokens] count]; i < count; i++)
596  {
597  var token = [[self _tokens] objectAtIndex:i];
598 
599  if ([token isKindOfClass:[CPString class]])
600  continue;
601 
602  [objectValue addObject:[token representedObject]];
603  }
604 
605 #if PLATFORM(DOM)
606 
607  if ([self _editorValue])
608  {
609  var token = [self _representedObjectForEditingString:[self _editorValue]];
610  [objectValue insertObject:token atIndex:_selectedRange.location];
611  }
612 
613 #endif
614 
615  return objectValue;
616 }
617 
618 - (void)setObjectValue:(id)aValue
619 {
620  if (aValue !== nil && ![aValue isKindOfClass:[CPArray class]])
621  {
622  [super setObjectValue:nil];
623  return;
624  }
625 
626  var superValue = [super objectValue];
627  if (aValue === superValue || [aValue isEqualToArray:superValue])
628  return;
629 
630  var contentView = [_tokenScrollView documentView],
631  oldTokens = [self _tokens],
632  newTokens = [];
633 
634  // Preserve as many existing tokens as possible to reduce redraw flickering.
635  if (aValue !== nil)
636  {
637  for (var i = 0, count = [aValue count]; i < count; i++)
638  {
639  // Do we have this token among the old ones?
640  var tokenObject = aValue[i],
641  tokenValue = [self _displayStringForRepresentedObject:tokenObject],
642  newToken = nil;
643 
644  for (var j = 0, oldCount = [oldTokens count]; j < oldCount; j++)
645  {
646  var oldToken = oldTokens[j];
647  if ([oldToken representedObject] == tokenObject)
648  {
649  // Yep. Reuse it.
650  [oldTokens removeObjectAtIndex:j];
651  newToken = oldToken;
652  break;
653  }
654  }
655 
656  if (newToken === nil)
657  {
658  newToken = [_CPTokenFieldToken new];
659  [newToken setTokenField:self];
660  [newToken setRepresentedObject:tokenObject];
661  [newToken setStringValue:tokenValue];
662  [newToken setEditable:[self isEditable]];
663  [contentView addSubview:newToken];
664  }
665 
666  newTokens.push(newToken);
667  }
668  }
669 
670  // Remove any now unused tokens.
671  for (var j = 0, oldCount = [oldTokens count]; j < oldCount; j++)
672  [oldTokens[j] removeFromSuperview];
673 
674  /*
675  [CPTextField setObjectValue] will try to set the _inputElement.value to
676  the new objectValue, if the _inputElement exists. This is wrong for us
677  since our objectValue is an array of tokens, so we can't use
678  [super setObjectValue:objectValue];
679 
680  Instead do what CPControl setObjectValue would.
681  */
682  _value = newTokens;
683 
684  // Reset the selection.
685  [self _selectToken:nil byExtendingSelection:NO];
686 
687  [self _updatePlaceholderState];
688 
689  _shouldScrollTo = CPScrollDestinationRight;
690  [self setNeedsLayout];
691  [self setNeedsDisplay:YES];
692 }
693 
694 - (void)setEnabled:(BOOL)shouldBeEnabled
695 {
696  [super setEnabled:shouldBeEnabled];
697 
698  // Set the enabled state of the tokens
699  for (var i = 0, count = [[self _tokens] count]; i < count; i++)
700  {
701  var token = [[self _tokens] objectAtIndex:i];
702 
703  if ([token respondsToSelector:@selector(setEnabled:)])
704  [token setEnabled:shouldBeEnabled];
705  }
706 }
707 
708 - (void)setEditable:(BOOL)shouldBeEditable
709 {
710  [super setEditable:shouldBeEditable];
711 
712  [[self _tokens] makeObjectsPerformSelector:@selector(setEditable:) withObject:shouldBeEditable];
713 }
714 
715 - (BOOL)sendAction:(SEL)anAction to:(id)anObject
716 {
717  _shouldNotifyTarget = NO;
718  [super sendAction:anAction to:anObject];
719 }
720 
721 // Incredible hack to disable supers implementation
722 // so it cannot change our object value and break the tokenfield
723 - (BOOL)_setStringValue:(CPString)aValue
724 {
725 }
726 
727 // =============
728 // = TEXTFIELD =
729 // =============
730 #if PLATFORM(DOM)
731 - (DOMElement)_inputElement
732 {
733  if (!CPTokenFieldDOMInputElement)
734  {
735  CPTokenFieldDOMInputElement = document.createElement("input");
736  CPTokenFieldDOMInputElement.style.position = "absolute";
737  CPTokenFieldDOMInputElement.style.border = "0px";
738  CPTokenFieldDOMInputElement.style.padding = "0px";
739  CPTokenFieldDOMInputElement.style.margin = "0px";
740  CPTokenFieldDOMInputElement.style.whiteSpace = "pre";
741  CPTokenFieldDOMInputElement.style.background = "transparent";
742  CPTokenFieldDOMInputElement.style.outline = "none";
743 
744  CPTokenFieldBlurHandler = function(anEvent)
745  {
747  anEvent,
748  CPTokenFieldInputOwner,
749  CPTokenFieldInputOwner ? [CPTokenFieldInputOwner._tokenScrollView documentView]._DOMElement : nil,
750  CPTokenFieldDOMInputElement,
751  CPTokenFieldInputResigning,
752  @ref(CPTokenFieldInputDidBlur));
753  };
754 
755  // FIXME make this not onblur
756  CPTokenFieldDOMInputElement.onblur = CPTokenFieldBlurHandler;
757 
758  CPTokenFieldDOMStandardInputElement = CPTokenFieldDOMInputElement;
759  }
760 
762  {
763  if ([CPTokenFieldInputOwner isSecure])
764  CPTokenFieldDOMInputElement.type = "password";
765  else
766  CPTokenFieldDOMInputElement.type = "text";
767 
768  return CPTokenFieldDOMInputElement;
769  }
770 
771  return CPTokenFieldDOMInputElement;
772 }
773 #endif
774 
775 - (CPString)_editorValue
776 {
777  if (![self hasThemeState:CPThemeStateEditing])
778  return @"";
779  return [self _inputElement].value;
780 }
781 
782 - (void)moveUp:(id)sender
783 {
784  [[self _autocompleteMenu] selectPrevious];
785  [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
786 }
787 
788 - (void)moveDown:(id)sender
789 {
790  [[self _autocompleteMenu] selectNext];
791  [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
792 }
793 
794 - (void)insertNewline:(id)sender
795 {
796  if ([self hasThemeState:CPThemeStateAutocompleting])
797  {
798  [self _autocompleteWithEvent:[CPApp currentEvent]];
799  }
800  else
801  {
802  [self sendAction:[self action] to:[self target]];
803  [[self window] makeFirstResponder:nil];
804  }
805 }
806 
807 - (void)insertTab:(id)sender
808 {
809  var anEvent = [CPApp currentEvent];
810  if ([self hasThemeState:CPThemeStateAutocompleting])
811  {
812  [self _autocompleteWithEvent:anEvent];
813  }
814  else
815  {
816  // Default to standard tabbing behaviour.
817  if (!([anEvent modifierFlags] & CPShiftKeyMask))
818  [[self window] selectNextKeyView:self];
819  else
820  [[self window] selectPreviousKeyView:self];
821  }
822 }
823 
824 - (void)insertText:(CPString)characters
825 {
826  // Note that in Cocoa NStokenField uses a hidden input field not accessible to the user,
827  // so insertText: is called on that field instead. That seems rather silly since it makes
828  // it pretty much impossible to override insertText:. This version is better.
829  if ([_tokenizingCharacterSet characterIsMember:[characters substringToIndex:1]])
830  {
831  [self _autocompleteWithEvent:[CPApp currentEvent]];
832  }
833  else
834  {
835  // If you type something while tokens are selected, overwrite them.
836  if (_selectedRange.length)
837  {
838  [self _removeSelectedTokens:self];
839  // Make sure the editor is placed so it can capture the characters we're overwriting with.
840  [self layoutSubviews];
841  }
842 
843  // If we didn't handle it, allow _propagateCurrentDOMEvent the input field to receive
844  // the new character.
845 
846  // This method also allows a subclass to override insertText: to do nothing.
847  // Unfortunately calling super with some different characters won't work since
848  // the browser will see the original key event.
849  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
850  }
851 }
852 
853 - (void)cancelOperation:(id)sender
854 {
855  [self _hideCompletions];
856 }
857 
858 - (void)moveLeft:(id)sender
859 {
860  // Left arrow
861  if ((_selectedRange.location > 0 || _selectedRange.length) && [self _editorValue] == "")
862  {
863  if (_selectedRange.length)
864  // Simply collapse the range.
865  _selectedRange.length = 0;
866  else
867  _selectedRange.location--;
868  [self setNeedsLayout];
869  _shouldScrollTo = CPScrollDestinationLeft;
870  }
871  else
872  {
873  // Allow cursor movement within the text field.
874  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
875  }
876 }
877 
878 - (void)moveLeftAndModifySelection:(id)sender
879 {
880  if (_selectedRange.location > 0 && [self _editorValue] == "")
881  {
882  _selectedRange.location--;
883  // When shift is depressed, select the next token backwards.
884  _selectedRange.length++;
885  [self setNeedsLayout];
886  _shouldScrollTo = CPScrollDestinationLeft;
887  }
888  else
889  {
890  // Allow cursor movement within the text field.
891  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
892  }
893 }
894 
895 - (void)moveRight:(id)sender
896 {
897  // Right arrow
898  if ((_selectedRange.location < [[self _tokens] count] || _selectedRange.length) && [self _editorValue] == "")
899  {
900  if (_selectedRange.length)
901  {
902  // Place the cursor at the end of the selection and collapse.
903  _selectedRange.location = CPMaxRange(_selectedRange);
904  _selectedRange.length = 0;
905  }
906  else
907  {
908  // Move the cursor forward one token if the input is empty and the right arrow key is pressed.
909  _selectedRange.location = MIN([[self _tokens] count], _selectedRange.location + _selectedRange.length + 1);
910  }
911 
912  [self setNeedsLayout];
913  _shouldScrollTo = CPScrollDestinationRight;
914  }
915  else
916  {
917  // Allow cursor movement within the text field.
918  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
919  }
920 }
921 
922 - (void)moveRightAndModifySelection:(id)sender
923 {
924  if (CPMaxRange(_selectedRange) < [[self _tokens] count] && [self _editorValue] == "")
925  {
926  // Leave the selection location in place but include the next token to the right.
927  _selectedRange.length++;
928  [self setNeedsLayout];
929  _shouldScrollTo = CPScrollDestinationRight;
930  }
931  else
932  {
933  // Allow selection to happen within the text field.
934  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
935  }
936 }
937 
938 - (void)deleteBackward:(id)sender
939 {
940  // TODO Even if the editor isn't empty you should be able to delete the previous token by placing the cursor
941  // at the beginning of the editor.
942  if ([self _editorValue] == @"")
943  {
944  [self _hideCompletions];
945 
946  if (CPEmptyRange(_selectedRange))
947  {
948  if (_selectedRange.location > 0)
949  {
950  var tokenView = [[self _tokens] objectAtIndex:(_selectedRange.location - 1)];
951  [self _selectToken:tokenView byExtendingSelection:NO];
952  }
953  }
954  else
955  [self _removeSelectedTokens:nil];
956  }
957  else
958  {
959  // Allow deletion to happen within the text field.
960  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
961  }
962 }
963 
964 - (void)deleteForward:(id)sender
965 {
966  // TODO Even if the editor isn't empty you should be able to delete the next token by placing the cursor
967  // at the end of the editor.
968  if ([self _editorValue] == @"")
969  {
970  // Delete forward if nothing is selected, else delete all selected.
971  [self _hideCompletions];
972 
973  if (CPEmptyRange(_selectedRange))
974  {
975  if (_selectedRange.location < [[self _tokens] count])
976  [self _deleteToken:[[self _tokens] objectAtIndex:[_selectedRange.location]]];
977  }
978  else
979  [self _removeSelectedTokens:nil];
980  }
981  else
982  {
983  // Allow deletion to happen within the text field.
984  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
985  }
986 }
987 
988 - (void)_selectText:(id)sender immediately:(BOOL)immediately
989 {
990  // Override CPTextField's version. The correct behaviour is that the text currently being
991  // edited is turned into a token if possible, or left as plain selected text if not.
992  // Regardless of if there is on-going text entry, all existing tokens are also selected.
993  // At this point we don't support having tokens and text selected at the same time (or
994  // any situation where the cursor isn't within the text being edited) so we just finish
995  // editing and select all tokens.
996 
997  if (([self isEditable] || [self isSelectable]))
998  {
999  [super _selectText:sender immediately:immediately];
1000 
1001  // Finish any editing.
1002  [self _autocomplete];
1003  _selectedRange = CPMakeRange(0, [[self _tokens] count]);
1004 
1005  [self setNeedsLayout];
1006  }
1007 }
1008 
1009 - (void)keyDown:(CPEvent)anEvent
1010 {
1011 #if PLATFORM(DOM)
1012  CPTokenFieldTextDidChangeValue = [self stringValue];
1013 #endif
1014 
1015  // Leave the default _propagateCurrentDOMEvent setting in place. This might be YES or NO depending
1016  // on if something that could be a browser shortcut was pressed or not, such as Cmd-R to reload.
1017  // If it was NO we want to leave it at NO however and only enable it in insertText:. This is what
1018  // allows a subclass to prevent characters from being inserted by overriding and not calling super.
1019 
1020  [self interpretKeyEvents:[anEvent]];
1021 
1022  [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
1023 }
1024 
1025 - (void)keyUp:(CPEvent)anEvent
1026 {
1027 #if PLATFORM(DOM)
1028  if ([self stringValue] !== CPTokenFieldTextDidChangeValue)
1029  {
1030  [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
1031  }
1032 #endif
1033 
1034  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
1035 }
1036 
1037 - (BOOL)performKeyEquivalent:(CPEvent)anEvent
1038 {
1039  var characters = [anEvent characters];
1040 
1041  // Here we handle the event when getting a CPNewlineCharacter or CPCarriageReturnCharacter when the menu is open
1042  // We don't want that the application dispatches the event to the other controls
1043  if ([self hasThemeState:CPThemeStateAutocompleting] && (characters === CPNewlineCharacter || characters === CPCarriageReturnCharacter))
1044  {
1045  [self keyDown:anEvent];
1046  return YES;
1047  }
1048 
1049  return [super performKeyEquivalent:anEvent];
1050 }
1051 
1052 - (void)textDidChange:(CPNotification)aNotification
1053 {
1054  if ([aNotification object] !== self)
1055  return;
1056 
1057  [super textDidChange:aNotification];
1058 
1059  // For future reference: in Cocoa, textDidChange: appears to call [self complete:].
1060  [self _delayedShowCompletions];
1061  // If there was a selection, collapse it now since we're typing in a new token.
1062  _selectedRange.length = 0;
1063 
1064  // Force immediate layout in case word wrapping is now necessary.
1065  [self setNeedsLayout];
1066 }
1067 
1068 // - (void)setTokenStyle: (NSTokenStyle) style;
1069 // - (NSTokenStyle)tokenStyle;
1070 //
1071 
1072 // ====================
1073 // = COMPLETION DELAY =
1074 // ====================
1075 - (void)setCompletionDelay:(CPTimeInterval)delay
1076 {
1077  _completionDelay = delay;
1078 }
1079 
1080 - (CPTimeInterval)completionDelay
1081 {
1082  return _completionDelay;
1083 }
1084 
1085 // ==========
1086 // = LAYOUT =
1087 // ==========
1088 - (void)layoutSubviews
1089 {
1090  [super layoutSubviews];
1091 
1092  [_tokenScrollView setFrame:[self rectForEphemeralSubviewNamed:"content-view"]];
1093 
1094  var textFieldContentView = [self layoutEphemeralSubviewNamed:@"content-view"
1095  positioned:CPWindowAbove
1096  relativeToEphemeralSubviewNamed:@"bezel-view"];
1097 
1098  if (textFieldContentView)
1099  [textFieldContentView setHidden:[self stringValue] !== @""];
1100 
1101  var frame = [self frame],
1102  contentView = [_tokenScrollView documentView],
1103  tokens = [self _tokens];
1104 
1105  // Hack to make sure we are handling an array
1106  if (![tokens isKindOfClass:[CPArray class]])
1107  return;
1108 
1109  // Move each token into the right position.
1110  var contentRect = CGRectMakeCopy([contentView bounds]),
1111  contentOrigin = contentRect.origin,
1112  contentSize = contentRect.size,
1113  offset = CGPointMake(contentOrigin.x, contentOrigin.y),
1114  spaceBetweenTokens = CGSizeMake(2.0, 2.0),
1115  isEditing = [[self window] firstResponder] == self,
1116  tokenToken = [_CPTokenFieldToken new],
1117  font = [self currentValueForThemeAttribute:@"font"],
1118  lineHeight = [font defaultLineHeightForFont],
1119  editorInset = [self currentValueForThemeAttribute:@"editor-inset"];
1120 
1121  // Put half a spacing above the tokens.
1122  offset.y += CEIL(spaceBetweenTokens.height / 2.0);
1123 
1124  // Get the height of a typical token, or a token token if you will.
1125  [tokenToken sizeToFit];
1126 
1127  var tokenHeight = CGRectGetHeight([tokenToken bounds]);
1128 
1129  var fitAndFrame = function(width, height)
1130  {
1131  var r = CGRectMake(0, 0, width, height);
1132 
1133  if (offset.x + width >= contentSize.width && offset.x > contentOrigin.x)
1134  {
1135  offset.x = contentOrigin.x;
1136  offset.y += height + spaceBetweenTokens.height;
1137  }
1138 
1139  r.origin.x = offset.x;
1140  r.origin.y = offset.y;
1141 
1142  // Make sure the frame fits.
1143  var scrollHeight = offset.y + tokenHeight + CEIL(spaceBetweenTokens.height / 2.0);
1144  if (CGRectGetHeight([contentView bounds]) < scrollHeight)
1145  [contentView setFrameSize:CGSizeMake(CGRectGetWidth([_tokenScrollView bounds]), scrollHeight)];
1146 
1147  offset.x += width + spaceBetweenTokens.width;
1148 
1149  return r;
1150  };
1151 
1152  var placeEditor = function(useRemainingWidth)
1153  {
1154  var element = [self _inputElement],
1155  textWidth = 1;
1156 
1157  if (_selectedRange.length === 0)
1158  {
1159  // XXX The "X" here is used to estimate the space needed to fit the next character
1160  // without clipping. Since different fonts might have different sizes of "X" this
1161  // solution is not ideal, but it works.
1162  textWidth = [(element.value || @"") + "X" sizeWithFont:font].width;
1163 
1164  if (useRemainingWidth)
1165  textWidth = MAX(contentSize.width - offset.x - 1, textWidth);
1166  }
1167 
1168  _inputFrame = fitAndFrame(textWidth, tokenHeight);
1169 
1170  _inputFrame.size.height = lineHeight;
1171 
1172  element.style.left = (_inputFrame.origin.x + editorInset.left) + "px";
1173  element.style.top = (_inputFrame.origin.y + editorInset.top) + "px";
1174  element.style.width = _inputFrame.size.width + "px";
1175  element.style.height = _inputFrame.size.height + "px";
1176 
1177  // When editing, always scroll to the cursor.
1178  if (_selectedRange.length == 0)
1179  [[_tokenScrollView documentView] scrollPoint:CGPointMake(0, _inputFrame.origin.y)];
1180  };
1181 
1182  for (var i = 0, count = [tokens count]; i < count; i++)
1183  {
1184  if (isEditing && !_selectedRange.length && i == CPMaxRange(_selectedRange))
1185  placeEditor(false);
1186 
1187  var tokenView = [tokens objectAtIndex:i];
1188 
1189  // Make sure we are only changing completed tokens
1190  if ([tokenView isKindOfClass:[CPString class]])
1191  continue;
1192 
1193  [tokenView setHighlighted:CPLocationInRange(i, _selectedRange)];
1194  [tokenView sizeToFit];
1195 
1196  var size = [contentView bounds].size,
1197  tokenViewSize = [tokenView bounds].size,
1198  tokenFrame = fitAndFrame(tokenViewSize.width, tokenViewSize.height);
1199 
1200  [tokenView setFrame:tokenFrame];
1201 
1202  [tokenView setButtonType:_buttonType];
1203  }
1204 
1205  if (isEditing && !_selectedRange.length && CPMaxRange(_selectedRange) >= [tokens count])
1206  placeEditor(true);
1207 
1208  // Hide the editor if there are selected tokens, but still keep it active
1209  // so we can continue using our standard keyboard handling events.
1210  if (isEditing && _selectedRange.length)
1211  {
1212  _inputFrame = nil;
1213  var inputElement = [self _inputElement];
1214  inputElement.style.display = "none";
1215  }
1216  else if (isEditing)
1217  {
1218  var inputElement = [self _inputElement];
1219  inputElement.style.display = "block";
1220  if (document.activeElement !== inputElement)
1221  inputElement.focus();
1222  }
1223 
1224  // Trim off any excess height downwards (in case we shrank).
1225  var scrollHeight = offset.y + tokenHeight;
1226  if (CGRectGetHeight([contentView bounds]) > scrollHeight)
1227  [contentView setFrameSize:CGSizeMake(CGRectGetWidth([_tokenScrollView bounds]), scrollHeight)];
1228 
1229  if (_shouldScrollTo !== CPScrollDestinationNone)
1230  {
1231  // Only carry out the scroll if the cursor isn't visible.
1232  if (!(isEditing && _selectedRange.length == 0))
1233  {
1234  var scrollToToken = _shouldScrollTo;
1235 
1236  if (scrollToToken === CPScrollDestinationLeft)
1237  scrollToToken = tokens[_selectedRange.location];
1238  else if (scrollToToken === CPScrollDestinationRight)
1239  scrollToToken = tokens[MAX(0, CPMaxRange(_selectedRange) - 1)];
1240  [self _scrollTokenViewToVisible:scrollToToken];
1241  }
1242 
1243  _shouldScrollTo = CPScrollDestinationNone;
1244  }
1245 }
1246 
1247 - (BOOL)_scrollTokenViewToVisible:(_CPTokenFieldToken)aToken
1248 {
1249  if (!aToken)
1250  return;
1251 
1252  return [[_tokenScrollView documentView] scrollPoint:CGPointMake(0, [aToken frameOrigin].y)];
1253 }
1254 
1255 @end
1256 
1258 
1268 - (CPArray)_completionsForSubstring:(CPString)substring indexOfToken:(int)tokenIndex indexOfSelectedItem:(int)selectedIndex
1269 {
1271  return [];
1272 
1273  return [_tokenFieldDelegate tokenField:self completionsForSubstring:substring indexOfToken:tokenIndex indexOfSelectedItem:selectedIndex];
1274 }
1275 
1279 - (CGPoint)_completionOrigin:(_CPAutocompleteMenu)anAutocompleteMenu
1280 {
1281  var relativeFrame = _inputFrame ? [[_tokenScrollView documentView] convertRect:_inputFrame toView:self ] : [self bounds];
1282 
1283  return CGPointMake(CGRectGetMinX(relativeFrame), CGRectGetMaxY(relativeFrame));
1284 }
1285 
1294 - (CPString)_displayStringForRepresentedObject:(id)representedObject
1295 {
1296  if (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_displayStringForRepresentedObject_)
1297  {
1298  var stringForRepresentedObject = [_tokenFieldDelegate tokenField:self displayStringForRepresentedObject:representedObject];
1299 
1300  if (stringForRepresentedObject !== nil)
1301  return stringForRepresentedObject;
1302  }
1303 
1304  return representedObject;
1305 }
1306 
1316 - (CPArray)_shouldAddObjects:(CPArray)tokens atIndex:(int)index
1317 {
1318  if (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_shouldAddObjects_atIndex_)
1319  {
1320  var approvedObjects = [_tokenFieldDelegate tokenField:self shouldAddObjects:tokens atIndex:index];
1321 
1322  if (approvedObjects !== nil)
1323  return approvedObjects;
1324  }
1325 
1326  return tokens;
1327 }
1328 
1338 - (id)_representedObjectForEditingString:(CPString)aString
1339 {
1340  if (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_representedObjectForEditingString_)
1341  {
1342  var token = [_tokenFieldDelegate tokenField:self representedObjectForEditingString:aString];
1343 
1344  if (token !== nil && token !== undefined)
1345  return token;
1346  // If nil was returned, assume the string is the represented object. The alternative would have been
1347  // to not add anything to the object value array for a nil response.
1348  }
1349 
1350  return aString;
1351 }
1352 
1353 - (BOOL)_hasMenuForRepresentedObject:(id)aRepresentedObject
1354 {
1355  if ((_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_hasMenuForRepresentedObject_) &&
1356  (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_menuForRepresentedObject_))
1357  return [_tokenFieldDelegate tokenField:self hasMenuForRepresentedObject:aRepresentedObject];
1358 
1359  return NO;
1360 }
1361 
1362 - (CPMenu)_menuForRepresentedObject:(id)aRepresentedObject
1363 {
1364  if ((_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_hasMenuForRepresentedObject_) &&
1365  (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_menuForRepresentedObject_))
1366  {
1367  var hasMenu = [_tokenFieldDelegate tokenField:self hasMenuForRepresentedObject:aRepresentedObject];
1368 
1369  if (hasMenu)
1370  return [_tokenFieldDelegate tokenField:self menuForRepresentedObject:aRepresentedObject] || nil;
1371  }
1372 
1373  return nil;
1374 }
1375 
1376 // We put the string on the pasteboard before calling this delegate method.
1377 // By default, we write the NSStringPboardType as well as an array of NSStrings.
1378 // - (BOOL)tokenField:(NSTokenField *)tokenField writeRepresentedObjects:(NSArray *)objects toPasteboard:(NSPasteboard *)pboard;
1379 //
1380 // Return an array of represented objects to add to the token field.
1381 // - (NSArray *)tokenField:(NSTokenField *)tokenField readFromPasteboard:(NSPasteboard *)pboard;
1382 //
1383 // By default the tokens have no menu.
1384 // - (NSMenu *)tokenField:(NSTokenField *)tokenField menuForRepresentedObject:(id)representedObject;
1385 // - (BOOL)tokenField:(NSTokenField *)tokenField hasMenuForRepresentedObject:(id)representedObject;
1386 //
1387 // This method allows you to change the style for individual tokens as well as have mixed text and tokens.
1388 // - (NSTokenStyle)tokenField:(NSTokenField *)tokenField styleForRepresentedObject:(id)representedObject;
1389 
1390 - (void)_delayedShowCompletions
1391 {
1392  [[self _autocompleteMenu] _delayedShowCompletions];
1393 }
1394 
1395 - (void)_hideCompletions
1396 {
1397  [_autocompleteMenu _hideCompletions];
1398 }
1399 
1400 
1401 - (void)setButtonType:(int)aButtonType
1402 {
1403  if (_buttonType === aButtonType)
1404  return;
1405 
1406  _buttonType = aButtonType;
1407  [self setNeedsLayout];
1408 }
1409 
1410 @end
1411 
1412 @implementation _CPTokenFieldToken : CPTextField
1413 {
1414  _CPTokenFieldTokenCloseButton _deleteButton;
1415  _CPTokenFieldTokenDisclosureButton _disclosureButton;
1416  CPTokenField _tokenField;
1417  id _representedObject;
1418  int _buttonType;
1419 }
1420 
1421 + (CPString)defaultThemeClass
1422 {
1423  return "tokenfield-token";
1424 }
1425 
1426 - (BOOL)acceptsFirstResponder
1427 {
1428  return NO;
1429 }
1430 
1431 - (id)initWithFrame:(CGRect)frame
1432 {
1433  if (self = [super initWithFrame:frame])
1434  {
1435  [self setEditable:NO];
1436  [self setHighlighted:NO];
1437  [self setBezeled:YES];
1438  [self setButtonType:CPTokenFieldDisclosureButtonType];
1439  }
1440 
1441  return self;
1442 }
1443 
1444 - (CPTokenField)tokenField
1445 {
1446  return _tokenField;
1447 }
1448 
1449 - (void)setTokenField:(CPTokenField)tokenField
1450 {
1451  _tokenField = tokenField;
1452 }
1453 
1454 - (id)representedObject
1455 {
1456  return _representedObject;
1457 }
1458 
1459 - (void)setRepresentedObject:(id)representedObject
1460 {
1461  _representedObject = representedObject;
1462  [self setNeedsLayout];
1463 }
1464 
1465 - (void)setEditable:(BOOL)shouldBeEditable
1466 {
1467  [super setEditable:shouldBeEditable];
1468  [self setNeedsLayout];
1469 }
1470 
1471 - (BOOL)setThemeState:(ThemeState)aState
1472 {
1473  var r = [super setThemeState:aState];
1474 
1475  // Share hover state with the disclosure and delete buttons.
1476  if (aState.hasThemeState(CPThemeStateHovered))
1477  {
1478  [_disclosureButton setThemeState:CPThemeStateHovered];
1479  [_deleteButton setThemeState:CPThemeStateHovered];
1480  }
1481 
1482  return r;
1483 }
1484 
1485 - (BOOL)unsetThemeState:(ThemeState)aState
1486 {
1487  var r = [super unsetThemeState:aState];
1488 
1489  // Share hover state with the disclosure and delete button.
1490  if (aState.hasThemeState(CPThemeStateHovered))
1491  {
1492  [_disclosureButton unsetThemeState:CPThemeStateHovered];
1493  [_deleteButton unsetThemeState:CPThemeStateHovered];
1494  }
1495 
1496  return r;
1497 }
1498 
1499 - (CGSize)_minimumFrameSize
1500 {
1501  var size = CGSizeMakeZero(),
1502  minSize = [self currentValueForThemeAttribute:@"min-size"],
1503  contentInset = [self currentValueForThemeAttribute:@"content-inset"];
1504 
1505  // Tokens are fixed height, so we could as well have used max-size here.
1506  size.height = minSize.height;
1507  size.width = MAX(minSize.width, [([self stringValue] || @" ") sizeWithFont:[self font]].width + contentInset.left + contentInset.right);
1508 
1509  return size;
1510 }
1511 
1512 - (void)setButtonType:(int)aButtonType
1513 {
1514  if (_buttonType === aButtonType)
1515  return;
1516 
1517  _buttonType = aButtonType;
1518 
1519  if (_buttonType === CPTokenFieldDisclosureButtonType)
1520  {
1521  if (_deleteButton)
1522  {
1523  [_deleteButton removeFromSuperview];
1524  _deleteButton = nil;
1525  }
1526 
1527  if (!_disclosureButton)
1528  {
1529  _disclosureButton = [[_CPTokenFieldTokenDisclosureButton alloc] initWithFrame:CGRectMakeZero()];
1530  [self addSubview:_disclosureButton];
1531  }
1532  }
1533  else
1534  {
1535  if (_disclosureButton)
1536  {
1537  [_disclosureButton removeFromSuperview];
1538  _disclosureButton = nil;
1539  }
1540 
1541  if (!_deleteButton)
1542  {
1543  _deleteButton = [[_CPTokenFieldTokenCloseButton alloc] initWithFrame:CGRectMakeZero()];
1544  [self addSubview:_deleteButton];
1545  [_deleteButton setTarget:self];
1546  [_deleteButton setAction:@selector(_delete:)];
1547  }
1548  }
1549 
1550  [self setNeedsLayout];
1551 }
1552 
1553 - (void)layoutSubviews
1554 {
1555  [super layoutSubviews];
1556 
1557  var bezelView = [self layoutEphemeralSubviewNamed:@"bezel-view"
1558  positioned:CPWindowBelow
1559  relativeToEphemeralSubviewNamed:@"content-view"];
1560 
1561  if (bezelView && _tokenField)
1562  {
1563  switch (_buttonType)
1564  {
1566  var shouldBeEnabled = [self hasMenu];
1567  [_disclosureButton setHidden:!shouldBeEnabled];
1568 
1569  if (shouldBeEnabled)
1570  [_disclosureButton setMenu:[self menu]];
1571 
1572  var frame = [bezelView frame],
1573  buttonOffset = [_disclosureButton currentValueForThemeAttribute:@"offset"],
1574  buttonSize = [_disclosureButton currentValueForThemeAttribute:@"min-size"];
1575 
1576  [_disclosureButton setFrame:CGRectMake(CGRectGetMaxX(frame) - buttonOffset.x, CGRectGetMinY(frame) + buttonOffset.y, buttonSize.width, buttonSize.height)];
1577  break;
1579  [_deleteButton setEnabled:[self isEditable] && [self isEnabled]];
1580 
1581  var frame = [bezelView frame],
1582  buttonOffset = [_deleteButton currentValueForThemeAttribute:@"offset"],
1583  buttonSize = [_deleteButton currentValueForThemeAttribute:@"min-size"];
1584 
1585  [_deleteButton setFrame:CGRectMake(CGRectGetMaxX(frame) - buttonOffset.x, CGRectGetMinY(frame) + buttonOffset.y, buttonSize.width, buttonSize.height)];
1586  break;
1587  }
1588  }
1589 }
1590 
1591 - (void)mouseDown:(CPEvent)anEvent
1592 {
1593  [_tokenField _mouseDownOnToken:self withEvent:anEvent];
1594 }
1595 
1596 - (void)mouseUp:(CPEvent)anEvent
1597 {
1598  [_tokenField _mouseUpOnToken:self withEvent:anEvent];
1599 }
1600 
1601 - (void)_delete:(id)sender
1602 {
1603  if ([self isEditable])
1604  [_tokenField _deleteToken:self];
1605 }
1606 
1607 - (BOOL)hasMenu
1608 {
1609  return [_tokenField _hasMenuForRepresentedObject:_representedObject];
1610 }
1611 
1612 - (CPMenu)menu
1613 {
1614  return [_tokenField _menuForRepresentedObject:_representedObject];
1615 }
1616 
1617 @end
1618 @implementation _CPTokenFieldTokenCloseButton : CPButton
1619 {
1620  id __doxygen__;
1621 }
1622 
1623 + (CPDictionary)themeAttributes
1624 {
1625  var attributes = [CPButton themeAttributes];
1626 
1627  [attributes setObject:CGPointMake(15, 5) forKey:@"offset"];
1628 
1629  return attributes;
1630 }
1631 
1632 + (CPString)defaultThemeClass
1633 {
1634  return "tokenfield-token-close-button";
1635 }
1636 
1637 - (void)mouseEntered:(CPEvent)anEvent
1638 {
1639  // Don't toggle hover state from within the button - we use the hover state of the token field as a whole.
1640 }
1641 
1642 - (void)mouseExited:(CPEvent)anEvent
1643 {
1644  // Don't toggle hover state from within the button - we use the hover state of the token field as a whole.
1645 }
1646 
1647 @end
1648 @implementation _CPTokenFieldTokenDisclosureButton : CPPopUpButton
1649 {
1650  id __doxygen__;
1651 }
1652 
1653 + (CPDictionary)themeAttributes
1654 {
1655  var attributes = [CPButton themeAttributes];
1656 
1657  [attributes setObject:CGPointMake(15, 5) forKey:@"offset"];
1658 
1659  return attributes;
1660 }
1661 
1662 + (CPString)defaultThemeClass
1663 {
1664  return "tokenfield-token-disclosure-button";
1665 }
1666 
1667 - (id)initWithFrame:(CGRect)aFrame
1668 {
1669  if (self = [self initWithFrame:aFrame pullsDown:YES])
1670  {
1671  [self setBordered:YES];
1672  [super setTitle:@""];
1673  }
1674 
1675  return self;
1676 }
1677 
1678 - (void)setTitle:(CPString)aTitle
1679 {
1680  // skip
1681 }
1682 
1683 - (void)synchronizeTitleAndSelectedItem
1684 {
1685  // skip
1686 }
1687 
1688 - (void)mouseEntered:(CPEvent)anEvent
1689 {
1690  // Don't toggle hover state from within the button - we use the hover state of the token field as a whole.
1691 }
1692 
1693 - (void)mouseExited:(CPEvent)anEvent
1694 {
1695  // Don't toggle hover state from within the button - we use the hover state of the token field as a whole.
1696 }
1697 
1698 @end
1699 
1700 
1701 var CPTokenFieldTokenizingCharacterSetKey = "CPTokenFieldTokenizingCharacterSetKey",
1702  CPTokenFieldCompletionDelayKey = "CPTokenFieldCompletionDelay",
1703  CPTokenFieldButtonTypeKey = "CPTokenFieldButtonTypeKey";
1704 
1706 
1707 - (id)initWithCoder:(CPCoder)aCoder
1708 {
1709  self = [super initWithCoder:aCoder];
1710 
1711  if (self)
1712  {
1713  _tokenizingCharacterSet = [aCoder decodeObjectForKey:CPTokenFieldTokenizingCharacterSetKey] || [[self class] defaultTokenizingCharacterSet];
1714  _completionDelay = [aCoder decodeDoubleForKey:CPTokenFieldCompletionDelayKey] || [[self class] defaultCompletionDelay];
1715  _buttonType = [aCoder decodeIntForKey:CPTokenFieldButtonTypeKey] || CPTokenFieldDisclosureButtonType;
1716 
1717  [self _init];
1718 
1719  [self setNeedsLayout];
1720  [self setNeedsDisplay:YES];
1721  }
1722 
1723  return self;
1724 }
1725 
1726 - (void)encodeWithCoder:(CPCoder)aCoder
1727 {
1728  [super encodeWithCoder:aCoder];
1729 
1730  [aCoder encodeInt:_tokenizingCharacterSet forKey:CPTokenFieldTokenizingCharacterSetKey];
1731  [aCoder encodeDouble:_completionDelay forKey:CPTokenFieldCompletionDelayKey];
1732  [aCoder encodeInt:_buttonType forKey:CPTokenFieldButtonTypeKey];
1733 }
1734 
1735 @end
1736 
1738 
1742 - (CPCharacterSet)tokenizingCharacterSet
1743 {
1744  return _tokenizingCharacterSet;
1745 }
1746 
1750 - (void)setTokenizingCharacterSet:(CPCharacterSet)aValue
1751 {
1752  _tokenizingCharacterSet = aValue;
1753 }
1754 
1758 - (int)buttonType
1759 {
1760  return _buttonType;
1761 }
1762 
1766 - (void)setButtonType:(int)aValue
1767 {
1768  _buttonType = aValue;
1769 }
1770 
1771 @end
CPThemeStateEditing
Definition: CPTheme.j:620
CPThemeStateHovered
Definition: CPTheme.j:611
BOOL makeFirstResponder:(CPResponder aResponder)
Definition: CPWindow.j:1632
Definition: CPMenu.h:2
function CPUnionRange(lhsRange, rhsRange)
Definition: CPRange.j:106
void encodeWithCoder:(CPCoder aCoder)
Definition: CPTextField.j:2090
float defaultLineHeightForFont()
Definition: CPFont.j:362
void textDidChange:(CPNotification note)
Definition: CPTextField.j:1218
CGRect frame
var CPScrollDestinationRight
Definition: CPTokenField.j:72
var CPTokenFieldDelegate_tokenField_menuForRepresentedObject_
Definition: CPTokenField.j:47
The main run loop for the application.
Definition: CPRunLoop.h:2
CPRightTextAlignment
Definition: CPText.j:77
CPThemeStateAutocompleting
Definition: CPTheme.j:624
CPResponder firstResponder()
Definition: CPWindow.j:1657
var CPTokenFieldTokenizingCharacterSetKey
CPFont font()
Definition: CPControl.j:899
CPDictionary themeAttributes()
Definition: CPButton.j:145
int width
CGRect bounds()
Definition: CPView.j:1326
var CPTokenFieldDelegate_tokenField_displayStringForRepresentedObject_
Definition: CPTokenField.j:48
CPTextFieldDidBlurNotification
Definition: CPTextField.j:46
CPString charactersIgnoringModifiers()
Definition: CPEvent.j:392
CPString stringValue()
Definition: CPTokenField.j:586
unsigned modifierFlags()
Definition: CPEvent.j:309
var CPTokenFieldDelegate_tokenField_completionsForSubstring_indexOfToken_indexOfSelectedItem_
Definition: CPTokenField.j:45
var CPTokenFieldCompletionDelayKey
var CPTokenFieldDelegate_tokenField_representedObjectForEditingString_
Definition: CPTokenField.j:49
A mutable key-value pair collection.
Definition: CPDictionary.h:2
id characterSetWithCharactersInString:(CPString aString)
function ThemeState(stateNames)
Definition: CPTheme.j:379
void setEnabled:(BOOL shouldBeEnabled)
Definition: CPTextField.j:412
CPRunLoop currentRunLoop()
Definition: CPRunLoop.j:232
void setEditable:(BOOL shouldBeEditable)
Definition: CPTextField.j:373
void textDidBlur:(CPNotification note)
Definition: CPTextField.j:1194
CGRect bounds()
Definition: CALayer.j:203
CPWindow window()
Definition: CPView.j:527
CPTabCharacter
Definition: CPText.j:51
function CPEmptyRange(aRange)
Definition: CPRange.j:59
function CPMaxRange(aRange)
Definition: CPRange.j:70
An immutable string (collection of characters).
Definition: CPString.h:2
void keyDown:(CPEvent anEvent)
CPRunLoop mainRunLoop()
Definition: CPRunLoop.j:240
BOOL sendAction:to:(SEL anAction, [to] id anObject)
Definition: CPControl.j:319
function CPFeatureIsCompatible(aFeature)
SEL action()
Definition: CPControl.j:290
void setObjectValue:(id aValue)
Definition: CPTokenField.j:618
void performBlock:argument:order:modes:(Function aBlock, [argument] id anArgument, [order] int anOrder, [modes] CPArray modes)
Definition: CPRunLoop.j:270
void selectNextKeyView:(id sender)
Definition: CPWindow.j:3275
BOOL isEditable()
Definition: CPTextField.j:403
BOOL performKeyEquivalent:(CPEvent anEvent)
Definition: CPView.j:3054
void setNeedsDisplay:(BOOL aFlag)
Definition: CPView.j:2597
CGRect rectForEphemeralSubviewNamed:(CPString aName)
Definition: CPTextField.j:1822
CPShiftKeyMask
var CPTokenFieldButtonTypeKey
A notification that can be posted to a CPNotificationCenter.
Definition: CPNotification.h:2
CPDate limitDateForMode:(CPString aMode)
Definition: CPRunLoop.j:342
void setNeedsLayout()
Definition: CPView.j:2748
void setObjectValue:(id aValue)
Definition: CPTextField.j:1310
CPTokenFieldDeleteButtonType
Definition: CPTokenField.j:75
id target()
Definition: CPControl.j:308
var CPTokenFieldDelegate_tokenField_hasMenuForRepresentedObject_
Definition: CPTokenField.j:44
function CPMakeRangeCopy(aRange)
Definition: CPRange.j:48
Defines methods for use when archiving & restoring (enc/decoding).
Definition: CPCoder.h:2
CPNotFound
Definition: CPObjJRuntime.j:62
CPInputTypeCanBeChangedFeature
var CPScrollDestinationLeft
Definition: CPTokenField.j:71
function CPLocationInRange(aLocation, aRange)
Definition: CPRange.j:93
void selectPreviousKeyView:(id sender)
Definition: CPWindow.j:3297
CPNotification notificationWithName:object:userInfo:(CPString aNotificationName, [object] id anObject, [userInfo] CPDictionary aUserInfo)
void layoutSubviews()
CPNewlineCharacter
Definition: CPText.j:52
void mouseDown:(CPEvent anEvent)
Definition: CPTextField.j:996
void setBezeled:(BOOL shouldBeBezeled)
Definition: CPTextField.j:464
void textDidEndEditing:(CPNotification note)
Definition: CPTextField.j:1243
CPCarriageReturnCharacter
Definition: CPText.j:54
BOOL sendAction:to:(SEL anAction, [to] id anObject)
Definition: CPTokenField.j:715
Definition: CPEvent.h:2
var CPScrollDestinationNone
Definition: CPTokenField.j:70
CPCenterTextAlignment
Definition: CPText.j:78
CPPlatformWindow platformWindow()
Definition: CPWindow.j:389
CGRect frame()
Definition: CPView.j:1046
var CPTokenFieldDelegate_tokenField_shouldAddObjects_atIndex_
Definition: CPTokenField.j:46
CPTokenFieldDisclosureButtonType
Definition: CPTokenField.j:74
CPTextFieldDidFocusNotification
Definition: CPTextField.j:45
BOOL scrollRectToVisible:(CGRect aRect)
Definition: CPView.j:2862
void layoutSubviews()
Definition: CPTextField.j:1855
id initWithCoder:(CPCoder aCoder)
Definition: CPTextField.j:2060
void setDelegate:(id< CPTextFieldDelegate > aDelegate)
Definition: CPTextField.j:1776
CPRange function CPMakeRange(location, length)
Definition: CPRange.j:37
function CPTextFieldBlurFunction(anEvent, owner, domElement, inputElement, resigning, didBlurRef)
Definition: CPTextField.j:65
CPView layoutEphemeralSubviewNamed:positioned:relativeToEphemeralSubviewNamed:(CPString aViewName, [positioned] CPWindowOrderingMode anOrderingMode, [relativeToEphemeralSubviewNamed] CPString relativeToViewName)
Definition: CPView.j:3407
CPMenu menu
CPString characters()
Definition: CPEvent.j:382
Definition: CPView.j:137
void textDidChange:(CPNotification aNotification)