API  0.9.10
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 
102 {
104 }
105 
106 + (CPTimeInterval)defaultCompletionDelay
107 {
108  return 0.5;
109 }
110 
112 {
113  return "tokenfield";
114 }
115 
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 
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 
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  element.blur();
514 
515  if (!CPTokenFieldInputDidBlur)
516  CPTokenFieldBlurHandler();
517 
518  CPTokenFieldInputDidBlur = NO;
519  CPTokenFieldInputResigning = NO;
520 
521  if (element.parentNode == [_tokenScrollView documentView]._DOMElement)
522  element.parentNode.removeChild(element);
523 
524  CPTokenFieldInputIsActive = NO;
525 
526  if (document.attachEvent)
527  {
528  CPTokenFieldCachedSelectStartFunction = nil;
529  CPTokenFieldCachedDragFunction = nil;
530 
531  document.body.ondrag = CPTokenFieldCachedDragFunction
532  document.body.onselectstart = CPTokenFieldCachedSelectStartFunction
533  }
534 
535 #endif
536 }
537 
538 - (void)mouseDown:(CPEvent)anEvent
539 {
540  _mouseDownEvent = anEvent;
541 
542  [self _selectToken:nil byExtendingSelection:NO];
543 
544  [super mouseDown:anEvent];
545 }
546 
547 - (void)mouseUp:(CPEvent)anEvent
548 {
549  _mouseDownEvent = nil;
550 }
551 
552 - (void)_mouseDownOnToken:(_CPTokenFieldToken)aToken withEvent:(CPEvent)anEvent
553 {
554  _mouseDownEvent = anEvent;
555 }
556 
557 - (void)_mouseUpOnToken:(_CPTokenFieldToken)aToken withEvent:(CPEvent)anEvent
558 {
559  if (_mouseDownEvent && CGPointEqualToPoint([_mouseDownEvent locationInWindow], [anEvent locationInWindow]))
560  {
561  [self _selectToken:aToken byExtendingSelection:[anEvent modifierFlags] & CPShiftKeyMask];
562  [[self window] makeFirstResponder:self];
563  // Snap to the token if it's only half visible due to mouse wheel scrolling.
564  _shouldScrollTo = aToken;
565  }
566 }
567 
568 // ===========
569 // = CONTROL =
570 // ===========
571 - (CPArray)_tokens
572 {
573  // We return super here because objectValue uses this method
574  // If we called self we would loop infinitely
575  return [super objectValue];
576 }
577 
579 {
580  return [[self objectValue] componentsJoinedByString:@","];
581 }
582 
584 {
585  var objectValue = [];
586 
587  for (var i = 0, count = [[self _tokens] count]; i < count; i++)
588  {
589  var token = [[self _tokens] objectAtIndex:i];
590 
591  if ([token isKindOfClass:[CPString class]])
592  continue;
593 
594  [objectValue addObject:[token representedObject]];
595  }
596 
597 #if PLATFORM(DOM)
598 
599  if ([self _editorValue])
600  {
601  var token = [self _representedObjectForEditingString:[self _editorValue]];
602  [objectValue insertObject:token atIndex:_selectedRange.location];
603  }
604 
605 #endif
606 
607  return objectValue;
608 }
609 
610 - (void)setObjectValue:(id)aValue
611 {
612  if (aValue !== nil && ![aValue isKindOfClass:[CPArray class]])
613  {
614  [super setObjectValue:nil];
615  return;
616  }
617 
618  var superValue = [super objectValue];
619  if (aValue === superValue || [aValue isEqualToArray:superValue])
620  return;
621 
622  var contentView = [_tokenScrollView documentView],
623  oldTokens = [self _tokens],
624  newTokens = [];
625 
626  // Preserve as many existing tokens as possible to reduce redraw flickering.
627  if (aValue !== nil)
628  {
629  for (var i = 0, count = [aValue count]; i < count; i++)
630  {
631  // Do we have this token among the old ones?
632  var tokenObject = aValue[i],
633  tokenValue = [self _displayStringForRepresentedObject:tokenObject],
634  newToken = nil;
635 
636  for (var j = 0, oldCount = [oldTokens count]; j < oldCount; j++)
637  {
638  var oldToken = oldTokens[j];
639  if ([oldToken representedObject] == tokenObject)
640  {
641  // Yep. Reuse it.
642  [oldTokens removeObjectAtIndex:j];
643  newToken = oldToken;
644  break;
645  }
646  }
647 
648  if (newToken === nil)
649  {
650  newToken = [_CPTokenFieldToken new];
651  [newToken setTokenField:self];
652  [newToken setRepresentedObject:tokenObject];
653  [newToken setStringValue:tokenValue];
654  [newToken setEditable:[self isEditable]];
655  [contentView addSubview:newToken];
656  }
657 
658  newTokens.push(newToken);
659  }
660  }
661 
662  // Remove any now unused tokens.
663  for (var j = 0, oldCount = [oldTokens count]; j < oldCount; j++)
664  [oldTokens[j] removeFromSuperview];
665 
666  /*
667  [CPTextField setObjectValue] will try to set the _inputElement.value to
668  the new objectValue, if the _inputElement exists. This is wrong for us
669  since our objectValue is an array of tokens, so we can't use
670  [super setObjectValue:objectValue];
671 
672  Instead do what CPControl setObjectValue would.
673  */
674  _value = newTokens;
675 
676  // Reset the selection.
677  [self _selectToken:nil byExtendingSelection:NO];
678 
679  [self _updatePlaceholderState];
680 
681  _shouldScrollTo = CPScrollDestinationRight;
682  [self setNeedsLayout];
683  [self setNeedsDisplay:YES];
684 }
685 
686 - (void)setEnabled:(BOOL)shouldBeEnabled
687 {
688  [super setEnabled:shouldBeEnabled];
689 
690  // Set the enabled state of the tokens
691  for (var i = 0, count = [[self _tokens] count]; i < count; i++)
692  {
693  var token = [[self _tokens] objectAtIndex:i];
694 
695  if ([token respondsToSelector:@selector(setEnabled:)])
696  [token setEnabled:shouldBeEnabled];
697  }
698 }
699 
700 - (void)setEditable:(BOOL)shouldBeEditable
701 {
702  [super setEditable:shouldBeEditable];
703 
704  [[self _tokens] makeObjectsPerformSelector:@selector(setEditable:) withObject:shouldBeEditable];
705 }
706 
707 - (BOOL)sendAction:(SEL)anAction to:(id)anObject
708 {
709  _shouldNotifyTarget = NO;
710  [super sendAction:anAction to:anObject];
711 }
712 
713 // Incredible hack to disable supers implementation
714 // so it cannot change our object value and break the tokenfield
715 - (BOOL)_setStringValue:(CPString)aValue
716 {
717 }
718 
719 // =============
720 // = TEXTFIELD =
721 // =============
722 #if PLATFORM(DOM)
723 - (DOMElement)_inputElement
724 {
725  if (!CPTokenFieldDOMInputElement)
726  {
727  CPTokenFieldDOMInputElement = document.createElement("input");
728  CPTokenFieldDOMInputElement.style.position = "absolute";
729  CPTokenFieldDOMInputElement.style.border = "0px";
730  CPTokenFieldDOMInputElement.style.padding = "0px";
731  CPTokenFieldDOMInputElement.style.margin = "0px";
732  CPTokenFieldDOMInputElement.style.whiteSpace = "pre";
733  CPTokenFieldDOMInputElement.style.background = "transparent";
734  CPTokenFieldDOMInputElement.style.outline = "none";
735 
736  CPTokenFieldBlurHandler = function(anEvent)
737  {
739  anEvent,
740  CPTokenFieldInputOwner,
741  CPTokenFieldInputOwner ? [CPTokenFieldInputOwner._tokenScrollView documentView]._DOMElement : nil,
742  CPTokenFieldDOMInputElement,
743  CPTokenFieldInputResigning,
744  @ref(CPTokenFieldInputDidBlur));
745  };
746 
747  // FIXME make this not onblur
748  CPTokenFieldDOMInputElement.onblur = CPTokenFieldBlurHandler;
749 
750  CPTokenFieldDOMStandardInputElement = CPTokenFieldDOMInputElement;
751  }
752 
754  {
755  if ([CPTokenFieldInputOwner isSecure])
756  CPTokenFieldDOMInputElement.type = "password";
757  else
758  CPTokenFieldDOMInputElement.type = "text";
759 
760  return CPTokenFieldDOMInputElement;
761  }
762 
763  return CPTokenFieldDOMInputElement;
764 }
765 #endif
766 
767 - (CPString)_editorValue
768 {
769  if (![self hasThemeState:CPThemeStateEditing])
770  return @"";
771  return [self _inputElement].value;
772 }
773 
774 - (void)moveUp:(id)sender
775 {
776  [[self _autocompleteMenu] selectPrevious];
777  [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
778 }
779 
780 - (void)moveDown:(id)sender
781 {
782  [[self _autocompleteMenu] selectNext];
783  [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
784 }
785 
786 - (void)insertNewline:(id)sender
787 {
788  if ([self hasThemeState:CPThemeStateAutocompleting])
789  {
790  [self _autocompleteWithEvent:[CPApp currentEvent]];
791  }
792  else
793  {
794  [self sendAction:[self action] to:[self target]];
795  [[self window] makeFirstResponder:nil];
796  }
797 }
798 
799 - (void)insertTab:(id)sender
800 {
801  var anEvent = [CPApp currentEvent];
802  if ([self hasThemeState:CPThemeStateAutocompleting])
803  {
804  [self _autocompleteWithEvent:anEvent];
805  }
806  else
807  {
808  // Default to standard tabbing behaviour.
809  if (!([anEvent modifierFlags] & CPShiftKeyMask))
810  [[self window] selectNextKeyView:self];
811  else
812  [[self window] selectPreviousKeyView:self];
813  }
814 }
815 
816 - (void)insertText:(CPString)characters
817 {
818  // Note that in Cocoa NStokenField uses a hidden input field not accessible to the user,
819  // so insertText: is called on that field instead. That seems rather silly since it makes
820  // it pretty much impossible to override insertText:. This version is better.
821  if ([_tokenizingCharacterSet characterIsMember:[characters substringToIndex:1]])
822  {
823  [self _autocompleteWithEvent:[CPApp currentEvent]];
824  }
825  else
826  {
827  // If you type something while tokens are selected, overwrite them.
828  if (_selectedRange.length)
829  {
830  [self _removeSelectedTokens:self];
831  // Make sure the editor is placed so it can capture the characters we're overwriting with.
832  [self layoutSubviews];
833  }
834 
835  // If we didn't handle it, allow _propagateCurrentDOMEvent the input field to receive
836  // the new character.
837 
838  // This method also allows a subclass to override insertText: to do nothing.
839  // Unfortunately calling super with some different characters won't work since
840  // the browser will see the original key event.
841  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
842  }
843 }
844 
845 - (void)cancelOperation:(id)sender
846 {
847  [self _hideCompletions];
848 }
849 
850 - (void)moveLeft:(id)sender
851 {
852  // Left arrow
853  if ((_selectedRange.location > 0 || _selectedRange.length) && [self _editorValue] == "")
854  {
855  if (_selectedRange.length)
856  // Simply collapse the range.
857  _selectedRange.length = 0;
858  else
859  _selectedRange.location--;
860  [self setNeedsLayout];
861  _shouldScrollTo = CPScrollDestinationLeft;
862  }
863  else
864  {
865  // Allow cursor movement within the text field.
866  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
867  }
868 }
869 
870 - (void)moveLeftAndModifySelection:(id)sender
871 {
872  if (_selectedRange.location > 0 && [self _editorValue] == "")
873  {
874  _selectedRange.location--;
875  // When shift is depressed, select the next token backwards.
876  _selectedRange.length++;
877  [self setNeedsLayout];
878  _shouldScrollTo = CPScrollDestinationLeft;
879  }
880  else
881  {
882  // Allow cursor movement within the text field.
883  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
884  }
885 }
886 
887 - (void)moveRight:(id)sender
888 {
889  // Right arrow
890  if ((_selectedRange.location < [[self _tokens] count] || _selectedRange.length) && [self _editorValue] == "")
891  {
892  if (_selectedRange.length)
893  {
894  // Place the cursor at the end of the selection and collapse.
895  _selectedRange.location = CPMaxRange(_selectedRange);
896  _selectedRange.length = 0;
897  }
898  else
899  {
900  // Move the cursor forward one token if the input is empty and the right arrow key is pressed.
901  _selectedRange.location = MIN([[self _tokens] count], _selectedRange.location + _selectedRange.length + 1);
902  }
903 
904  [self setNeedsLayout];
905  _shouldScrollTo = CPScrollDestinationRight;
906  }
907  else
908  {
909  // Allow cursor movement within the text field.
910  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
911  }
912 }
913 
914 - (void)moveRightAndModifySelection:(id)sender
915 {
916  if (CPMaxRange(_selectedRange) < [[self _tokens] count] && [self _editorValue] == "")
917  {
918  // Leave the selection location in place but include the next token to the right.
919  _selectedRange.length++;
920  [self setNeedsLayout];
921  _shouldScrollTo = CPScrollDestinationRight;
922  }
923  else
924  {
925  // Allow selection to happen within the text field.
926  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
927  }
928 }
929 
930 - (void)deleteBackward:(id)sender
931 {
932  // TODO Even if the editor isn't empty you should be able to delete the previous token by placing the cursor
933  // at the beginning of the editor.
934  if ([self _editorValue] == @"")
935  {
936  [self _hideCompletions];
937 
938  if (CPEmptyRange(_selectedRange))
939  {
940  if (_selectedRange.location > 0)
941  {
942  var tokenView = [[self _tokens] objectAtIndex:(_selectedRange.location - 1)];
943  [self _selectToken:tokenView byExtendingSelection:NO];
944  }
945  }
946  else
947  [self _removeSelectedTokens:nil];
948  }
949  else
950  {
951  // Allow deletion to happen within the text field.
952  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
953  }
954 }
955 
956 - (void)deleteForward:(id)sender
957 {
958  // TODO Even if the editor isn't empty you should be able to delete the next token by placing the cursor
959  // at the end of the editor.
960  if ([self _editorValue] == @"")
961  {
962  // Delete forward if nothing is selected, else delete all selected.
963  [self _hideCompletions];
964 
965  if (CPEmptyRange(_selectedRange))
966  {
967  if (_selectedRange.location < [[self _tokens] count])
968  [self _deleteToken:[[self _tokens] objectAtIndex:[_selectedRange.location]]];
969  }
970  else
971  [self _removeSelectedTokens:nil];
972  }
973  else
974  {
975  // Allow deletion to happen within the text field.
976  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
977  }
978 }
979 
980 - (void)_selectText:(id)sender immediately:(BOOL)immediately
981 {
982  // Override CPTextField's version. The correct behaviour is that the text currently being
983  // edited is turned into a token if possible, or left as plain selected text if not.
984  // Regardless of if there is on-going text entry, all existing tokens are also selected.
985  // At this point we don't support having tokens and text selected at the same time (or
986  // any situation where the cursor isn't within the text being edited) so we just finish
987  // editing and select all tokens.
988 
989  if (([self isEditable] || [self isSelectable]))
990  {
991  [super _selectText:sender immediately:immediately];
992 
993  // Finish any editing.
994  [self _autocomplete];
995  _selectedRange = CPMakeRange(0, [[self _tokens] count]);
996 
997  [self setNeedsLayout];
998  }
999 }
1000 
1001 - (void)keyDown:(CPEvent)anEvent
1002 {
1003 #if PLATFORM(DOM)
1004  CPTokenFieldTextDidChangeValue = [self stringValue];
1005 #endif
1006 
1007  // Leave the default _propagateCurrentDOMEvent setting in place. This might be YES or NO depending
1008  // on if something that could be a browser shortcut was pressed or not, such as Cmd-R to reload.
1009  // If it was NO we want to leave it at NO however and only enable it in insertText:. This is what
1010  // allows a subclass to prevent characters from being inserted by overriding and not calling super.
1011 
1012  [self interpretKeyEvents:[anEvent]];
1013 
1014  [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
1015 }
1016 
1017 - (void)keyUp:(CPEvent)anEvent
1018 {
1019 #if PLATFORM(DOM)
1020  if ([self stringValue] !== CPTokenFieldTextDidChangeValue)
1021  {
1022  [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
1023  }
1024 #endif
1025 
1026  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
1027 }
1028 
1029 - (BOOL)performKeyEquivalent:(CPEvent)anEvent
1030 {
1031  var characters = [anEvent characters];
1032 
1033  // Here we handle the event when getting a CPNewlineCharacter or CPCarriageReturnCharacter when the menu is open
1034  // We don't want that the application dispatches the event to the other controls
1035  if ([self hasThemeState:CPThemeStateAutocompleting] && (characters === CPNewlineCharacter || characters === CPCarriageReturnCharacter))
1036  {
1037  [self keyDown:anEvent];
1038  return YES;
1039  }
1040 
1041  return [super performKeyEquivalent:anEvent];
1042 }
1043 
1044 - (void)textDidChange:(CPNotification)aNotification
1045 {
1046  if ([aNotification object] !== self)
1047  return;
1048 
1049  [super textDidChange:aNotification];
1050 
1051  // For future reference: in Cocoa, textDidChange: appears to call [self complete:].
1052  [self _delayedShowCompletions];
1053  // If there was a selection, collapse it now since we're typing in a new token.
1054  _selectedRange.length = 0;
1055 
1056  // Force immediate layout in case word wrapping is now necessary.
1057  [self setNeedsLayout];
1058 }
1059 
1060 // - (void)setTokenStyle: (NSTokenStyle) style;
1061 // - (NSTokenStyle)tokenStyle;
1062 //
1063 
1064 // ====================
1065 // = COMPLETION DELAY =
1066 // ====================
1067 - (void)setCompletionDelay:(CPTimeInterval)delay
1068 {
1069  _completionDelay = delay;
1070 }
1071 
1072 - (CPTimeInterval)completionDelay
1073 {
1074  return _completionDelay;
1075 }
1076 
1077 // ==========
1078 // = LAYOUT =
1079 // ==========
1081 {
1082  [super layoutSubviews];
1083 
1084  [_tokenScrollView setFrame:[self rectForEphemeralSubviewNamed:"content-view"]];
1085 
1086  var textFieldContentView = [self layoutEphemeralSubviewNamed:@"content-view"
1087  positioned:CPWindowAbove
1088  relativeToEphemeralSubviewNamed:@"bezel-view"];
1089 
1090  if (textFieldContentView)
1091  [textFieldContentView setHidden:[self stringValue] !== @""];
1092 
1093  var frame = [self frame],
1094  contentView = [_tokenScrollView documentView],
1095  tokens = [self _tokens];
1096 
1097  // Hack to make sure we are handling an array
1098  if (![tokens isKindOfClass:[CPArray class]])
1099  return;
1100 
1101  // Move each token into the right position.
1102  var contentRect = CGRectMakeCopy([contentView bounds]),
1103  contentOrigin = contentRect.origin,
1104  contentSize = contentRect.size,
1105  offset = CGPointMake(contentOrigin.x, contentOrigin.y),
1106  spaceBetweenTokens = CGSizeMake(2.0, 2.0),
1107  isEditing = [[self window] firstResponder] == self,
1108  tokenToken = [_CPTokenFieldToken new],
1109  font = [self currentValueForThemeAttribute:@"font"],
1110  lineHeight = [font defaultLineHeightForFont],
1111  editorInset = [self currentValueForThemeAttribute:@"editor-inset"];
1112 
1113  // Put half a spacing above the tokens.
1114  offset.y += CEIL(spaceBetweenTokens.height / 2.0);
1115 
1116  // Get the height of a typical token, or a token token if you will.
1117  [tokenToken sizeToFit];
1118 
1119  var tokenHeight = CGRectGetHeight([tokenToken bounds]);
1120 
1121  var fitAndFrame = function(width, height)
1122  {
1123  var r = CGRectMake(0, 0, width, height);
1124 
1125  if (offset.x + width >= contentSize.width && offset.x > contentOrigin.x)
1126  {
1127  offset.x = contentOrigin.x;
1128  offset.y += height + spaceBetweenTokens.height;
1129  }
1130 
1131  r.origin.x = offset.x;
1132  r.origin.y = offset.y;
1133 
1134  // Make sure the frame fits.
1135  var scrollHeight = offset.y + tokenHeight + CEIL(spaceBetweenTokens.height / 2.0);
1136  if (CGRectGetHeight([contentView bounds]) < scrollHeight)
1137  [contentView setFrameSize:CGSizeMake(CGRectGetWidth([_tokenScrollView bounds]), scrollHeight)];
1138 
1139  offset.x += width + spaceBetweenTokens.width;
1140 
1141  return r;
1142  };
1143 
1144  var placeEditor = function(useRemainingWidth)
1145  {
1146  var element = [self _inputElement],
1147  textWidth = 1;
1148 
1149  if (_selectedRange.length === 0)
1150  {
1151  // XXX The "X" here is used to estimate the space needed to fit the next character
1152  // without clipping. Since different fonts might have different sizes of "X" this
1153  // solution is not ideal, but it works.
1154  textWidth = [(element.value || @"") + "X" sizeWithFont:font].width;
1155 
1156  if (useRemainingWidth)
1157  textWidth = MAX(contentSize.width - offset.x - 1, textWidth);
1158  }
1159 
1160  _inputFrame = fitAndFrame(textWidth, tokenHeight);
1161 
1162  _inputFrame.size.height = lineHeight;
1163 
1164  element.style.left = (_inputFrame.origin.x + editorInset.left) + "px";
1165  element.style.top = (_inputFrame.origin.y + editorInset.top) + "px";
1166  element.style.width = _inputFrame.size.width + "px";
1167  element.style.height = _inputFrame.size.height + "px";
1168 
1169  // When editing, always scroll to the cursor.
1170  if (_selectedRange.length == 0)
1171  [[_tokenScrollView documentView] scrollPoint:CGPointMake(0, _inputFrame.origin.y)];
1172  };
1173 
1174  for (var i = 0, count = [tokens count]; i < count; i++)
1175  {
1176  if (isEditing && !_selectedRange.length && i == CPMaxRange(_selectedRange))
1177  placeEditor(false);
1178 
1179  var tokenView = [tokens objectAtIndex:i];
1180 
1181  // Make sure we are only changing completed tokens
1182  if ([tokenView isKindOfClass:[CPString class]])
1183  continue;
1184 
1185  [tokenView setHighlighted:CPLocationInRange(i, _selectedRange)];
1186  [tokenView sizeToFit];
1187 
1188  var size = [contentView bounds].size,
1189  tokenViewSize = [tokenView bounds].size,
1190  tokenFrame = fitAndFrame(tokenViewSize.width, tokenViewSize.height);
1191 
1192  [tokenView setFrame:tokenFrame];
1193 
1194  [tokenView setButtonType:_buttonType];
1195  }
1196 
1197  if (isEditing && !_selectedRange.length && CPMaxRange(_selectedRange) >= [tokens count])
1198  placeEditor(true);
1199 
1200  // Hide the editor if there are selected tokens, but still keep it active
1201  // so we can continue using our standard keyboard handling events.
1202  if (isEditing && _selectedRange.length)
1203  {
1204  _inputFrame = nil;
1205  var inputElement = [self _inputElement];
1206  inputElement.style.display = "none";
1207  }
1208  else if (isEditing)
1209  {
1210  var inputElement = [self _inputElement];
1211  inputElement.style.display = "block";
1212  if (document.activeElement !== inputElement)
1213  inputElement.focus();
1214  }
1215 
1216  // Trim off any excess height downwards (in case we shrank).
1217  var scrollHeight = offset.y + tokenHeight;
1218  if (CGRectGetHeight([contentView bounds]) > scrollHeight)
1219  [contentView setFrameSize:CGSizeMake(CGRectGetWidth([_tokenScrollView bounds]), scrollHeight)];
1220 
1221  if (_shouldScrollTo !== CPScrollDestinationNone)
1222  {
1223  // Only carry out the scroll if the cursor isn't visible.
1224  if (!(isEditing && _selectedRange.length == 0))
1225  {
1226  var scrollToToken = _shouldScrollTo;
1227 
1228  if (scrollToToken === CPScrollDestinationLeft)
1229  scrollToToken = tokens[_selectedRange.location]
1230  else if (scrollToToken === CPScrollDestinationRight)
1231  scrollToToken = tokens[MAX(0, CPMaxRange(_selectedRange) - 1)];
1232  [self _scrollTokenViewToVisible:scrollToToken];
1233  }
1234 
1235  _shouldScrollTo = CPScrollDestinationNone;
1236  }
1237 }
1238 
1239 - (BOOL)_scrollTokenViewToVisible:(_CPTokenFieldToken)aToken
1240 {
1241  if (!aToken)
1242  return;
1243 
1244  return [[_tokenScrollView documentView] scrollPoint:CGPointMake(0, [aToken frameOrigin].y)];
1245 }
1246 
1247 @end
1248 
1250 
1260 - (CPArray)_completionsForSubstring:(CPString)substring indexOfToken:(int)tokenIndex indexOfSelectedItem:(int)selectedIndex
1261 {
1263  return [];
1264 
1265  return [_tokenFieldDelegate tokenField:self completionsForSubstring:substring indexOfToken:tokenIndex indexOfSelectedItem:selectedIndex];
1266 }
1267 
1271 - (CGPoint)_completionOrigin:(_CPAutocompleteMenu)anAutocompleteMenu
1272 {
1273  var relativeFrame = _inputFrame ? [[_tokenScrollView documentView] convertRect:_inputFrame toView:self ] : [self bounds];
1274 
1275  return CGPointMake(CGRectGetMinX(relativeFrame), CGRectGetMaxY(relativeFrame));
1276 }
1277 
1286 - (CPString)_displayStringForRepresentedObject:(id)representedObject
1287 {
1288  if (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_displayStringForRepresentedObject_)
1289  {
1290  var stringForRepresentedObject = [_tokenFieldDelegate tokenField:self displayStringForRepresentedObject:representedObject];
1291 
1292  if (stringForRepresentedObject !== nil)
1293  return stringForRepresentedObject;
1294  }
1295 
1296  return representedObject;
1297 }
1298 
1308 - (CPArray)_shouldAddObjects:(CPArray)tokens atIndex:(int)index
1309 {
1310  if (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_shouldAddObjects_atIndex_)
1311  {
1312  var approvedObjects = [_tokenFieldDelegate tokenField:self shouldAddObjects:tokens atIndex:index];
1313 
1314  if (approvedObjects !== nil)
1315  return approvedObjects;
1316  }
1317 
1318  return tokens;
1319 }
1320 
1330 - (id)_representedObjectForEditingString:(CPString)aString
1331 {
1332  if (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_representedObjectForEditingString_)
1333  {
1334  var token = [_tokenFieldDelegate tokenField:self representedObjectForEditingString:aString];
1335 
1336  if (token !== nil && token !== undefined)
1337  return token;
1338  // If nil was returned, assume the string is the represented object. The alternative would have been
1339  // to not add anything to the object value array for a nil response.
1340  }
1341 
1342  return aString;
1343 }
1344 
1345 - (BOOL)_hasMenuForRepresentedObject:(id)aRepresentedObject
1346 {
1347  if ((_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_hasMenuForRepresentedObject_) &&
1348  (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_menuForRepresentedObject_))
1349  return [_tokenFieldDelegate tokenField:self hasMenuForRepresentedObject:aRepresentedObject];
1350 
1351  return NO;
1352 }
1353 
1354 - (CPMenu)_menuForRepresentedObject:(id)aRepresentedObject
1355 {
1356  if ((_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_hasMenuForRepresentedObject_) &&
1357  (_implementedTokenFieldDelegateMethods & CPTokenFieldDelegate_tokenField_menuForRepresentedObject_))
1358  {
1359  var hasMenu = [_tokenFieldDelegate tokenField:self hasMenuForRepresentedObject:aRepresentedObject];
1360 
1361  if (hasMenu)
1362  return [_tokenFieldDelegate tokenField:self menuForRepresentedObject:aRepresentedObject] || nil;
1363  }
1364 
1365  return nil;
1366 }
1367 
1368 // We put the string on the pasteboard before calling this delegate method.
1369 // By default, we write the NSStringPboardType as well as an array of NSStrings.
1370 // - (BOOL)tokenField:(NSTokenField *)tokenField writeRepresentedObjects:(NSArray *)objects toPasteboard:(NSPasteboard *)pboard;
1371 //
1372 // Return an array of represented objects to add to the token field.
1373 // - (NSArray *)tokenField:(NSTokenField *)tokenField readFromPasteboard:(NSPasteboard *)pboard;
1374 //
1375 // By default the tokens have no menu.
1376 // - (NSMenu *)tokenField:(NSTokenField *)tokenField menuForRepresentedObject:(id)representedObject;
1377 // - (BOOL)tokenField:(NSTokenField *)tokenField hasMenuForRepresentedObject:(id)representedObject;
1378 //
1379 // This method allows you to change the style for individual tokens as well as have mixed text and tokens.
1380 // - (NSTokenStyle)tokenField:(NSTokenField *)tokenField styleForRepresentedObject:(id)representedObject;
1381 
1382 - (void)_delayedShowCompletions
1383 {
1384  [[self _autocompleteMenu] _delayedShowCompletions];
1385 }
1386 
1387 - (void)_hideCompletions
1388 {
1389  [_autocompleteMenu _hideCompletions];
1390 }
1391 
1392 
1393 - (void)setButtonType:(int)aButtonType
1394 {
1395  if (_buttonType === aButtonType)
1396  return;
1397 
1398  _buttonType = aButtonType;
1399  [self setNeedsLayout];
1400 }
1401 
1402 @end
1403 
1404 @implementation _CPTokenFieldToken : CPTextField
1405 {
1406  _CPTokenFieldTokenCloseButton _deleteButton;
1407  _CPTokenFieldTokenDisclosureButton _disclosureButton;
1408  CPTokenField _tokenField;
1409  id _representedObject;
1410  int _buttonType;
1411 }
1412 
1414 {
1415  return "tokenfield-token";
1416 }
1417 
1418 - (BOOL)acceptsFirstResponder
1419 {
1420  return NO;
1421 }
1422 
1423 - (id)initWithFrame:(CGRect)frame
1424 {
1425  if (self = [super initWithFrame:frame])
1426  {
1427  [self setEditable:NO];
1428  [self setHighlighted:NO];
1429  [self setBezeled:YES];
1430  [self setButtonType:CPTokenFieldDisclosureButtonType];
1431  }
1432 
1433  return self;
1434 }
1435 
1436 - (CPTokenField)tokenField
1437 {
1438  return _tokenField;
1439 }
1440 
1441 - (void)setTokenField:(CPTokenField)tokenField
1442 {
1443  _tokenField = tokenField;
1444 }
1445 
1446 - (id)representedObject
1447 {
1448  return _representedObject;
1449 }
1450 
1451 - (void)setRepresentedObject:(id)representedObject
1452 {
1453  _representedObject = representedObject;
1454  [self setNeedsLayout];
1455 }
1456 
1457 - (void)setEditable:(BOOL)shouldBeEditable
1458 {
1459  [super setEditable:shouldBeEditable];
1460  [self setNeedsLayout];
1461 }
1462 
1463 - (BOOL)setThemeState:(ThemeState)aState
1464 {
1465  var r = [super setThemeState:aState];
1466 
1467  // Share hover state with the disclosure and delete buttons.
1468  if (aState.hasThemeState(CPThemeStateHovered))
1469  {
1470  [_disclosureButton setThemeState:CPThemeStateHovered];
1471  [_deleteButton setThemeState:CPThemeStateHovered];
1472  }
1473 
1474  return r;
1475 }
1476 
1477 - (BOOL)unsetThemeState:(ThemeState)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 
1616 {
1617  var attributes = [CPButton themeAttributes];
1618 
1619  [attributes setObject:CGPointMake(15, 5) forKey:@"offset"];
1620 
1621  return attributes;
1622 }
1623 
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 
1646 {
1647  var attributes = [CPButton themeAttributes];
1648 
1649  [attributes setObject:CGPointMake(15, 5) forKey:@"offset"];
1650 
1651  return attributes;
1652 }
1653 
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 
1735 {
1736  return _tokenizingCharacterSet;
1737 }
1738 
1742 - (void)setTokenizingCharacterSet:(CPCharacterSet)aValue
1743 {
1744  _tokenizingCharacterSet = aValue;
1745 }
1746 
1751 {
1752  return _buttonType;
1753 }
1754 
1758 - (void)setButtonType:(int)aValue
1759 {
1760  _buttonType = aValue;
1761 }
1762 
1763 @end
CPThemeStateEditing
Definition: CPTheme.j:555
CPThemeStateHovered
Definition: CPTheme.j:546
BOOL makeFirstResponder:(CPResponder aResponder)
Definition: CPWindow.j:1617
Definition: CPMenu.h:2
void addSubview:(CPView aSubview)
Definition: CPView.j:512
function CPUnionRange(lhsRange, rhsRange)
Definition: CPRange.j:106
void encodeWithCoder:(CPCoder aCoder)
Definition: CPTextField.j:2084
float defaultLineHeightForFont()
Definition: CPFont.j:362
BOOL isEnabled()
Definition: CPControl.j:970
void textDidChange:(CPNotification note)
Definition: CPTextField.j:1212
BOOL setThemeState:(ThemeState aState)
Definition: CPView.j:3214
var CPScrollDestinationRight
Definition: CPTokenField.j:72
CPDictionary themeAttributes()
Definition: CPTokenField.j:116
BOOL becomeFirstResponder()
Definition: CPTokenField.j:372
var CPTokenFieldDelegate_tokenField_menuForRepresentedObject_
Definition: CPTokenField.j:47
The main run loop for the application.
Definition: CPRunLoop.h:2
CPRightTextAlignment
Definition: CPText.j:74
CPThemeStateAutocompleting
Definition: CPTheme.j:559
void setEditable:(BOOL shouldBeEditable)
Definition: CPTokenField.j:700
CPResponder firstResponder()
Definition: CPWindow.j:1642
var CPTokenFieldTokenizingCharacterSetKey
CPFont font()
Definition: CPControl.j:899
CPDictionary themeAttributes()
Definition: CPButton.j:145
int width
CGRect bounds()
Definition: CPView.j:1302
var CPTokenFieldDelegate_tokenField_displayStringForRepresentedObject_
Definition: CPTokenField.j:48
CPTextFieldDidBlurNotification
Definition: CPTextField.j:46
void removeFromSuperview()
Definition: CPView.j:654
CPString stringValue()
Definition: CPTokenField.j:578
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)
CPString defaultThemeClass()
Definition: CPTokenField.j:111
function ThemeState(stateNames)
Definition: CPTheme.j:314
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:1188
CPWindow window()
Definition: CPView.j:503
CPTimeInterval completionDelay()
CPTabCharacter
Definition: CPText.j:48
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
void setButtonType:(int aValue)
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:610
void performBlock:argument:order:modes:(Function aBlock, [argument] id anArgument, [order] int anOrder, [modes] CPArray modes)
Definition: CPRunLoop.j:270
BOOL acceptsFirstResponder()
Definition: CPTextField.j:590
void textDidBeginEditing:(CPNotification note)
Definition: CPTextField.j:1225
CGRect contentRectForBounds:(CGRect bounds)
Definition: CPTextField.j:1802
BOOL isEditable()
Definition: CPTextField.j:403
BOOL performKeyEquivalent:(CPEvent anEvent)
Definition: CPView.j:3013
void setNeedsDisplay:(BOOL aFlag)
Definition: CPView.j:2556
CGRect rectForEphemeralSubviewNamed:(CPString aName)
Definition: CPTextField.j:1816
CPTextAlignment alignment()
Definition: CPControl.j:784
CPShiftKeyMask
var CPTokenFieldButtonTypeKey
A notification that can be posted to a CPNotificationCenter.
Definition: CPNotification.h:2
void setHighlighted:(BOOL isHighlighted)
Definition: CPControl.j:990
CPDate limitDateForMode:(CPString aMode)
Definition: CPRunLoop.j:342
void setNeedsLayout()
Definition: CPView.j:2707
void setObjectValue:(id aValue)
Definition: CPTextField.j:1304
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
BOOL isSelectable()
Definition: CPTextField.j:437
CPNotFound
Definition: CPObjJRuntime.j:62
CPInputTypeCanBeChangedFeature
CPString cssString()
Definition: CPFont.j:383
BOOL unsetThemeState:(ThemeState aState)
Definition: CPView.j:3227
var CPScrollDestinationLeft
Definition: CPTokenField.j:71
function CPLocationInRange(aLocation, aRange)
Definition: CPRange.j:93
void selectPreviousKeyView:(id sender)
Definition: CPWindow.j:3244
CPNotification notificationWithName:object:userInfo:(CPString aNotificationName, [object] id anObject, [userInfo] CPDictionary aUserInfo)
void layoutSubviews()
CPNewlineCharacter
Definition: CPText.j:49
void mouseDown:(CPEvent anEvent)
Definition: CPTextField.j:990
void setBezeled:(BOOL shouldBeBezeled)
Definition: CPTextField.j:464
void textDidEndEditing:(CPNotification note)
Definition: CPTextField.j:1237
CPCarriageReturnCharacter
Definition: CPText.j:51
BOOL sendAction:to:(SEL anAction, [to] id anObject)
Definition: CPTokenField.j:707
CPTextFieldStatePlaceholder
Definition: CPTextField.j:111
CPTimeInterval defaultCompletionDelay()
Definition: CPTokenField.j:106
Definition: CPEvent.h:2
void setBordered:(BOOL shouldBeBordered)
Definition: CPTextField.j:509
BOOL resignFirstResponder()
Definition: CPTokenField.j:478
CPCharacterSet defaultTokenizingCharacterSet()
Definition: CPTokenField.j:101
var CPScrollDestinationNone
Definition: CPTokenField.j:70
CPCenterTextAlignment
Definition: CPText.j:75
CPPlatformWindow platformWindow()
Definition: CPWindow.j:384
CGRect frame()
Definition: CPView.j:1022
var CPTokenFieldDelegate_tokenField_shouldAddObjects_atIndex_
Definition: CPTokenField.j:46
CPTokenFieldDisclosureButtonType
Definition: CPTokenField.j:74
CPTextFieldDidFocusNotification
Definition: CPTextField.j:45
BOOL isSecure()
Definition: CPTextField.j:454
BOOL scrollRectToVisible:(CGRect aRect)
Definition: CPView.j:2821
void layoutSubviews()
Definition: CPTextField.j:1849
id initWithCoder:(CPCoder aCoder)
Definition: CPTextField.j:2054
void setDelegate:(id< CPTextFieldDelegate > aDelegate)
Definition: CPTextField.j:1770
void textDidFocus:(CPNotification note)
Definition: CPTextField.j:1200
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:3366
CPMenu menu
CPString characters()
Definition: CPEvent.j:382
Definition: CPView.j:136
void textDidChange:(CPNotification aNotification)