API  0.9.7
 All Classes Files Functions Variables Macros Groups Pages
CPTextField.j
Go to the documentation of this file.
1 /*
2  * CPTextField.j
3  * AppKit
4  *
5  * Created by Francisco Tolmasky.
6  * Copyright 2008, 280 North, Inc.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public
10  * License as published by the Free Software Foundation; either
11  * version 2.1 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21  */
22 
23 
24 @class CPPasteboard
25 
26 @global CPApp
27 @global CPStringPboardType
28 
29 CPTextFieldSquareBezel = 0;
30 CPTextFieldRoundedBezel = 1;
32 CPTextFieldDidFocusNotification = @"CPTextFieldDidFocusNotification";
33 CPTextFieldDidBlurNotification = @"CPTextFieldDidBlurNotification";
34 
35 var CPTextFieldDOMInputElement = nil,
36  CPTextFieldDOMPasswordInputElement = nil,
37  CPTextFieldDOMStandardInputElement = nil,
38  CPTextFieldInputOwner = nil,
39  CPTextFieldTextDidChangeValue = nil,
40  CPTextFieldInputResigning = NO,
41  CPTextFieldInputDidBlur = NO,
42  CPTextFieldInputIsActive = NO,
43  CPTextFieldCachedSelectStartFunction = nil,
44  CPTextFieldCachedDragFunction = nil,
45  CPTextFieldBlurHandler = nil,
46  CPTextFieldInputFunction = nil;
47 
48 var CPSecureTextFieldCharacter = "\u2022";
49 
50 
51 function CPTextFieldBlurFunction(anEvent, owner, domElement, inputElement, resigning, didBlurRef)
52 {
53  if (owner && domElement != inputElement.parentNode)
54  return;
55 
56  var ownerWindow = [owner window];
57 
58  if (!resigning && [ownerWindow isKeyWindow])
59  {
60  /*
61  Browsers blur text fields when a click occurs anywhere outside the text field. That is normal for browsers, but in Cocoa the key view retains focus unless the click target accepts first responder. So if we lost focus but were not told to resign and our window is still key, restore focus,
62  but only if the text field is completely within the browser window. If we restore focus when it
63  is off screen, the entire body scrolls out of our control.
64  */
65  if ([owner _isWithinUsablePlatformRect])
66  {
67  window.setTimeout(function()
68  {
69  inputElement.focus();
70  }, 0.0);
71  }
72  }
73 
74  CPTextFieldHandleBlur(anEvent, @ref(owner));
75  @deref(didBlurRef) = YES;
76 
77  return true;
78 }
79 
80 function CPTextFieldHandleBlur(anEvent, ownerRef)
81 {
82  @deref(ownerRef) = nil;
83 
84  [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
85 }
86 
87 
88 @implementation CPString (CPTextFieldAdditions)
89 
93 - (CPString)string
94 {
95  return self;
96 }
97 
98 @end
99 
100 CPTextFieldStateRounded = CPThemeState("rounded");
101 CPTextFieldStatePlaceholder = CPThemeState("placeholder");
102 
107 @implementation CPTextField : CPControl
108 {
109  BOOL _isEditing;
110 
111  BOOL _isEditable;
112  BOOL _isSelectable;
113  BOOL _isSecure;
114  BOOL _willBecomeFirstResponderByClick;
115 
116  BOOL _drawsBackground;
117 
118  CPColor _textFieldBackgroundColor;
119 
120  CPString _placeholderString;
121  CPString _stringValue;
122 
123  id _delegate;
124 
125  // NS-style Display Properties
126  CPTextFieldBezelStyle _bezelStyle;
127  BOOL _isBordered;
128  CPControlSize _controlSize;
129 }
130 
131 + (Class)_binderClassForBinding:(CPString)aBinding
132 {
133  if (aBinding === CPValueBinding)
134  return [_CPTextFieldValueBinder class];
135  else if ([aBinding hasPrefix:CPDisplayPatternValueBinding])
136  return [_CPTextFieldPatternValueBinder class];
137  else if ([aBinding hasPrefix:CPEditableBinding])
138  return [CPMultipleValueAndBinding class];
139 
140  return [super _binderClassForBinding:aBinding];
141 }
142 
143 + (CPTextField)textFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth
144 {
145  return [self textFieldWithStringValue:aStringValue placeholder:aPlaceholder width:aWidth theme:[CPTheme defaultTheme]];
146 }
147 
148 + (CPTextField)textFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth theme:(CPTheme)aTheme
149 {
150  var textField = [[self alloc] initWithFrame:CGRectMake(0.0, 0.0, aWidth, 29.0)];
151 
152  [textField setTheme:aTheme];
153  [textField setStringValue:aStringValue];
154  [textField setPlaceholderString:aPlaceholder];
155  [textField setBordered:YES];
156  [textField setBezeled:YES];
157  [textField setEditable:YES];
158 
159  [textField sizeToFit];
160 
161  return textField;
162 }
163 
164 + (CPTextField)roundedTextFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth
165 {
166  return [self roundedTextFieldWithStringValue:aStringValue placeholder:aPlaceholder width:aWidth theme:[CPTheme defaultTheme]];
167 }
168 
169 + (CPTextField)roundedTextFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth theme:(CPTheme)aTheme
170 {
171  var textField = [[CPTextField alloc] initWithFrame:CGRectMake(0.0, 0.0, aWidth, 29.0)];
172 
173  [textField setTheme:aTheme];
174  [textField setStringValue:aStringValue];
175  [textField setPlaceholderString:aPlaceholder];
176  [textField setBezelStyle:CPTextFieldRoundedBezel];
177  [textField setBordered:YES];
178  [textField setBezeled:YES];
179  [textField setEditable:YES];
180 
181  [textField sizeToFit];
182 
183  return textField;
184 }
185 
186 + (CPTextField)labelWithTitle:(CPString)aTitle
187 {
188  return [self labelWithTitle:aTitle theme:[CPTheme defaultTheme]];
189 }
190 
191 + (CPTextField)labelWithTitle:(CPString)aTitle theme:(CPTheme)aTheme
192 {
193  var textField = [[self alloc] init];
194 
195  [textField setStringValue:aTitle];
196  [textField sizeToFit];
197 
198  return textField;
199 }
200 
201 + (CPString)defaultThemeClass
202 {
203  return "textfield";
204 }
205 
206 + (CPDictionary)themeAttributes
207 {
208  return @{
209  @"bezel-inset": CGInsetMakeZero(),
210  @"content-inset": CGInsetMake(1.0, 0.0, 0.0, 0.0),
211  @"bezel-color": [CPNull null],
212  };
213 }
214 
215 #if PLATFORM(DOM)
216 - (DOMElement)_inputElement
217 {
218  if (!CPTextFieldDOMInputElement)
219  {
220  CPTextFieldDOMInputElement = document.createElement("input");
221  CPTextFieldDOMInputElement.style.position = "absolute";
222  CPTextFieldDOMInputElement.style.border = "0px";
223  CPTextFieldDOMInputElement.style.padding = "0px";
224  CPTextFieldDOMInputElement.style.margin = "0px";
225  CPTextFieldDOMInputElement.style.whiteSpace = "pre";
226  CPTextFieldDOMInputElement.style.background = "transparent";
227  CPTextFieldDOMInputElement.style.outline = "none";
228 
229  CPTextFieldBlurHandler = function(anEvent)
230  {
231  return CPTextFieldBlurFunction(
232  anEvent,
233  CPTextFieldInputOwner,
234  CPTextFieldInputOwner ? CPTextFieldInputOwner._DOMElement : nil,
235  CPTextFieldDOMInputElement,
236  CPTextFieldInputResigning,
237  @ref(CPTextFieldInputDidBlur));
238  };
239 
241  {
242  CPTextFieldInputFunction = function(anEvent)
243  {
244  if (!CPTextFieldInputOwner)
245  return;
246 
247  var cappEvent = [CPEvent keyEventWithType:CPKeyUp
248  location:CGPointMakeZero()
249  modifierFlags:0
250  timestamp:[CPEvent currentTimestamp]
251  windowNumber:[[CPApp keyWindow] windowNumber]
252  context:nil
253  characters:nil
254  charactersIgnoringModifiers:nil
255  isARepeat:NO
256  keyCode:nil];
257 
258  [CPTextFieldInputOwner keyUp:cappEvent];
259 
260  [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
261  }
262 
263  CPTextFieldDOMInputElement.oninput = CPTextFieldInputFunction;
264  }
265 
266  // FIXME make this not onblur
267  CPTextFieldDOMInputElement.onblur = CPTextFieldBlurHandler;
268 
269  CPTextFieldDOMStandardInputElement = CPTextFieldDOMInputElement;
270  }
271 
273  {
274  if ([self isSecure])
275  CPTextFieldDOMInputElement.type = "password";
276  else
277  CPTextFieldDOMInputElement.type = "text";
278 
279  return CPTextFieldDOMInputElement;
280  }
281 
282  if ([self isSecure])
283  {
284  if (!CPTextFieldDOMPasswordInputElement)
285  {
286  CPTextFieldDOMPasswordInputElement = document.createElement("input");
287  CPTextFieldDOMPasswordInputElement.style.position = "absolute";
288  CPTextFieldDOMPasswordInputElement.style.border = "0px";
289  CPTextFieldDOMPasswordInputElement.style.padding = "0px";
290  CPTextFieldDOMPasswordInputElement.style.margin = "0px";
291  CPTextFieldDOMPasswordInputElement.style.whiteSpace = "pre";
292  CPTextFieldDOMPasswordInputElement.style.background = "transparent";
293  CPTextFieldDOMPasswordInputElement.style.outline = "none";
294  CPTextFieldDOMPasswordInputElement.type = "password";
295 
296  CPTextFieldDOMPasswordInputElement.onblur = CPTextFieldBlurHandler;
297  }
298 
299  CPTextFieldDOMInputElement = CPTextFieldDOMPasswordInputElement;
300  }
301  else
302  {
303  CPTextFieldDOMInputElement = CPTextFieldDOMStandardInputElement;
304  }
305 
306  return CPTextFieldDOMInputElement;
307 }
308 #endif
309 
310 - (id)initWithFrame:(CGRect)aFrame
311 {
312  self = [super initWithFrame:aFrame];
313 
314  if (self)
315  {
316  [self setStringValue:@""];
317  [self setPlaceholderString:@""];
318 
319  _sendActionOn = CPKeyUpMask | CPKeyDownMask;
320 
321  [self setValue:CPLeftTextAlignment forThemeAttribute:@"alignment"];
322  }
323 
324  return self;
325 }
326 
327 #pragma mark Controlling Editability and Selectability
328 
333 - (void)setEditable:(BOOL)shouldBeEditable
334 {
335  if (_isEditable === shouldBeEditable)
336  return;
337 
338  _isEditable = shouldBeEditable;
339 
340  if (shouldBeEditable)
341  _isSelectable = YES;
342 
343  if (_isEditable)
344  [self setThemeState:CPThemeStateEditable];
345  else
346  [self unsetThemeState:CPThemeStateEditable];
347 
348  // We only allow first responder status if the field is enable, and editable or selectable.
349  if (!(shouldBeEditable && ![self isSelectable]) && [[self window] firstResponder] === self)
350  [[self window] makeFirstResponder:nil];
351 
352  if (shouldBeEditable)
353  [self setThemeState:CPThemeStateEditable];
354  else
355  [self unsetThemeState:CPThemeStateEditable];
356 }
357 
361 - (BOOL)isEditable
362 {
363  return _isEditable;
364 }
365 
370 - (void)setEnabled:(BOOL)shouldBeEnabled
371 {
372  [super setEnabled:shouldBeEnabled];
373 
374  // We only allow first responder status if the field is enabled.
375  if (!shouldBeEnabled && [[self window] firstResponder] === self)
376  [[self window] makeFirstResponder:nil];
377 }
378 
383 - (void)setSelectable:(BOOL)aFlag
384 {
385  _isSelectable = aFlag;
386 }
387 
391 - (BOOL)isSelectable
392 {
393  return _isSelectable;
394 }
395 
400 - (void)setSecure:(BOOL)aFlag
401 {
402  _isSecure = aFlag;
403 }
404 
408 - (BOOL)isSecure
409 {
410  return _isSecure;
411 }
412 
413 // Setting the Bezel Style
418 - (void)setBezeled:(BOOL)shouldBeBezeled
419 {
420  if (shouldBeBezeled)
421  [self setThemeState:CPThemeStateBezeled];
422  else
423  [self unsetThemeState:CPThemeStateBezeled];
424 }
425 
429 - (BOOL)isBezeled
430 {
431  return [self hasThemeState:CPThemeStateBezeled];
432 }
433 
438 - (void)setBezelStyle:(CPTextFieldBezelStyle)aBezelStyle
439 {
440  var shouldBeRounded = aBezelStyle === CPTextFieldRoundedBezel;
441 
442  if (shouldBeRounded)
443  [self setThemeState:CPTextFieldStateRounded];
444  else
445  [self unsetThemeState:CPTextFieldStateRounded];
446 }
447 
451 - (CPTextFieldBezelStyle)bezelStyle
452 {
453  if ([self hasThemeState:CPTextFieldStateRounded])
454  return CPTextFieldRoundedBezel;
455 
456  return CPTextFieldSquareBezel;
457 }
458 
463 - (void)setBordered:(BOOL)shouldBeBordered
464 {
465  if (shouldBeBordered)
466  [self setThemeState:CPThemeStateBordered];
467  else
468  [self unsetThemeState:CPThemeStateBordered];
469 }
470 
474 - (BOOL)isBordered
475 {
476  return [self hasThemeState:CPThemeStateBordered];
477 }
478 
483 - (void)setDrawsBackground:(BOOL)shouldDrawBackground
484 {
485  if (_drawsBackground == shouldDrawBackground)
486  return;
487 
488  _drawsBackground = shouldDrawBackground;
489 
490  [self setNeedsLayout];
491  [self setNeedsDisplay:YES];
492 }
493 
497 - (BOOL)drawsBackground
498 {
499  return _drawsBackground;
500 }
501 
506 - (void)setTextFieldBackgroundColor:(CPColor)aColor
507 {
508  if (_textFieldBackgroundColor == aColor)
509  return;
510 
511  _textFieldBackgroundColor = aColor;
512 
513  [self setNeedsLayout];
514  [self setNeedsDisplay:YES];
515 }
516 
520 - (CPColor)textFieldBackgroundColor
521 {
522  return _textFieldBackgroundColor;
523 }
524 
526 - (BOOL)acceptsFirstResponder
527 {
528  return [self isEnabled] && ([self isEditable] || [self isSelectable]) && [self _isWithinUsablePlatformRect];
529 }
530 
532 - (BOOL)becomeFirstResponder
533 {
534  if (![self isEnabled])
535  return NO;
536 
537  // As long as we are the first responder we need to monitor the key status of our window.
538  [self _setObserveWindowKeyNotifications:YES];
539 
540  _isEditing = NO;
541 
542  if ([[self window] isKeyWindow] && [self isEditable])
543  return [self _becomeFirstKeyResponder];
544 
545  return YES;
546 }
547 
548 /*
549  A text field can be the first responder without necessarily being the focus of keyboard input. For example, it might be the first responder of window A but window B is the main and key window. It's important we don't put a focused input field into a text field in a non-key window, even if that field is the first responder, because the key window might also have a first responder text field which the user will expect to receive keyboard input.
550 
551  Since a first responder but non-key window text field can't receive input it should not even look like an active text field (Cocoa has a "slightly active" text field look it uses when another window is the key window, but Cappuccino doesn't today.)
552 
553  It's also possible for a text field to be non-editable but selectable in which case it can also become the first responder -
554  this is what allows text to be copied from it.
555 */
556 - (BOOL)_becomeFirstKeyResponder
557 {
558  // If the text field is still not completely on screen, refuse to become
559  // first responder, because the browser will scroll it into view out of our control.
560  if (![self _isWithinUsablePlatformRect])
561  return NO;
562 
563  // A selectable but non-editable text field may be the first responder, but never the
564  // first key responder (first key responder indicating editability.)
565  if (![self isEditable])
566  return NO;
567 
568  [self setThemeState:CPThemeStateEditing];
569 
570  [self _updatePlaceholderState];
571 
572  [self setNeedsLayout];
573 
574  _stringValue = [self stringValue];
575 
576 #if PLATFORM(DOM)
577 
578  var element = [self _inputElement],
579  font = [self currentValueForThemeAttribute:@"font"],
580  lineHeight = [font defaultLineHeightForFont];
581 
582  element.value = _stringValue;
583  element.style.color = [[self currentValueForThemeAttribute:@"text-color"] cssString];
584 
586  element.style.font = [font cssString];
587 
588  element.style.zIndex = 1000;
589 
590  switch ([self alignment])
591  {
593  element.style.textAlign = "center";
594  break;
595 
597  element.style.textAlign = "right";
598  break;
599 
600  default:
601  element.style.textAlign = "left";
602  }
603 
604  var contentRect = [self contentRectForBounds:[self bounds]],
605  verticalAlign = [self currentValueForThemeAttribute:"vertical-alignment"];
606 
607  switch (verticalAlign)
608  {
610  var topPoint = CGRectGetMinY(contentRect) + "px";
611  break;
612 
614  var topPoint = (CGRectGetMidY(contentRect) - (lineHeight / 2)) + "px";
615  break;
616 
618  var topPoint = (CGRectGetMaxY(contentRect) - lineHeight) + "px";
619  break;
620 
621  default:
622  var topPoint = CGRectGetMinY(contentRect) + "px";
623  break;
624  }
625 
626  element.style.top = topPoint;
627 
628  var left = CGRectGetMinX(contentRect);
629 
630  // If the browser has a built in left padding, compensate for it. We need the input text to be exactly on top of the original text.
632  left -= 1;
633 
634  element.style.left = left + "px";
635  element.style.width = CGRectGetWidth(contentRect) + "px";
636  element.style.height = ROUND(lineHeight) + "px";
637  element.style.lineHeight = ROUND(lineHeight) + "px";
638  element.style.verticalAlign = "top";
639  element.style.cursor = "auto";
640 
641  _DOMElement.appendChild(element);
642 
643  // The font change above doesn't work for some browsers if the element isn't already appendChild'ed.
645  element.style.font = [font cssString];
646 
647  CPTextFieldInputIsActive = YES;
648 
649  if (document.attachEvent)
650  {
651  CPTextFieldCachedSelectStartFunction = [[self window] platformWindow]._DOMBodyElement.onselectstart;
652  CPTextFieldCachedDragFunction = [[self window] platformWindow]._DOMBodyElement.ondrag;
653 
654  [[self window] platformWindow]._DOMBodyElement.ondrag = function () {};
655  [[self window] platformWindow]._DOMBodyElement.onselectstart = function () {};
656  }
657 
658  CPTextFieldInputOwner = self;
659 
660  window.setTimeout(function()
661  {
662  /*
663  setTimeout handlers are not guaranteed to fire in the order they were initiated. This can cause a race condition when several windows with text fields are opened quickly, resulting in several instances of this timeout function being fired, perhaps out of order. So we have to check that by the time this function is fired, CPTextFieldInputOwner has not been changed to another text field in the meantime.
664  */
665  if (CPTextFieldInputOwner !== self)
666  return;
667 
668  element.focus();
669 
670  // Select the text if the textfield became first responder through keyboard interaction
671  if (!_willBecomeFirstResponderByClick)
672  [self _selectText:self immediately:YES];
673 
674  _willBecomeFirstResponderByClick = NO;
675 
676  [self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]];
677  }, 0.0);
678 
679 #endif
680 
681  return YES;
682 }
683 
685 - (BOOL)resignFirstResponder
686 {
687 #if PLATFORM(DOM)
688  // We might have been the first responder without actually editing.
689  if (_isEditing && CPTextFieldInputOwner === self)
690  {
691  var element = [self _inputElement],
692  newValue = element.value,
693  error = @"";
694 
695  if (newValue !== _stringValue)
696  {
697  [self _setStringValue:newValue];
698  }
699 
700  // If there is a formatter, always give it a chance to reject the resignation,
701  // even if the value has not changed.
702  if ([self _valueIsValid:newValue] === NO)
703  {
704  element.focus();
705  return NO;
706  }
707  }
708 #endif
709 
710  // When we are no longer the first responder we don't worry about the key status of our window anymore.
711  [self _setObserveWindowKeyNotifications:NO];
712 
713  [self _resignFirstKeyResponder];
714 
715  _isEditing = NO;
716  if ([self isEditable])
717  {
718  [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidEndEditingNotification object:self userInfo:@{"CPTextMovement": [self _currentTextMovement]}]];
719 
720  if ([self sendsActionOnEndEditing])
721  [self sendAction:[self action] to:[self target]];
722  }
723 
724  [self textDidBlur:[CPNotification notificationWithName:CPTextFieldDidBlurNotification object:self userInfo:nil]];
725 
726  return YES;
727 }
728 
729 - (void)_resignFirstKeyResponder
730 {
731  [self unsetThemeState:CPThemeStateEditing];
732 
733  // Cache the formatted string
734  _stringValue = [self stringValue];
735 
736  _willBecomeFirstResponderByClick = NO;
737 
738  [self _updatePlaceholderState];
739  [self setNeedsLayout];
740 
741 #if PLATFORM(DOM)
742 
743  var element = [self _inputElement];
744 
745  CPTextFieldInputResigning = YES;
746 
747  if (CPTextFieldInputIsActive)
748  element.blur();
749 
750  if (!CPTextFieldInputDidBlur)
751  CPTextFieldBlurHandler();
752 
753  CPTextFieldInputDidBlur = NO;
754  CPTextFieldInputResigning = NO;
755 
756  if (element.parentNode == _DOMElement)
757  element.parentNode.removeChild(element);
758 
759  CPTextFieldInputIsActive = NO;
760 
761  if (document.attachEvent)
762  {
763  [[self window] platformWindow]._DOMBodyElement.ondrag = CPTextFieldCachedDragFunction;
764  [[self window] platformWindow]._DOMBodyElement.onselectstart = CPTextFieldCachedSelectStartFunction;
765 
766  CPTextFieldCachedSelectStartFunction = nil;
767  CPTextFieldCachedDragFunction = nil;
768  }
769 
770 #endif
771 }
772 
773 - (void)_setObserveWindowKeyNotifications:(BOOL)shouldObserve
774 {
775  if (shouldObserve)
776  {
777  [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_windowDidResignKey:) name:CPWindowDidResignKeyNotification object:[self window]];
778  [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_windowDidBecomeKey:) name:CPWindowDidBecomeKeyNotification object:[self window]];
779  }
780  else
781  {
782  [[CPNotificationCenter defaultCenter] removeObserver:self name:CPWindowDidResignKeyNotification object:[self window]];
783  [[CPNotificationCenter defaultCenter] removeObserver:self name:CPWindowDidBecomeKeyNotification object:[self window]];
784  }
785 }
786 
787 - (void)_windowDidResignKey:(CPNotification)aNotification
788 {
789  if (![[self window] isKeyWindow])
790  [self _resignFirstKeyResponder];
791 }
792 
793 - (void)_windowDidBecomeKey:(CPNotification)aNotification
794 {
795  if (!([self isEnabled] && [self isEditable]))
796  return;
797 
798  var wind = [self window];
799 
800  if ([wind isKeyWindow] && [wind firstResponder] === self)
801  if (![self _becomeFirstKeyResponder])
802  [wind makeFirstResponder:nil];
803 }
804 
805 - (BOOL)_valueIsValid:(CPString)aValue
806 {
807 #if PLATFORM(DOM)
808 
809  var error = @"";
810 
811  if ([self _setStringValue:aValue isNewValue:NO errorDescription:@ref(error)] === NO)
812  {
813  var acceptInvalidValue = NO;
814 
815  if ([_delegate respondsToSelector:@selector(control:didFailToFormatString:errorDescription:)])
816  acceptInvalidValue = [_delegate control:self didFailToFormatString:aValue errorDescription:error];
817 
818  if (acceptInvalidValue === NO)
819  return NO;
820  }
821 
822 #endif
823 
824  return YES;
825 }
826 
831 - (BOOL)needsPanelToBecomeKey
832 {
833  return [self acceptsFirstResponder];
834 }
835 
839 - (BOOL)acceptsFirstMouse:(CPEvent)anEvent
840 {
841  return [self acceptsFirstResponder];
842 }
843 
844 - (void)_didEdit
845 {
846  if (!_isEditing)
847  {
848  _isEditing = YES;
849  [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
850  }
851 
852  [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
853 }
854 
855 - (void)mouseDown:(CPEvent)anEvent
856 {
857  // Don't track! (ever?)
858  if ([self isEditable] && [self isEnabled])
859  {
860  _willBecomeFirstResponderByClick = YES;
861  [[self window] makeFirstResponder:self];
862  }
863  else if ([self isSelectable])
864  {
865  if (document.attachEvent)
866  {
867  CPTextFieldCachedSelectStartFunction = [[self window] platformWindow]._DOMBodyElement.onselectstart;
868  CPTextFieldCachedDragFunction = [[self window] platformWindow]._DOMBodyElement.ondrag;
869 
870  [[self window] platformWindow]._DOMBodyElement.ondrag = function () {};
871  [[self window] platformWindow]._DOMBodyElement.onselectstart = function () {};
872  }
873  return [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:YES];
874  }
875  else
876  return [[self nextResponder] mouseDown:anEvent];
877 }
878 
879 - (void)mouseUp:(CPEvent)anEvent
880 {
881  if (![self isEnabled] || !([self isSelectable] || [self isEditable]))
882  [[self nextResponder] mouseUp:anEvent];
883  else if ([self isSelectable])
884  {
885  if (document.attachEvent)
886  {
887  [[self window] platformWindow]._DOMBodyElement.ondrag = CPTextFieldCachedDragFunction;
888  [[self window] platformWindow]._DOMBodyElement.onselectstart = CPTextFieldCachedSelectStartFunction;
889 
890  CPTextFieldCachedSelectStartFunction = nil
891  CPTextFieldCachedDragFunction = nil;
892  }
893 
894  // TODO clickCount === 2 should select the clicked word.
895 
896  if ([[CPApp currentEvent] clickCount] === 3)
897  {
898  [self selectText:nil];
899  return;
900  }
901 
902  return [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:YES];
903  }
904 }
905 
906 - (void)mouseDragged:(CPEvent)anEvent
907 {
908  if (![self isEnabled] || !([self isSelectable] || [self isEditable]))
909  [[self nextResponder] mouseDragged:anEvent];
910  else if ([self isSelectable])
911  return [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:YES];
912 }
913 
914 - (void)keyUp:(CPEvent)anEvent
915 {
916  if (!([self isEnabled] && [self isEditable]))
917  return;
918 
919 #if PLATFORM(DOM)
920  var newValue = [self _inputElement].value;
921 
922  if (newValue !== _stringValue)
923  {
924  [self _setStringValue:newValue];
925 
926  [self _didEdit];
927  }
928 
929  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
930 #endif
931 }
932 
933 - (void)keyDown:(CPEvent)anEvent
934 {
935  // CPTextField uses an HTML input element to take the input so we need to
936  // propagate the dom event so the element is updated. This has to be done
937  // before interpretKeyEvents: though so individual commands have a chance
938  // to override this (escape to clear the text in a search field for example).
939  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
940 
941  [self interpretKeyEvents:[anEvent]];
942 
943  [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
944 }
945 
955 - (void)doCommandBySelector:(SEL)aSelector
956 {
957  if ([self respondsToSelector:aSelector])
958  [self performSelector:aSelector];
959 }
960 
961 - (void)insertNewline:(id)sender
962 {
963  if (!([self isEnabled] && [self isEditable]))
964  return;
965 
966  var newValue = [self _inputElement].value;
967 
968  if (newValue !== _stringValue)
969  {
970  [self _setStringValue:newValue];
971  [self _didEdit];
972  }
973 
974  if ([self _valueIsValid:_stringValue])
975  {
976  // If _isEditing == YES then the target action can also be called via
977  // resignFirstResponder, and it is possible that the target action
978  // itself will change this textfield's responder status, so start by
979  // setting the _isEditing flag to NO to prevent the target action being
980  // called twice (once below and once from resignFirstResponder).
981  if (_isEditing)
982  {
983  _isEditing = NO;
984  [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidEndEditingNotification object:self userInfo:@{"CPTextMovement": [self _currentTextMovement]}]];
985  }
986 
987  // If there is no target action, or the sendAction call returns
988  // success.
989  if (![self action] || [self sendAction:[self action] to:[self target]])
990  {
991  [self selectAll:nil];
992  }
993  }
994 
995  [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
996 }
997 
998 - (void)insertNewlineIgnoringFieldEditor:(id)sender
999 {
1000  [self _insertCharacterIgnoringFieldEditor:CPNewlineCharacter];
1001 }
1002 
1003 - (void)insertTabIgnoringFieldEditor:(id)sender
1004 {
1005  [self _insertCharacterIgnoringFieldEditor:CPTabCharacter];
1006 }
1007 
1008 - (void)_insertCharacterIgnoringFieldEditor:(CPString)aCharacter
1009 {
1010  if (!([self isEnabled] && [self isEditable]))
1011  return;
1012 
1013 #if PLATFORM(DOM)
1014 
1015  var oldValue = _stringValue,
1016  range = [self selectedRange],
1017  element = [self _inputElement];
1018 
1019  element.value = [element.value stringByReplacingCharactersInRange:[self selectedRange] withString:aCharacter];
1020  [self _setStringValue:element.value];
1021 
1022  // NOTE: _stringValue is now the current input element value
1023  if (oldValue !== _stringValue)
1024  {
1025  [self _didEdit];
1026  }
1027 
1028 #endif
1029 }
1030 
1031 - (void)textDidBlur:(CPNotification)note
1032 {
1033  // this looks to prevent false propagation of notifications for other objects
1034  if ([note object] != self)
1035  return;
1036 
1038 }
1039 
1040 - (void)textDidFocus:(CPNotification)note
1041 {
1042  // this looks to prevent false propagation of notifications for other objects
1043  if ([note object] != self)
1044  return;
1045 
1047 }
1048 
1049 - (void)textDidChange:(CPNotification)note
1050 {
1051  if ([note object] !== self)
1052  return;
1053 
1054  [self _continuouslyReverseSetBinding];
1055 
1056  [super textDidChange:note];
1057 }
1058 
1062 - (id)objectValue
1063 {
1064  return [super objectValue];
1065 }
1066 
1067 /*
1068  Sets the internal string value without updating the value in the input element.
1069  This should only be invoked when the underlying text element's value has changed.
1070 */
1071 - (BOOL)_setStringValue:(CPString)aValue
1072 {
1073  return [self _setStringValue:aValue isNewValue:YES errorDescription:nil];
1074 }
1075 
1080 - (BOOL)_setStringValue:(CPString)aValue isNewValue:(BOOL)isNewValue errorDescription:(CPStringRef)anError
1081 {
1082  _stringValue = aValue;
1083 
1084  var objectValue = aValue,
1085  formatter = [self formatter],
1086  result = YES;
1087 
1088  if (formatter)
1089  {
1090  var object = nil;
1091 
1092  if ([formatter getObjectValue:@ref(object) forString:aValue errorDescription:anError])
1093  objectValue = object;
1094  else
1095  {
1096  objectValue = undefined; // Mark the value as invalid
1097  result = NO;
1098  }
1099 
1100  isNewValue |= objectValue !== [super objectValue];
1101  }
1102 
1103  if (isNewValue)
1104  {
1105  [self willChangeValueForKey:@"objectValue"];
1106  [super setObjectValue:objectValue];
1107  [self _updatePlaceholderState];
1108  [self didChangeValueForKey:@"objectValue"];
1109  }
1110 
1111  return result;
1112 }
1113 
1114 - (void)setObjectValue:(id)aValue
1115 {
1116  [super setObjectValue:aValue];
1117 
1118  var formatter = [self formatter];
1119 
1120  if (formatter)
1121  {
1122  // If there is a formatter, make sure the object value can be formatted successfully
1123  var formattedString = [self hasThemeState:CPThemeStateEditing] ? [formatter editingStringForObjectValue:aValue] : [formatter stringForObjectValue:aValue];
1124 
1125  if (formattedString === nil)
1126  {
1127  var value = nil;
1128 
1129  // Formatting failed, get an "empty" object by formatting an empty string.
1130  // If that fails, the value is undefined.
1131  if ([formatter getObjectValue:@ref(value) forString:@"" errorDescription:nil] === NO)
1132  value = undefined;
1133 
1134  [super setObjectValue:value];
1135  _stringValue = (value === nil || value === undefined) ? @"" : String(value);
1136  }
1137  else
1138  _stringValue = formattedString;
1139  }
1140  else
1141  _stringValue = [self stringValue];
1142 
1143 #if PLATFORM(DOM)
1144 
1145  if (CPTextFieldInputOwner === self || [[self window] firstResponder] === self)
1146  [self _inputElement].value = _stringValue;
1147 
1148 #endif
1149 
1150  [self _updatePlaceholderState];
1151 }
1152 
1153 - (void)_updatePlaceholderState
1154 {
1155  if ((!_stringValue || _stringValue.length === 0) && ![self hasThemeState:CPThemeStateEditing])
1156  [self setThemeState:CPTextFieldStatePlaceholder];
1157  else
1158  [self unsetThemeState:CPTextFieldStatePlaceholder];
1159 }
1160 
1165 - (void)setPlaceholderString:(CPString)aStringValue
1166 {
1167  if (_placeholderString === aStringValue)
1168  return;
1169 
1170  _placeholderString = aStringValue;
1171 
1172  // Only update things if we need to show the placeholder
1173  if ([self hasThemeState:CPTextFieldStatePlaceholder])
1174  {
1175  [self setNeedsLayout];
1176  [self setNeedsDisplay:YES];
1177  }
1178 }
1179 
1183 - (CPString)placeholderString
1184 {
1185  return _placeholderString;
1186 }
1187 
1208 - (void)sizeToFit
1209 {
1210  [self setFrameSize:[self _minimumFrameSize]];
1211 }
1212 
1213 - (CGSize)_minimumFrameSize
1214 {
1215  var frameSize = [self frameSize],
1216  contentInset = [self currentValueForThemeAttribute:@"content-inset"],
1217  minSize = [self currentValueForThemeAttribute:@"min-size"],
1218  maxSize = [self currentValueForThemeAttribute:@"max-size"],
1219  lineBreakMode = [self lineBreakMode],
1220  text = (_stringValue || @" "),
1221  textSize = CGSizeMakeCopy(frameSize),
1222  font = [self currentValueForThemeAttribute:@"font"];
1223 
1224  textSize.width -= contentInset.left + contentInset.right;
1225  textSize.height -= contentInset.top + contentInset.bottom;
1226 
1227  if (frameSize.width !== 0 &&
1228  ![self isBezeled] &&
1229  (lineBreakMode === CPLineBreakByWordWrapping || lineBreakMode === CPLineBreakByCharWrapping))
1230  {
1231  textSize = [text sizeWithFont:font inWidth:textSize.width];
1232  }
1233  else
1234  {
1235  textSize = [text sizeWithFont:font];
1236 
1237  // Account for possible fractional pixels at right edge
1238  textSize.width += 1;
1239  }
1240 
1241  // Account for possible fractional pixels at bottom edge
1242  textSize.height += 1;
1243 
1244  frameSize.height = textSize.height + contentInset.top + contentInset.bottom;
1245 
1246  if ([self isBezeled])
1247  {
1248  frameSize.height = MAX(frameSize.height, minSize.height);
1249 
1250  if (maxSize.width > 0.0)
1251  frameSize.width = MIN(frameSize.width, maxSize.width);
1252 
1253  if (maxSize.height > 0.0)
1254  frameSize.height = MIN(frameSize.height, maxSize.height);
1255  }
1256  else
1257  frameSize.width = textSize.width + contentInset.left + contentInset.right;
1258 
1259  frameSize.width = MAX(frameSize.width, minSize.width);
1260 
1261  return frameSize;
1262 }
1263 
1267 - (void)selectText:(id)sender
1268 {
1269  [self _selectText:sender immediately:NO];
1270 }
1271 
1272 - (void)_selectText:(id)sender immediately:(BOOL)immediately
1273 {
1274  // Selecting the text in a field makes it the first responder
1275  if ([self isEditable] || [self isSelectable])
1276  {
1277  var wind = [self window];
1278 
1279 #if PLATFORM(DOM)
1280  if ([self isEditable])
1281  {
1282  var element = [self _inputElement];
1283 
1284  if ([wind firstResponder] === self)
1285  {
1286  if (immediately)
1287  element.select();
1288  else
1289  window.setTimeout(function() { element.select(); }, 0);
1290  }
1291  else if (wind !== nil && [wind makeFirstResponder:self])
1292  [self _selectText:sender immediately:immediately];
1293  }
1294  else
1295  {
1296  [self setSelectedRange:CPMakeRange(0, _stringValue.length)];
1297  }
1298 #else
1299  // Even if we can't actually select the text we need to preserve the first
1300  // responder side effect.
1301  if (wind !== nil && [wind firstResponder] !== self)
1302  [wind makeFirstResponder:self];
1303 #endif
1304  }
1305 
1306 }
1307 
1308 - (void)copy:(id)sender
1309 {
1310  // First write to the Cappuccino clipboard.
1311  var stringToCopy = nil;
1312 
1313  if ([self isEditable])
1314  {
1315  var selectedRange = [self selectedRange];
1316 
1317  if (selectedRange.length < 1)
1318  return;
1319 
1320  stringToCopy = [_stringValue substringWithRange:selectedRange];
1321  }
1322  else
1323  {
1324  // selectedRange won't work if we're displaying our text using a <div>. Instead we have to ask the browser
1325  // what's selected and hope it's right in a Cappuccino context as well.
1326 #if PLATFORM(DOM)
1327  stringToCopy = [[[self window] platformWindow] _selectedText];
1328 #endif
1329  }
1330 
1331  var pasteboard = [CPPasteboard generalPasteboard];
1332 
1333  [pasteboard declareTypes:[CPStringPboardType] owner:nil];
1334  [pasteboard setString:stringToCopy forType:CPStringPboardType];
1335 
1336  if ([CPPlatform isBrowser])
1337  {
1338  // Then also allow the browser to capture the copied text into the system clipboard.
1339  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
1340  }
1341 }
1342 
1343 - (void)cut:(id)sender
1344 {
1345  if (![self isEnabled])
1346  return;
1347 
1348  [self copy:sender];
1349 
1350  if (![self isEditable])
1351  return;
1352 
1353  if (![[CPApp currentEvent] _platformIsEffectingCutOrPaste])
1354  {
1355  [self deleteBackward:sender];
1356  }
1357  else
1358  {
1359  // Allow the browser's standard cut handling. This should also result in the deleteBackward: happening.
1360  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
1361 
1362  // If we don't have an oninput listener, we won't detect the change made by the cut and need to fake a key up "soon".
1364  [CPTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(keyUp:) userInfo:nil repeats:NO];
1365  }
1366 }
1367 
1368 - (void)paste:(id)sender
1369 {
1370  if (!([self isEnabled] && [self isEditable]))
1371  return;
1372 
1373  if (![[CPApp currentEvent] _platformIsEffectingCutOrPaste])
1374  {
1375  var pasteboard = [CPPasteboard generalPasteboard];
1376 
1377  if (![[pasteboard types] containsObject:CPStringPboardType])
1378  return;
1379 
1380  [self deleteBackward:sender];
1381 
1382  var selectedRange = [self selectedRange],
1383  pasteString = [pasteboard stringForType:CPStringPboardType],
1384  newValue = [_stringValue stringByReplacingCharactersInRange:selectedRange withString:pasteString];
1385 
1386  [self setStringValue:newValue];
1387  [self _didEdit];
1388  [self setSelectedRange:CPMakeRange(selectedRange.location + pasteString.length, 0)];
1389  }
1390  // If we don't have an oninput listener, we won't detect the change made by the cut and need to fake a key up "soon".
1391  else
1392  {
1393  // Allow the browser's standard paste handling.
1394  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
1395 
1397  [CPTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(keyUp:) userInfo:nil repeats:NO];
1398  }
1399 }
1400 
1401 - (CPRange)selectedRange
1402 {
1403  // TODO Need a way to figure out the selected range if we're not using an input. Need
1404  // to get whole document selection and somehow see which part is inside of this text field.
1405  if ([[self window] firstResponder] !== self)
1406  return CPMakeRange(0, 0);
1407 
1408 #if PLATFORM(DOM)
1409 
1410  // we wrap this in try catch because firefox will throw an exception in certain instances
1411  try
1412  {
1413  var inputElement = [self _inputElement],
1414  selectionStart = inputElement.selectionStart,
1415  selectionEnd = inputElement.selectionEnd;
1416 
1417  if ([selectionStart isKindOfClass:CPNumber])
1418  return CPMakeRange(selectionStart, selectionEnd - selectionStart);
1419 
1420  // browsers which don't support selectionStart/selectionEnd (aka IE).
1421  var theDocument = inputElement.ownerDocument || inputElement.document,
1422  selectionRange = theDocument.selection.createRange(),
1423  range = inputElement.createTextRange();
1424 
1425  if (range.inRange(selectionRange))
1426  {
1427  range.setEndPoint('EndToStart', selectionRange);
1428  return CPMakeRange(range.text.length, selectionRange.text.length);
1429  }
1430  }
1431  catch (e)
1432  {
1433  // fall through to the return
1434  }
1435 
1436 #endif
1437 
1438  return CPMakeRange(0, 0);
1439 }
1440 
1441 - (void)setSelectedRange:(CPRange)aRange
1442 {
1443  if (![[self window] firstResponder] === self)
1444  return;
1445 
1446 #if PLATFORM(DOM)
1447 
1448  if (![self isEditable])
1449  {
1450  // No input element - selectable text field only.
1451  var contentView = [self layoutEphemeralSubviewNamed:@"content-view"
1452  positioned:CPWindowAbove
1453  relativeToEphemeralSubviewNamed:@"bezel-view"];
1454 
1455  if (contentView)
1456  [contentView setSelectedRange:aRange];
1457  }
1458  else
1459  {
1460  // Input element
1461  var inputElement = [self _inputElement];
1462 
1463  try
1464  {
1465  if ([inputElement.selectionStart isKindOfClass:CPNumber])
1466  {
1467  inputElement.selectionStart = aRange.location;
1468  inputElement.selectionEnd = CPMaxRange(aRange);
1469  }
1470  else
1471  {
1472  // browsers which don't support selectionStart/selectionEnd (aka IE).
1473  var theDocument = inputElement.ownerDocument || inputElement.document,
1474  existingRange = theDocument.selection.createRange(),
1475  range = inputElement.createTextRange();
1476 
1477  if (range.inRange(existingRange))
1478  {
1479  range.collapse(true);
1480  range.move('character', aRange.location);
1481  range.moveEnd('character', aRange.length);
1482  range.select();
1483  }
1484  }
1485  }
1486  catch (e)
1487  {
1488  }
1489  }
1490 #endif
1491 }
1492 
1493 - (void)selectAll:(id)sender
1494 {
1495  [self selectText:sender];
1496 }
1497 
1498 - (void)deleteBackward:(id)sender
1499 {
1500  if (!([self isEnabled] && [self isEditable]))
1501  return;
1502 
1503  var selectedRange = [self selectedRange];
1504 
1505  if (selectedRange.length < 1)
1506  {
1507  if (selectedRange.location < 1)
1508  return;
1509 
1510  // Delete a single element backward from the insertion point if there's no selection.
1511  selectedRange.location -= 1;
1512  selectedRange.length += 1;
1513  }
1514 
1515  var newValue = [_stringValue stringByReplacingCharactersInRange:selectedRange withString:""];
1516 
1517  [self setStringValue:newValue];
1518  [self setSelectedRange:CPMakeRange(selectedRange.location, 0)];
1519  [self _didEdit];
1520 
1521 #if PLATFORM(DOM)
1522  // Since we just performed the deletion manually, we don't need the browser to do anything else.
1523  // (Previously we would allow the event to propagate for the browser to delete 1 character only,
1524  // and we'd delete the rest manually. But this meant that if deleteBackward: was called without
1525  // it being a browser backspace event, 1 character would be left behind.)
1526  [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
1527 #endif
1528 }
1529 
1530 - (void)delete:(id)sender
1531 {
1532  if (!([self isEnabled] && [self isEditable]))
1533  return;
1534 
1535  // delete: only works when there's a selection (as opposed to deleteForward: and deleteBackward:).
1536  var selectedRange = [self selectedRange];
1537 
1538  if (selectedRange.length < 1)
1539  return;
1540 
1541  var newValue = [_stringValue stringByReplacingCharactersInRange:selectedRange withString:""];
1542 
1543  [self setStringValue:newValue];
1544  [self setSelectedRange:CPMakeRange(selectedRange.location, 0)];
1545  [self _didEdit];
1546 
1547 #if PLATFORM(DOM)
1548  // Since we just performed the deletion manually, we don't need the browser to do anything else.
1549  [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
1550 #endif
1551 }
1552 
1553 - (void)deleteForward:(id)sender
1554 {
1555  if (!([self isEnabled] && [self isEditable]))
1556  return;
1557 
1558  var selectedRange = [self selectedRange];
1559 
1560  if (selectedRange.length < 1)
1561  {
1562  if (selectedRange.location + 1 >= _stringValue.length)
1563  return;
1564 
1565  selectedRange.length += 1;
1566  }
1567 
1568  var newValue = [_stringValue stringByReplacingCharactersInRange:selectedRange withString:""];
1569 
1570  [self setStringValue:newValue];
1571  [self setSelectedRange:CPMakeRange(selectedRange.location, 0)];
1572  [self _didEdit];
1573 
1574 #if PLATFORM(DOM)
1575  // Since we just performed the deletion manually, we don't need the browser to do anything else.
1576  [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
1577 #endif
1578 }
1579 
1580 #pragma mark Setting the Delegate
1581 
1582 - (void)setDelegate:(id)aDelegate
1583 {
1584  var defaultCenter = [CPNotificationCenter defaultCenter];
1585 
1586  //unsubscribe the existing delegate if it exists
1587  if (_delegate)
1588  {
1589  [defaultCenter removeObserver:_delegate name:CPControlTextDidBeginEditingNotification object:self];
1590  [defaultCenter removeObserver:_delegate name:CPControlTextDidChangeNotification object:self];
1591  [defaultCenter removeObserver:_delegate name:CPControlTextDidEndEditingNotification object:self];
1592  [defaultCenter removeObserver:_delegate name:CPTextFieldDidFocusNotification object:self];
1593  [defaultCenter removeObserver:_delegate name:CPTextFieldDidBlurNotification object:self];
1594  }
1595 
1596  _delegate = aDelegate;
1597 
1598  if ([_delegate respondsToSelector:@selector(controlTextDidBeginEditing:)])
1599  [defaultCenter
1600  addObserver:_delegate
1601  selector:@selector(controlTextDidBeginEditing:)
1602  name:CPControlTextDidBeginEditingNotification
1603  object:self];
1604 
1605  if ([_delegate respondsToSelector:@selector(controlTextDidChange:)])
1606  [defaultCenter
1607  addObserver:_delegate
1608  selector:@selector(controlTextDidChange:)
1609  name:CPControlTextDidChangeNotification
1610  object:self];
1611 
1612 
1613  if ([_delegate respondsToSelector:@selector(controlTextDidEndEditing:)])
1614  [defaultCenter
1615  addObserver:_delegate
1616  selector:@selector(controlTextDidEndEditing:)
1617  name:CPControlTextDidEndEditingNotification
1618  object:self];
1619 
1620  if ([_delegate respondsToSelector:@selector(controlTextDidFocus:)])
1621  [defaultCenter
1622  addObserver:_delegate
1623  selector:@selector(controlTextDidFocus:)
1624  name:CPTextFieldDidFocusNotification
1625  object:self];
1626 
1627  if ([_delegate respondsToSelector:@selector(controlTextDidBlur:)])
1628  [defaultCenter
1629  addObserver:_delegate
1630  selector:@selector(controlTextDidBlur:)
1631  name:CPTextFieldDidBlurNotification
1632  object:self];
1633 }
1634 
1635 - (id)delegate
1636 {
1637  return _delegate;
1638 }
1639 
1640 - (CGRect)contentRectForBounds:(CGRect)bounds
1641 {
1642  var contentInset = [self currentValueForThemeAttribute:@"content-inset"];
1643 
1644  return CGRectInsetByInset(bounds, contentInset);
1645 }
1646 
1647 - (CGRect)bezelRectForBounds:(CGRect)bounds
1648 {
1649  var bezelInset = [self currentValueForThemeAttribute:@"bezel-inset"];
1650 
1651  return CGRectInsetByInset(bounds, bezelInset);
1652 }
1653 
1654 - (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
1655 {
1656  if (aName === "bezel-view")
1657  return [self bezelRectForBounds:[self bounds]];
1658 
1659  else if (aName === "content-view")
1660  return [self contentRectForBounds:[self bounds]];
1661 
1662  return [super rectForEphemeralSubviewNamed:aName];
1663 }
1664 
1665 - (CPView)createEphemeralSubviewNamed:(CPString)aName
1666 {
1667  if (aName === "bezel-view")
1668  {
1669  var view = [[CPView alloc] initWithFrame:CGRectMakeZero()];
1670 
1671  [view setHitTests:NO];
1672 
1673  return view;
1674  }
1675  else
1676  {
1677  var view = [[_CPImageAndTextView alloc] initWithFrame:CGRectMakeZero()];
1678 
1679  [view setHitTests:NO];
1680 
1681  return view;
1682  }
1683 
1684  return [super createEphemeralSubviewNamed:aName];
1685 }
1686 
1687 - (void)layoutSubviews
1688 {
1689  var bezelView = [self layoutEphemeralSubviewNamed:@"bezel-view"
1690  positioned:CPWindowBelow
1691  relativeToEphemeralSubviewNamed:@"content-view"];
1692 
1693  if (bezelView)
1694  [bezelView setBackgroundColor:[self currentValueForThemeAttribute:@"bezel-color"]];
1695 
1696  var contentView = [self layoutEphemeralSubviewNamed:@"content-view"
1697  positioned:CPWindowAbove
1698  relativeToEphemeralSubviewNamed:@"bezel-view"];
1699 
1700  if (contentView)
1701  {
1702  [contentView setHidden:[self hasThemeState:CPThemeStateEditing]];
1703 
1704  var string = "";
1705 
1706  if ([self hasThemeState:CPTextFieldStatePlaceholder])
1707  string = [self placeholderString];
1708  else
1709  {
1710  string = _stringValue;
1711 
1712  if ([self isSecure])
1713  string = secureStringForString(string);
1714  }
1715 
1716  [contentView setText:string];
1717 
1718  [contentView setTextColor:[self currentValueForThemeAttribute:@"text-color"]];
1719  [contentView setFont:[self currentValueForThemeAttribute:@"font"]];
1720  [contentView setAlignment:[self currentValueForThemeAttribute:@"alignment"]];
1721  [contentView setVerticalAlignment:[self currentValueForThemeAttribute:@"vertical-alignment"]];
1722  [contentView setLineBreakMode:[self currentValueForThemeAttribute:@"line-break-mode"]];
1723  [contentView setTextShadowColor:[self currentValueForThemeAttribute:@"text-shadow-color"]];
1724  [contentView setTextShadowOffset:[self currentValueForThemeAttribute:@"text-shadow-offset"]];
1725  }
1726 }
1727 
1728 - (void)takeValueFromKeyPath:(CPString)aKeyPath ofObjects:(CPArray)objects
1729 {
1730  var count = objects.length,
1731  value = [objects[0] valueForKeyPath:aKeyPath];
1732 
1733  [self setStringValue:value];
1734  [self setPlaceholderString:@""];
1735 
1736  while (count-- > 1)
1737  if (value !== [objects[count] valueForKeyPath:aKeyPath])
1738  {
1739  [self setPlaceholderString:@"Multiple Values"];
1740  [self setStringValue:@""];
1741  }
1742 }
1743 
1744 #pragma mark Overrides
1745 
1746 - (void)viewDidHide
1747 {
1748  [super viewDidHide];
1749 
1750  if ([[self window] firstResponder] === self)
1751  [self _resignFirstKeyResponder];
1752 }
1753 
1754 - (void)viewDidUnhide
1755 {
1756  [super viewDidUnhide];
1757 
1758  if ([self isEditable] && [[self window] firstResponder] === self)
1759  [self _becomeFirstKeyResponder];
1760 }
1761 
1762 - (BOOL)validateUserInterfaceItem:(id /*<CPValidatedUserInterfaceItem>*/)anItem
1763 {
1764  var theAction = [anItem action];
1765 
1766  if (![self isEditable] && (theAction == @selector(cut:) || theAction == @selector(paste:) || theAction == @selector(delete:)))
1767  return NO;
1768 
1769  // FIXME - [self selectedRange] is always empty if we're not an editable field, so we must assume yes here.
1770  if (![self isEditable])
1771  return YES;
1772 
1773  if (theAction == @selector(copy:) || theAction == @selector(cut:) || theAction == @selector(delete:))
1774  return [self selectedRange].length;
1775 
1776  return YES;
1777 }
1778 
1779 #pragma mark Private
1780 
1781 - (BOOL)_isWithinUsablePlatformRect
1782 {
1783  // Make sure the text field is completely within the platform window
1784  // so the browser will not scroll it into view.
1785 
1786  var wind = [self window];
1787 
1788  // If the field is not yet within a window, it can't be first responder
1789  if (!wind)
1790  return NO;
1791 
1792  var frame = [self convertRectToBase:[self bounds]],
1793  usableRect = [[wind platformWindow] usableContentFrame];
1794 
1795  frame.origin = [wind convertBaseToGlobal:frame.origin];
1796 
1797  return (CGRectGetMinX(frame) >= CGRectGetMinX(usableRect) &&
1798  CGRectGetMaxX(frame) <= CGRectGetMaxX(usableRect) &&
1799  CGRectGetMinY(frame) >= CGRectGetMinY(usableRect) &&
1800  CGRectGetMaxY(frame) <= CGRectGetMaxY(usableRect));
1801 }
1802 
1803 @end
1804 
1805 var secureStringForString = function(aString)
1806 {
1807  // This is true for when aString === "" and null/undefined.
1808  if (!aString)
1809  return "";
1810 
1811  return Array(aString.length + 1).join(CPSecureTextFieldCharacter);
1812 };
1813 
1814 
1815 var CPTextFieldIsEditableKey = "CPTextFieldIsEditableKey",
1816  CPTextFieldIsSelectableKey = "CPTextFieldIsSelectableKey",
1817  CPTextFieldIsBorderedKey = "CPTextFieldIsBorderedKey",
1818  CPTextFieldIsBezeledKey = "CPTextFieldIsBezeledKey",
1819  CPTextFieldBezelStyleKey = "CPTextFieldBezelStyleKey",
1820  CPTextFieldDrawsBackgroundKey = "CPTextFieldDrawsBackgroundKey",
1821  CPTextFieldLineBreakModeKey = "CPTextFieldLineBreakModeKey",
1822  CPTextFieldAlignmentKey = "CPTextFieldAlignmentKey",
1823  CPTextFieldBackgroundColorKey = "CPTextFieldBackgroundColorKey",
1824  CPTextFieldPlaceholderStringKey = "CPTextFieldPlaceholderStringKey";
1825 
1826 @implementation CPTextField (CPCoding)
1827 
1833 - (id)initWithCoder:(CPCoder)aCoder
1834 {
1835  self = [super initWithCoder:aCoder];
1836 
1837  if (self)
1838  {
1839  [self setEditable:[aCoder decodeBoolForKey:CPTextFieldIsEditableKey]];
1840  [self setSelectable:[aCoder decodeBoolForKey:CPTextFieldIsSelectableKey]];
1841 
1842  [self setDrawsBackground:[aCoder decodeBoolForKey:CPTextFieldDrawsBackgroundKey]];
1843 
1844  [self setTextFieldBackgroundColor:[aCoder decodeObjectForKey:CPTextFieldBackgroundColorKey]];
1845 
1846  [self setLineBreakMode:[aCoder decodeIntForKey:CPTextFieldLineBreakModeKey]];
1847  [self setAlignment:[aCoder decodeIntForKey:CPTextFieldAlignmentKey]];
1848 
1849  [self setPlaceholderString:[aCoder decodeObjectForKey:CPTextFieldPlaceholderStringKey]];
1850  }
1851 
1852  return self;
1853 }
1854 
1859 - (void)encodeWithCoder:(CPCoder)aCoder
1860 {
1861  [super encodeWithCoder:aCoder];
1862 
1863  [aCoder encodeBool:_isEditable forKey:CPTextFieldIsEditableKey];
1864  [aCoder encodeBool:_isSelectable forKey:CPTextFieldIsSelectableKey];
1865 
1866  [aCoder encodeBool:_drawsBackground forKey:CPTextFieldDrawsBackgroundKey];
1867 
1868  [aCoder encodeObject:_textFieldBackgroundColor forKey:CPTextFieldBackgroundColorKey];
1869 
1870  [aCoder encodeInt:[self lineBreakMode] forKey:CPTextFieldLineBreakModeKey];
1871  [aCoder encodeInt:[self alignment] forKey:CPTextFieldAlignmentKey];
1872 
1873  [aCoder encodeObject:_placeholderString forKey:CPTextFieldPlaceholderStringKey];
1874 }
1875 
1876 @end
1877 @implementation _CPTextFieldValueBinder : CPBinder
1878 {
1879  id __doxygen__;
1880 }
1881 
1882 - (void)_updatePlaceholdersWithOptions:(CPDictionary)options forBinding:(CPString)aBinding
1883 {
1884  [super _updatePlaceholdersWithOptions:options];
1885 
1886  [self _setPlaceholder:@"Multiple Values" forMarker:CPMultipleValuesMarker isDefault:YES];
1887  [self _setPlaceholder:@"No Selection" forMarker:CPNoSelectionMarker isDefault:YES];
1888  [self _setPlaceholder:@"Not Applicable" forMarker:CPNotApplicableMarker isDefault:YES];
1889  [self _setPlaceholder:@"" forMarker:CPNullMarker isDefault:YES];
1890 }
1891 
1892 - (void)setPlaceholderValue:(id)aValue withMarker:(CPString)aMarker forBinding:(CPString)aBinding
1893 {
1894  [_source setPlaceholderString:aValue];
1895  [_source setObjectValue:nil];
1896 }
1897 
1898 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
1899 {
1900  if (!aValue || (aValue.isa && [aValue isMemberOfClass:CPNull]))
1901  [_source setPlaceholderString:[self _placeholderForMarker:CPNullMarker]];
1902 
1903  [_source setObjectValue:aValue];
1904 }
1905 
1906 @end
1907 @implementation _CPTextFieldPatternValueBinder : CPValueWithPatternBinding
1908 {
1909  id __doxygen__;
1910 }
1911 
1912 - (void)setPlaceholderValue:(id)aValue withMarker:(CPString)aMarker forBinding:(CPString)aBinding
1913 {
1914  [_source setPlaceholderString:aValue];
1915  [_source setObjectValue:nil];
1916 }
1917 
1918 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
1919 {
1920  if (!aValue || (aValue.isa && [aValue isMemberOfClass:CPNull]))
1921  [_source setPlaceholderString:[self _placeholderForMarker:CPNullMarker]];
1922 
1923  [_source setObjectValue:aValue];
1924 }
1925 
1926 @end