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