PageRenderTime 50ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/AppKit/CPTokenField.j

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