<commit_before>
/*
 * CPTextField.j
 * AppKit
 *
 * Created by <NAME>.
 * Copyright 2008, 280 North, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

@import "CPControl.j"
@import "CPStringDrawing.j"
@import "CPCompatibility.j"
@import "_CPImageAndTextView.j"


CPTextFieldSquareBezel          = 0;    /*! A textfield bezel with a squared corners. */
CPTextFieldRoundedBezel         = 1;    /*! A textfield bezel with rounded corners. */

CPTextFieldDidFocusNotification = @"CPTextFieldDidFocusNotification";
CPTextFieldDidBlurNotification  = @"CPTextFieldDidBlurNotification";

#if PLATFORM(DOM)

var CPTextFieldDOMInputElement = nil,
    CPTextFieldDOMPasswordInputElement = nil,
    CPTextFieldDOMStandardInputElement = nil,
    CPTextFieldInputOwner = nil,
    CPTextFieldTextDidChangeValue = nil,
    CPTextFieldInputResigning = NO,
    CPTextFieldInputDidBlur = NO,
    CPTextFieldInputIsActive = NO,
    CPTextFieldCachedSelectStartFunction = nil,
    CPTextFieldCachedDragFunction = nil,
    CPTextFieldBlurFunction = nil;

#endif

var CPSecureTextFieldCharacter = "\u2022";

@implementation CPString (CPTextFieldAdditions)

/*!
    Returns the string (\c self).
*/
- (CPString)string
{
    return self;
}

@end

CPTextFieldStateRounded     = CPThemeState("rounded");
CPTextFieldStatePlaceholder = CPThemeState("placeholder");

/*!
    @ingroup appkit
    This control displays editable text in a Cappuccino application.
*/
@implementation CPTextField : CPControl
{
    BOOL                    _isEditing;

    BOOL                    _isEditable;
    BOOL                    _isSelectable;
    BOOL                    _isSecure;
    BOOL                    _willBecomeFirstResponderByClick;

    BOOL                    _drawsBackground;

    CPColor                 _textFieldBackgroundColor;

    id                      _placeholderString;

    id                      _delegate;

    CPString                _textDidChangeValue;

    // NS-style Display Properties
    CPTextFieldBezelStyle   _bezelStyle;
    BOOL                    _isBordered;
    CPControlSize           _controlSize;
}

+ (CPTextField)textFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth
{
    return [self textFieldWithStringValue:aStringValue placeholder:aPlaceholder width:aWidth theme:[CPTheme defaultTheme]];
}

+ (CPTextField)textFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth theme:(CPTheme)aTheme
{
    var textField = [[self alloc] initWithFrame:CGRectMake(0.0, 0.0, aWidth, 29.0)];

    [textField setTheme:aTheme];
    [textField setStringValue:aStringValue];
    [textField setPlaceholderString:aPlaceholder];
    [textField setBordered:YES];
    [textField setBezeled:YES];
    [textField setEditable:YES];

    [textField sizeToFit];

    return textField;
}

+ (CPTextField)roundedTextFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth
{
    return [self roundedTextFieldWithStringValue:aStringValue placeholder:aPlaceholder width:aWidth theme:[CPTheme defaultTheme]];
}

+ (CPTextField)roundedTextFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth theme:(CPTheme)aTheme
{
    var textField = [[CPTextField alloc] initWithFrame:CGRectMake(0.0, 0.0, aWidth, 29.0)];

    [textField setTheme:aTheme];
    [textField setStringValue:aStringValue];
    [textField setPlaceholderString:aPlaceholder];
    [textField setBezelStyle:CPTextFieldRoundedBezel];
    [textField setBordered:YES];
    [textField setBezeled:YES];
    [textField setEditable:YES];

    [textField sizeToFit];

    return textField;
}

+ (CPTextField)labelWithTitle:(CPString)aTitle
{
    return [self labelWithTitle:aTitle theme:[CPTheme defaultTheme]];
}

+ (CPTextField)labelWithTitle:(CPString)aTitle theme:(CPTheme)aTheme
{
    var textField = [[self alloc] init];

    [textField setStringValue:aTitle];
    [textField sizeToFit];

    return textField;
}

+ (CPString)defaultThemeClass
{
    return "textfield";
}

+ (Class)_binderClassForBinding:(CPString)theBinding
{
    if (theBinding === CPValueBinding)
        return [_CPTextFieldValueBinder class];

    return [super _binderClassForBinding:theBinding];
}

+ (id)themeAttributes
{
    return [CPDictionary dictionaryWithObjects:[_CGInsetMakeZero(), _CGInsetMake(2.0, 2.0, 2.0, 2.0), [CPNull null]]
                                       forKeys:[@"bezel-inset", @"content-inset", @"bezel-color"]];
}

/* @ignore */
#if PLATFORM(DOM)
- (DOMElement)_inputElement
{
    if (!CPTextFieldDOMInputElement)
    {
        CPTextFieldDOMInputElement = document.createElement("input");
        CPTextFieldDOMInputElement.style.position = "absolute";
        CPTextFieldDOMInputElement.style.border = "0px";
        CPTextFieldDOMInputElement.style.padding = "0px";
        CPTextFieldDOMInputElement.style.margin = "0px";
        CPTextFieldDOMInputElement.style.whiteSpace = "pre";
        CPTextFieldDOMInputElement.style.background = "transparent";
        CPTextFieldDOMInputElement.style.outline = "none";

        CPTextFieldBlurFunction = function(anEvent)
        {
            if (CPTextFieldInputOwner && CPTextFieldInputOwner._DOMElement != CPTextFieldDOMInputElement.parentNode)
                return;

            if (!CPTextFieldInputResigning)
            {
                [[CPTextFieldInputOwner window] makeFirstResponder:nil];
                return;
            }

            CPTextFieldHandleBlur(anEvent, CPTextFieldDOMInputElement);
            CPTextFieldInputDidBlur = YES;

            return true;
        }

        CPTextFieldHandleBlur = function(anEvent)
        {
            CPTextFieldInputOwner = nil;

            [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
        }

        //FIXME make this not onblur
        CPTextFieldDOMInputElement.onblur = CPTextFieldBlurFunction;

        CPTextFieldDOMStandardInputElement = CPTextFieldDOMInputElement;
    }

    if (CPFeatureIsCompatible(CPInputTypeCanBeChangedFeature))
    {
        if ([self isSecure])
            CPTextFieldDOMInputElement.type = "password";
        else
            CPTextFieldDOMInputElement.type = "text";

        return CPTextFieldDOMInputElement;
    }

    if ([self isSecure])
    {
        if (!CPTextFieldDOMPasswordInputElement)
        {
            CPTextFieldDOMPasswordInputElement = document.createElement("input");
            CPTextFieldDOMPasswordInputElement.style.position = "absolute";
            CPTextFieldDOMPasswordInputElement.style.border = "0px";
            CPTextFieldDOMPasswordInputElement.style.padding = "0px";
            CPTextFieldDOMPasswordInputElement.style.margin = "0px";
            CPTextFieldDOMPasswordInputElement.style.whiteSpace = "pre";
            CPTextFieldDOMPasswordInputElement.style.background = "transparent";
            CPTextFieldDOMPasswordInputElement.style.outline = "none";
            CPTextFieldDOMPasswordInputElement.type = "password";

            CPTextFieldDOMPasswordInputElement.onblur = CPTextFieldBlurFunction;
        }

        CPTextFieldDOMInputElement = CPTextFieldDOMPasswordInputElement;
    }
    else
    {
        CPTextFieldDOMInputElement = CPTextFieldDOMStandardInputElement;
    }

    return CPTextFieldDOMInputElement;
}
#endif

- (id)initWithFrame:(CGRect)aFrame
{
    self = [super initWithFrame:aFrame];

    if (self)
    {
        [self setStringValue:@""];
        [self setPlaceholderString:@""];

        _sendActionOn = CPKeyUpMask | CPKeyDownMask;

        [self setValue:CPLeftTextAlignment forThemeAttribute:@"alignment"];
    }

    return self;
}

#pragma mark Controlling Editability and Selectability

/*!
    Sets whether or not the receiver text field can be edited. If NO, any
    ongoing edit is ended.
*/
- (void)setEditable:(BOOL)shouldBeEditable
{
    if (_isEditable === shouldBeEditable)
        return;

    _isEditable = shouldBeEditable;

    if (shouldBeEditable)
        _isSelectable = YES;

    // We only allow first responder status if the field is editable and enabled.
    if (!shouldBeEditable && [[self window] firstResponder] === self)
        [[self window] makeFirstResponder:nil];
}

/*!
    Returns \c YES if the textfield is currently editable by the user.
*/
- (BOOL)isEditable
{
    return _isEditable;
}

/*!
    Sets whether the field reacts to events. If NO, any ongoing edit is
    ended.
*/
- (void)setEnabled:(BOOL)shouldBeEnabled
{
    [super setEnabled:shouldBeEnabled];

    // We only allow first responder status if the field is editable and enabled.
    if (!shouldBeEnabled && [[self window] firstResponder] === self)
        [[self window] makeFirstResponder:nil];
}

/*!
    Sets whether the field's text is selectable by the user.
    @param aFlag \c YES makes the text selectable
*/
- (void)setSelectable:(BOOL)aFlag
{
    _isSelectable = aFlag;
}

/*!
    Returns \c YES if the field's text is selectable by the user.
*/
- (BOOL)isSelectable
{
    return _isSelectable;
}

/*!
    Sets whether the field's text is secure.
    @param aFlag \c YES makes the text secure
*/
- (void)setSecure:(BOOL)aFlag
{
    _isSecure = aFlag;
}

/*!
    Returns \c YES if the field's text is secure (password entry).
*/
- (BOOL)isSecure
{
    return _isSecure;
}

// Setting the Bezel Style
/*!
    Sets whether the textfield will have a bezeled border.
    @param shouldBeBezeled \c YES means the textfield will draw a bezeled border
*/
- (void)setBezeled:(BOOL)shouldBeBezeled
{
    if (shouldBeBezeled)
        [self setThemeState:CPThemeStateBezeled];
    else
        [self unsetThemeState:CPThemeStateBezeled];
}

/*!
    Returns \c YES if the textfield draws a bezeled border.
*/
- (BOOL)isBezeled
{
    return [self hasThemeState:CPThemeStateBezeled];
}

/*!
    Sets the textfield's bezel style.
    @param aBezelStyle the constant for the desired bezel style
*/
- (void)setBezelStyle:(CPTextFieldBezelStyle)aBezelStyle
{
    var shouldBeRounded = aBezelStyle === CPTextFieldRoundedBezel;

    if (shouldBeRounded)
        [self setThemeState:CPTextFieldStateRounded];
    else
        [self unsetThemeState:CPTextFieldStateRounded];
}

/*!
    Returns the textfield's bezel style.
*/
- (CPTextFieldBezelStyle)bezelStyle
{
    if ([self hasThemeState:CPTextFieldStateRounded])
        return CPTextFieldRoundedBezel;

    return CPTextFieldSquareBezel;
}

/*!
    Sets whether the textfield will have a border drawn.
    @param shouldBeBordered \c YES makes the textfield draw a border
*/
- (void)setBordered:(BOOL)shouldBeBordered
{
    if (shouldBeBordered)
        [self setThemeState:CPThemeStateBordered];
    else
        [self unsetThemeState:CPThemeStateBordered];
}

/*!
    Returns \c YES if the textfield has a border.
*/
- (BOOL)isBordered
{
    return [self hasThemeState:CPThemeStateBordered];
}

/*!
    Sets whether the textfield will have a background drawn.
    @param shouldDrawBackground \c YES makes the textfield draw a background
*/
- (void)setDrawsBackground:(BOOL)shouldDrawBackground
{
    if (_drawsBackground == shouldDrawBackground)
        return;

    _drawsBackground = shouldDrawBackground;

    [self setNeedsLayout];
    [self setNeedsDisplay:YES];
}

/*!
    Returns \c YES if the textfield draws a background.
*/
- (BOOL)drawsBackground
{
    return _drawsBackground;
}

/*!
    Sets the background color, which is shown for non-bezeled text fields with drawsBackground set to YES
    @param aColor The background color
*/
- (void)setTextFieldBackgroundColor:(CPColor)aColor
{
    if (_textFieldBackgroundColor == aColor)
        return;

    _textFieldBackgroundColor = aColor;

    [self setNeedsLayout];
    [self setNeedsDisplay:YES];
}

/*!
    Returns the background color.
*/
- (CPColor)textFieldBackgroundColor
{
    return _textFieldBackgroundColor;
}

/* @ignore */
- (BOOL)acceptsFirstResponder
{
    return [self isEditable] && [self isEnabled];
}

/* @ignore */
- (BOOL)becomeFirstResponder
{
#if PLATFORM(DOM)
    if (CPTextFieldInputOwner && [CPTextFieldInputOwner window] !== [self window])
        [[CPTextFieldInputOwner window] makeFirstResponder:nil];
#endif

    [self setThemeState:CPThemeStateEditing];

    [self _updatePlaceholderState];

    [self setNeedsLayout];

    _isEditing = NO;

#if PLATFORM(DOM)

    var string = [self stringValue],
        element = [self _inputElement],
        font = [self currentValueForThemeAttribute:@"font"];

    // generate the font metric
    [font _getMetrics];

    element.value = string;
    element.style.color = [[self currentValueForThemeAttribute:@"text-color"] cssString];
    element.style.font = [font cssString];
    element.style.zIndex = 1000;

    switch ([self alignment])
    {
        case CPCenterTextAlignment: element.style.textAlign = "center";
                                    break;
        case CPRightTextAlignment:  element.style.textAlign = "right";
                                    break;
        default:                    element.style.textAlign = "left";
    }

    var contentRect = [self contentRectForBounds:[self bounds]],
        verticalAlign = [self currentValueForThemeAttribute:"vertical-alignment"];

    switch (verticalAlign)
    {
        case CPTopVerticalTextAlignment:
            var topPoint = (_CGRectGetMinY(contentRect) + 1) + "px"; // for the same reason we have a -1 for the left, we also have a + 1 here
            break;

        case CPCenterVerticalTextAlignment:
            var topPoint = (_CGRectGetMidY(contentRect) - (font._lineHeight / 2) + 1) + "px";
            break;

        case CPBottomVerticalTextAlignment:
            var topPoint = (_CGRectGetMaxY(contentRect) - font._lineHeight) + "px";
            break;

        default:
            var topPoint = (_CGRectGetMinY(contentRect) + 1) + "px";
            break;
    }

    element.style.top = topPoint;
    element.style.left = (_CGRectGetMinX(contentRect) - 1) + "px"; // why -1?
    element.style.width = _CGRectGetWidth(contentRect) + "px";
    element.style.height = font._lineHeight + "px"; // private ivar for the line height of the DOM text at this particular size

    _DOMElement.appendChild(element);

    window.setTimeout(function()
    {
        element.focus();

        // Select the text if the textfield became first responder through keyboard interaction
        if (!_willBecomeFirstResponderByClick)
            [self selectText:self];

        _willBecomeFirstResponderByClick = NO;

        [self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]];
        CPTextFieldInputOwner = self;
    }, 0.0);

    element.value = [self stringValue];

    [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];

    CPTextFieldInputIsActive = YES;

    if (document.attachEvent)
    {
        CPTextFieldCachedSelectStartFunction = [[self window] platformWindow]._DOMBodyElement.onselectstart;
        CPTextFieldCachedDragFunction = [[self window] platformWindow]._DOMBodyElement.ondrag;

        [[self window] platformWindow]._DOMBodyElement.ondrag = function () {};
        [[self window] platformWindow]._DOMBodyElement.onselectstart = function () {};
    }
#endif

    return YES;
}

/* @ignore */
- (BOOL)resignFirstResponder
{
    [self unsetThemeState:CPThemeStateEditing];

    [self _updatePlaceholderState];

    [self setNeedsLayout];

#if PLATFORM(DOM)

    var element = [self _inputElement];

    if ([self stringValue] !== element.value)
        [self _setStringValue:element.value];

    CPTextFieldInputResigning = YES;

    if (CPTextFieldInputIsActive)
        element.blur();

    if (!CPTextFieldInputDidBlur)
        CPTextFieldBlurFunction();

    CPTextFieldInputDidBlur = NO;
    CPTextFieldInputResigning = NO;

    if (element.parentNode == _DOMElement)
        element.parentNode.removeChild(element);

    CPTextFieldInputIsActive = NO;

    if (document.attachEvent)
    {
        [[self window] platformWindow]._DOMBodyElement.ondrag = CPTextFieldCachedDragFunction;
        [[self window] platformWindow]._DOMBodyElement.onselectstart = CPTextFieldCachedSelectStartFunction;

        CPTextFieldCachedSelectStartFunction = nil;
        CPTextFieldCachedDragFunction = nil;
    }

#endif

    //post CPControlTextDidEndEditingNotification
    if (_isEditing)
    {
        _isEditing = NO;
        [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidEndEditingNotification object:self userInfo:nil]];

        if ([self sendsActionOnEndEditing])
            [self sendAction:[self action] to:[self target]];
    }

    [self textDidBlur:[CPNotification notificationWithName:CPTextFieldDidBlurNotification object:self userInfo:nil]];

    return YES;
}

/*!
    Text fields require panels to become key window, so this returns \c YES.
*/
- (BOOL)needsPanelToBecomeKey
{
    return YES;
}

- (void)mouseDown:(CPEvent)anEvent
{
    // Don't track! (ever?)
    if ([self isEditable] && [self isEnabled])
    {
        _willBecomeFirstResponderByClick = YES;
        [[self window] makeFirstResponder:self];
    }
    else if ([self isSelectable])
    {
        if (document.attachEvent)
        {
            CPTextFieldCachedSelectStartFunction = [[self window] platformWindow]._DOMBodyElement.onselectstart;
            CPTextFieldCachedDragFunction = [[self window] platformWindow]._DOMBodyElement.ondrag;

            [[self window] platformWindow]._DOMBodyElement.ondrag = function () {};
            [[self window] platformWindow]._DOMBodyElement.onselectstart = function () {};
        }
        return [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:YES];
    }
    else
        return [[self nextResponder] mouseDown:anEvent];
}

- (void)mouseUp:(CPEvent)anEvent
{
    if (![self isSelectable] && (![self isEditable] || ![self isEnabled]))
        [[self nextResponder] mouseUp:anEvent];
    else if ([self isSelectable])
    {
        if (document.attachEvent)
        {
            [[self window] platformWindow]._DOMBodyElement.ondrag = CPTextFieldCachedDragFunction;
            [[self window] platformWindow]._DOMBodyElement.onselectstart = CPTextFieldCachedSelectStartFunction;

            CPTextFieldCachedSelectStartFunction = nil
            CPTextFieldCachedDragFunction = nil;
        }
        return [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:YES];
    }
}

- (void)mouseDragged:(CPEvent)anEvent
{
    if (![self isSelectable] && (![self isEditable] || ![self isEnabled]))
        [[self nextResponder] mouseDragged:anEvent];
    else if ([self isSelectable])
        return [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:YES];
}

- (void)keyUp:(CPEvent)anEvent
{
    var oldValue = [self stringValue];
    [self _setStringValue:[self _inputElement].value];

    if (oldValue !== [self stringValue])
    {
        if (!_isEditing)
        {
            _isEditing = YES;
            [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
        }

        [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
    }

    [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
}

- (void)keyDown:(CPEvent)anEvent
{
    if ([anEvent _couldBeKeyEquivalent] && [self performKeyEquivalent:anEvent])
        return;

    // CPTextField uses an HTML input element to take the input so we need to
    // propagate the dom event so the element is updated. This has to be done
    // before interpretKeyEvents: though so individual commands have a chance
    // to override this (escape to clear the text in a search field for example).
    [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];

    [self interpretKeyEvents:[anEvent]];

    [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
}

/*!
    Invoke the action specified by aSelector on the current responder.

    This is implemented by CPResponder and by default it passes any unrecognized
    actions on to the next responder but text fields apparently aren't supposed
    to do that according to this documentation by Apple:

    http://developer.apple.com/mac/library/documentation/cocoa/reference/NSTextInputClient_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/NSTextInputClient/doCommandBySelector:
*/
- (void)doCommandBySelector:(SEL)aSelector
{
    if ([self respondsToSelector:aSelector])
        [self performSelector:aSelector];
}

- (void)insertNewline:(id)sender
{
    if (_isEditing)
    {
        _isEditing = NO;
        [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidEndEditingNotification object:self userInfo:nil]];
    }

    [self sendAction:[self action] to:[self target]];
    [self selectText:nil];

    [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
}

- (void)insertNewlineIgnoringFieldEditor:(id)sender
{
    var oldValue = [self stringValue];

    [self _inputElement].value += CPNewlineCharacter;
    [self _setStringValue:[self _inputElement].value];

    if (oldValue !== [self stringValue])
    {
        if (!_isEditing)
        {
            _isEditing = YES;
            [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
        }

        [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
    }
}

- (void)insertTabIgnoringFieldEditor:(id)sender
{
    var oldValue = [self stringValue];

    [self _inputElement].value += CPTabCharacter;
    [self _setStringValue:[self _inputElement].value];

    if (oldValue !== [self stringValue])
    {
        if (!_isEditing)
        {
            _isEditing = YES;
            [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
        }

        [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
    }
}

- (void)textDidBlur:(CPNotification)note
{
    // this looks to prevent false propagation of notifications for other objects
    if ([note object] != self)
        return;

    [[CPNotificationCenter defaultCenter] postNotification:note];
}

- (void)textDidFocus:(CPNotification)note
{
    // this looks to prevent false propagation of notifications for other objects
    if ([note object] != self)
        return;

    [[CPNotificationCenter defaultCenter] postNotification:note];
}

- (void)sendAction:(SEL)anAction to:(id)anObject
{
    [self _reverseSetBinding];

    [CPApp sendAction:anAction to:anObject from:self];
}

/*!
    Returns the string the text field.
*/
- (id)objectValue
{
    return [super objectValue];
}

/*
    @ignore
    Sets the internal string value without updating the value in the input element
*/
- (void)_setStringValue:(id)aValue
{
    [self willChangeValueForKey:@"objectValue"];
    [super setObjectValue:String(aValue)];
    [self _updatePlaceholderState];
    [self didChangeValueForKey:@"objectValue"];
}

- (void)setObjectValue:(id)aValue
{
    [super setObjectValue:aValue];

#if PLATFORM(DOM)

    if (CPTextFieldInputOwner === self || [[self window] firstResponder] === self)
        [self _inputElement].value = aValue;
#endif

    [self _updatePlaceholderState];
}

- (void)_updatePlaceholderState
{
    var string = [self stringValue];

    if ((!string || string.length === 0) && ![self hasThemeState:CPThemeStateEditing])
        [self setThemeState:CPTextFieldStatePlaceholder];
    else
        [self unsetThemeState:CPTextFieldStatePlaceholder];
}

/*!
    Sets a placeholder string for the receiver.  The placeholder is displayed until editing begins,
    and after editing ends, if the text field has an empty string value
*/
- (void)setPlaceholderString:(CPString)aStringValue
{
    if (_placeholderString === aStringValue)
        return;

    _placeholderString = aStringValue;

    // Only update things if we need to show the placeholder
    if ([self hasThemeState:CPTextFieldStatePlaceholder])
    {
        [self setNeedsLayout];
        [self setNeedsDisplay:YES];
    }
}

/*!
    Returns the receiver's placeholder string
*/
- (CPString)placeholderString
{
    return _placeholderString;
}

/*!
    For non-bezeled text fields (typically a label), sizeToFit has two behaviors, depending
    on the line break mode of the receiver.

    For non-bezeled receivers with a non-wrapping line break mode, sizeToFit will change the frame of the
    receiver to perfectly fit the current text in stringValue in the current font, respecting
    the current theme value for content-inset. For receivers with a wrapping line break mode,
    sizeToFit will wrap the text within the current width (respecting the current content-inset),
    so it will ONLY change the HEIGHT.

    For bezeled text fields (typically editable fields), sizeToFit will ONLY change the HEIGHT
    of the text field. It will not change the width of the text field. sizeToFit will attempt to
    change the height to fit a single line of text, respecting the current theme values for min-size,
    max-size and content-inset.

    The logic behind this decision is that most of the time you do not know what content will be placed
    in a bezeled text field, so you want to just choose a fixed width and leave it at that size.
    However, since you don't know how tall it needs to be if you change the font, sizeToFit will still be
    useful for making the textfield an appropriate height.
*/

- (void)sizeToFit
{
    [self setFrameSize:[self _minimumFrameSize]];
}

- (CGSize)_minimumFrameSize
{
    var frameSize = [self frameSize],
        contentInset = [self currentValueForThemeAttribute:@"content-inset"],
        minSize = [self currentValueForThemeAttribute:@"min-size"],
        maxSize = [self currentValueForThemeAttribute:@"max-size"],
        lineBreakMode = [self lineBreakMode],
        text = ([self stringValue] || @" "),
        textSize = _CGSizeMakeCopy(frameSize),
        font = [self currentValueForThemeAttribute:@"font"];

    textSize.width -= contentInset.left + contentInset.right;
    textSize.height -= contentInset.top + contentInset.bottom;

    if (frameSize.width !== 0 &&
        ![self isBezeled]     &&
        (lineBreakMode === CPLineBreakByWordWrapping || lineBreakMode === CPLineBreakByCharWrapping))
    {
        textSize = [text sizeWithFont:font inWidth:textSize.width];
    }
    else
        textSize = [text sizeWithFont:font];

    frameSize.height = textSize.height + contentInset.top + contentInset.bottom;

    if ([self isBezeled])
    {
        frameSize.height = MAX(frameSize.height, minSize.height);

        if (maxSize.width > 0.0)
            frameSize.width = MIN(frameSize.width, maxSize.width);

        if (maxSize.height > 0.0)
            frameSize.height = MIN(frameSize.height, maxSize.height);
    }
    else
        frameSize.width = textSize.width + contentInset.left + contentInset.right;

    frameSize.width = MAX(frameSize.width, minSize.width);

    return frameSize;
}

/*!
    Make the receiver the first responder and select all the text in the field.
*/
- (void)selectText:(id)sender
{
    // FIXME Should this really make the text field the first responder?

    if (([self isEditable] || [self isSelectable]))
    {
#if PLATFORM(DOM)
        var element = [self _inputElement];

        if ([[self window] firstResponder] === self)
            window.setTimeout(function() { element.select(); }, 0);
        else if ([self window] !== nil && [[self window] makeFirstResponder:self])
            window.setTimeout(function() {[self selectText:sender];}, 0);
#else
        // Even if we can't actually select the text we need to preserve the first
        // responder side effect.
        if ([self window] !== nil && [[self window] firstResponder] !== self)
            [[self window] makeFirstResponder:self];
#endif
    }
}

- (void)copy:(id)sender
{
    if (![CPPlatform isBrowser])
    {
        var selectedRange = [self selectedRange];

        if (selectedRange.length < 1)
            return;

        var pasteboard = [CPPasteboard generalPasteboard],
            stringValue = [self stringValue],
            stringForPasting = [stringValue substringWithRange:selectedRange];

        [pasteboard declareTypes:[CPStringPboardType] owner:nil];
        [pasteboard setString:stringForPasting forType:CPStringPboardType];
    }
}

- (void)cut:(id)sender
{
    if (![CPPlatform isBrowser])
    {
        [self copy:sender];
        [self deleteBackward:sender];
    }
}

- (void)paste:(id)sender
{
    if (![CPPlatform isBrowser])
    {
        var pasteboard = [CPPasteboard generalPasteboard];

        if (![[pasteboard types] containsObject:CPStringPboardType])
            return;

        [self deleteBackward:sender];

        var selectedRange = [self selectedRange],
            stringValue = [self stringValue],
            pasteString = [pasteboard stringForType:CPStringPboardType],
            newValue = [stringValue stringByReplacingCharactersInRange:selectedRange withString:pasteString];

        [self setStringValue:newValue];
        [self setSelectedRange:CPMakeRange(selectedRange.location + pasteString.length, 0)];
    }
}

- (CPRange)selectedRange
{
    if ([[self window] firstResponder] !== self)
        return CPMakeRange(0, 0);

    // we wrap this in try catch because firefox will throw an exception in certain instances
    try
    {
        var inputElement = [self _inputElement],
            selectionStart = inputElement.selectionStart,
            selectionEnd = inputElement.selectionEnd;

        if ([selectionStart isKindOfClass:CPNumber])
            return CPMakeRange(selectionStart, selectionEnd - selectionStart);

        // browsers which don't support selectionStart/selectionEnd (aka IE).
        var theDocument = inputElement.ownerDocument || inputElement.document,
            selectionRange = theDocument.selection.createRange(),
            range = inputElement.createTextRange();

        if (range.inRange(selectionRange))
        {
            range.setEndPoint('EndToStart', selectionRange);
            return CPMakeRange(range.text.length, selectionRange.text.length);
        }
    }
    catch (e)
    {
        // fall through to the return<commit_msg>fix textfield cut and paste notifications<commit_after>
/*
 * CPTextField.j
 * AppKit
 *
 * Created by <NAME>.
 * Copyright 2008, 280 North, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

@import "CPControl.j"
@import "CPStringDrawing.j"
@import "CPCompatibility.j"
@import "_CPImageAndTextView.j"


CPTextFieldSquareBezel          = 0;    /*! A textfield bezel with a squared corners. */
CPTextFieldRoundedBezel         = 1;    /*! A textfield bezel with rounded corners. */

CPTextFieldDidFocusNotification = @"CPTextFieldDidFocusNotification";
CPTextFieldDidBlurNotification  = @"CPTextFieldDidBlurNotification";

#if PLATFORM(DOM)

var CPTextFieldDOMInputElement = nil,
    CPTextFieldDOMPasswordInputElement = nil,
    CPTextFieldDOMStandardInputElement = nil,
    CPTextFieldInputOwner = nil,
    CPTextFieldTextDidChangeValue = nil,
    CPTextFieldInputResigning = NO,
    CPTextFieldInputDidBlur = NO,
    CPTextFieldInputIsActive = NO,
    CPTextFieldCachedSelectStartFunction = nil,
    CPTextFieldCachedDragFunction = nil,
    CPTextFieldBlurFunction = nil;

#endif

var CPSecureTextFieldCharacter = "\u2022";

@implementation CPString (CPTextFieldAdditions)

/*!
    Returns the string (\c self).
*/
- (CPString)string
{
    return self;
}

@end

CPTextFieldStateRounded     = CPThemeState("rounded");
CPTextFieldStatePlaceholder = CPThemeState("placeholder");

/*!
    @ingroup appkit
    This control displays editable text in a Cappuccino application.
*/
@implementation CPTextField : CPControl
{
    BOOL                    _isEditing;

    BOOL                    _isEditable;
    BOOL                    _isSelectable;
    BOOL                    _isSecure;
    BOOL                    _willBecomeFirstResponderByClick;

    BOOL                    _drawsBackground;

    CPColor                 _textFieldBackgroundColor;

    id                      _placeholderString;

    id                      _delegate;

    CPString                _textDidChangeValue;

    // NS-style Display Properties
    CPTextFieldBezelStyle   _bezelStyle;
    BOOL                    _isBordered;
    CPControlSize           _controlSize;
}

+ (CPTextField)textFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth
{
    return [self textFieldWithStringValue:aStringValue placeholder:aPlaceholder width:aWidth theme:[CPTheme defaultTheme]];
}

+ (CPTextField)textFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth theme:(CPTheme)aTheme
{
    var textField = [[self alloc] initWithFrame:CGRectMake(0.0, 0.0, aWidth, 29.0)];

    [textField setTheme:aTheme];
    [textField setStringValue:aStringValue];
    [textField setPlaceholderString:aPlaceholder];
    [textField setBordered:YES];
    [textField setBezeled:YES];
    [textField setEditable:YES];

    [textField sizeToFit];

    return textField;
}

+ (CPTextField)roundedTextFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth
{
    return [self roundedTextFieldWithStringValue:aStringValue placeholder:aPlaceholder width:aWidth theme:[CPTheme defaultTheme]];
}

+ (CPTextField)roundedTextFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth theme:(CPTheme)aTheme
{
    var textField = [[CPTextField alloc] initWithFrame:CGRectMake(0.0, 0.0, aWidth, 29.0)];

    [textField setTheme:aTheme];
    [textField setStringValue:aStringValue];
    [textField setPlaceholderString:aPlaceholder];
    [textField setBezelStyle:CPTextFieldRoundedBezel];
    [textField setBordered:YES];
    [textField setBezeled:YES];
    [textField setEditable:YES];

    [textField sizeToFit];

    return textField;
}

+ (CPTextField)labelWithTitle:(CPString)aTitle
{
    return [self labelWithTitle:aTitle theme:[CPTheme defaultTheme]];
}

+ (CPTextField)labelWithTitle:(CPString)aTitle theme:(CPTheme)aTheme
{
    var textField = [[self alloc] init];

    [textField setStringValue:aTitle];
    [textField sizeToFit];

    return textField;
}

+ (CPString)defaultThemeClass
{
    return "textfield";
}

+ (Class)_binderClassForBinding:(CPString)theBinding
{
    if (theBinding === CPValueBinding)
        return [_CPTextFieldValueBinder class];

    return [super _binderClassForBinding:theBinding];
}

+ (id)themeAttributes
{
    return [CPDictionary dictionaryWithObjects:[_CGInsetMakeZero(), _CGInsetMake(2.0, 2.0, 2.0, 2.0), [CPNull null]]
                                       forKeys:[@"bezel-inset", @"content-inset", @"bezel-color"]];
}

/* @ignore */
#if PLATFORM(DOM)
- (DOMElement)_inputElement
{
    if (!CPTextFieldDOMInputElement)
    {
        CPTextFieldDOMInputElement = document.createElement("input");
        CPTextFieldDOMInputElement.style.position = "absolute";
        CPTextFieldDOMInputElement.style.border = "0px";
        CPTextFieldDOMInputElement.style.padding = "0px";
        CPTextFieldDOMInputElement.style.margin = "0px";
        CPTextFieldDOMInputElement.style.whiteSpace = "pre";
        CPTextFieldDOMInputElement.style.background = "transparent";
        CPTextFieldDOMInputElement.style.outline = "none";

        CPTextFieldBlurFunction = function(anEvent)
        {
            if (CPTextFieldInputOwner && CPTextFieldInputOwner._DOMElement != CPTextFieldDOMInputElement.parentNode)
                return;

            if (!CPTextFieldInputResigning)
            {
                [[CPTextFieldInputOwner window] makeFirstResponder:nil];
                return;
            }

            CPTextFieldHandleBlur(anEvent, CPTextFieldDOMInputElement);
            CPTextFieldInputDidBlur = YES;

            return true;
        }

        CPTextFieldHandleBlur = function(anEvent)
        {
            CPTextFieldInputOwner = nil;

            [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
        }

        //FIXME make this not onblur
        CPTextFieldDOMInputElement.onblur = CPTextFieldBlurFunction;

        CPTextFieldDOMStandardInputElement = CPTextFieldDOMInputElement;
    }

    if (CPFeatureIsCompatible(CPInputTypeCanBeChangedFeature))
    {
        if ([self isSecure])
            CPTextFieldDOMInputElement.type = "password";
        else
            CPTextFieldDOMInputElement.type = "text";

        return CPTextFieldDOMInputElement;
    }

    if ([self isSecure])
    {
        if (!CPTextFieldDOMPasswordInputElement)
        {
            CPTextFieldDOMPasswordInputElement = document.createElement("input");
            CPTextFieldDOMPasswordInputElement.style.position = "absolute";
            CPTextFieldDOMPasswordInputElement.style.border = "0px";
            CPTextFieldDOMPasswordInputElement.style.padding = "0px";
            CPTextFieldDOMPasswordInputElement.style.margin = "0px";
            CPTextFieldDOMPasswordInputElement.style.whiteSpace = "pre";
            CPTextFieldDOMPasswordInputElement.style.background = "transparent";
            CPTextFieldDOMPasswordInputElement.style.outline = "none";
            CPTextFieldDOMPasswordInputElement.type = "password";

            CPTextFieldDOMPasswordInputElement.onblur = CPTextFieldBlurFunction;
        }

        CPTextFieldDOMInputElement = CPTextFieldDOMPasswordInputElement;
    }
    else
    {
        CPTextFieldDOMInputElement = CPTextFieldDOMStandardInputElement;
    }

    return CPTextFieldDOMInputElement;
}
#endif

- (id)initWithFrame:(CGRect)aFrame
{
    self = [super initWithFrame:aFrame];

    if (self)
    {
        [self setStringValue:@""];
        [self setPlaceholderString:@""];

        _sendActionOn = CPKeyUpMask | CPKeyDownMask;

        [self setValue:CPLeftTextAlignment forThemeAttribute:@"alignment"];
    }

    return self;
}

#pragma mark Controlling Editability and Selectability

/*!
    Sets whether or not the receiver text field can be edited. If NO, any
    ongoing edit is ended.
*/
- (void)setEditable:(BOOL)shouldBeEditable
{
    if (_isEditable === shouldBeEditable)
        return;

    _isEditable = shouldBeEditable;

    if (shouldBeEditable)
        _isSelectable = YES;

    // We only allow first responder status if the field is editable and enabled.
    if (!shouldBeEditable && [[self window] firstResponder] === self)
        [[self window] makeFirstResponder:nil];
}

/*!
    Returns \c YES if the textfield is currently editable by the user.
*/
- (BOOL)isEditable
{
    return _isEditable;
}

/*!
    Sets whether the field reacts to events. If NO, any ongoing edit is
    ended.
*/
- (void)setEnabled:(BOOL)shouldBeEnabled
{
    [super setEnabled:shouldBeEnabled];

    // We only allow first responder status if the field is editable and enabled.
    if (!shouldBeEnabled && [[self window] firstResponder] === self)
        [[self window] makeFirstResponder:nil];
}

/*!
    Sets whether the field's text is selectable by the user.
    @param aFlag \c YES makes the text selectable
*/
- (void)setSelectable:(BOOL)aFlag
{
    _isSelectable = aFlag;
}

/*!
    Returns \c YES if the field's text is selectable by the user.
*/
- (BOOL)isSelectable
{
    return _isSelectable;
}

/*!
    Sets whether the field's text is secure.
    @param aFlag \c YES makes the text secure
*/
- (void)setSecure:(BOOL)aFlag
{
    _isSecure = aFlag;
}

/*!
    Returns \c YES if the field's text is secure (password entry).
*/
- (BOOL)isSecure
{
    return _isSecure;
}

// Setting the Bezel Style
/*!
    Sets whether the textfield will have a bezeled border.
    @param shouldBeBezeled \c YES means the textfield will draw a bezeled border
*/
- (void)setBezeled:(BOOL)shouldBeBezeled
{
    if (shouldBeBezeled)
        [self setThemeState:CPThemeStateBezeled];
    else
        [self unsetThemeState:CPThemeStateBezeled];
}

/*!
    Returns \c YES if the textfield draws a bezeled border.
*/
- (BOOL)isBezeled
{
    return [self hasThemeState:CPThemeStateBezeled];
}

/*!
    Sets the textfield's bezel style.
    @param aBezelStyle the constant for the desired bezel style
*/
- (void)setBezelStyle:(CPTextFieldBezelStyle)aBezelStyle
{
    var shouldBeRounded = aBezelStyle === CPTextFieldRoundedBezel;

    if (shouldBeRounded)
        [self setThemeState:CPTextFieldStateRounded];
    else
        [self unsetThemeState:CPTextFieldStateRounded];
}

/*!
    Returns the textfield's bezel style.
*/
- (CPTextFieldBezelStyle)bezelStyle
{
    if ([self hasThemeState:CPTextFieldStateRounded])
        return CPTextFieldRoundedBezel;

    return CPTextFieldSquareBezel;
}

/*!
    Sets whether the textfield will have a border drawn.
    @param shouldBeBordered \c YES makes the textfield draw a border
*/
- (void)setBordered:(BOOL)shouldBeBordered
{
    if (shouldBeBordered)
        [self setThemeState:CPThemeStateBordered];
    else
        [self unsetThemeState:CPThemeStateBordered];
}

/*!
    Returns \c YES if the textfield has a border.
*/
- (BOOL)isBordered
{
    return [self hasThemeState:CPThemeStateBordered];
}

/*!
    Sets whether the textfield will have a background drawn.
    @param shouldDrawBackground \c YES makes the textfield draw a background
*/
- (void)setDrawsBackground:(BOOL)shouldDrawBackground
{
    if (_drawsBackground == shouldDrawBackground)
        return;

    _drawsBackground = shouldDrawBackground;

    [self setNeedsLayout];
    [self setNeedsDisplay:YES];
}

/*!
    Returns \c YES if the textfield draws a background.
*/
- (BOOL)drawsBackground
{
    return _drawsBackground;
}

/*!
    Sets the background color, which is shown for non-bezeled text fields with drawsBackground set to YES
    @param aColor The background color
*/
- (void)setTextFieldBackgroundColor:(CPColor)aColor
{
    if (_textFieldBackgroundColor == aColor)
        return;

    _textFieldBackgroundColor = aColor;

    [self setNeedsLayout];
    [self setNeedsDisplay:YES];
}

/*!
    Returns the background color.
*/
- (CPColor)textFieldBackgroundColor
{
    return _textFieldBackgroundColor;
}

/* @ignore */
- (BOOL)acceptsFirstResponder
{
    return [self isEditable] && [self isEnabled];
}

/* @ignore */
- (BOOL)becomeFirstResponder
{
#if PLATFORM(DOM)
    if (CPTextFieldInputOwner && [CPTextFieldInputOwner window] !== [self window])
        [[CPTextFieldInputOwner window] makeFirstResponder:nil];
#endif

    [self setThemeState:CPThemeStateEditing];

    [self _updatePlaceholderState];

    [self setNeedsLayout];

    _isEditing = NO;

#if PLATFORM(DOM)

    var string = [self stringValue],
        element = [self _inputElement],
        font = [self currentValueForThemeAttribute:@"font"];

    // generate the font metric
    [font _getMetrics];

    element.value = string;
    element.style.color = [[self currentValueForThemeAttribute:@"text-color"] cssString];
    element.style.font = [font cssString];
    element.style.zIndex = 1000;

    switch ([self alignment])
    {
        case CPCenterTextAlignment: element.style.textAlign = "center";
                                    break;
        case CPRightTextAlignment:  element.style.textAlign = "right";
                                    break;
        default:                    element.style.textAlign = "left";
    }

    var contentRect = [self contentRectForBounds:[self bounds]],
        verticalAlign = [self currentValueForThemeAttribute:"vertical-alignment"];

    switch (verticalAlign)
    {
        case CPTopVerticalTextAlignment:
            var topPoint = (_CGRectGetMinY(contentRect) + 1) + "px"; // for the same reason we have a -1 for the left, we also have a + 1 here
            break;

        case CPCenterVerticalTextAlignment:
            var topPoint = (_CGRectGetMidY(contentRect) - (font._lineHeight / 2) + 1) + "px";
            break;

        case CPBottomVerticalTextAlignment:
            var topPoint = (_CGRectGetMaxY(contentRect) - font._lineHeight) + "px";
            break;

        default:
            var topPoint = (_CGRectGetMinY(contentRect) + 1) + "px";
            break;
    }

    element.style.top = topPoint;
    element.style.left = (_CGRectGetMinX(contentRect) - 1) + "px"; // why -1?
    element.style.width = _CGRectGetWidth(contentRect) + "px";
    element.style.height = font._lineHeight + "px"; // private ivar for the line height of the DOM text at this particular size

    _DOMElement.appendChild(element);

    window.setTimeout(function()
    {
        element.focus();

        // Select the text if the textfield became first responder through keyboard interaction
        if (!_willBecomeFirstResponderByClick)
            [self selectText:self];

        _willBecomeFirstResponderByClick = NO;

        [self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]];
        CPTextFieldInputOwner = self;
    }, 0.0);

    element.value = [self stringValue];

    [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];

    CPTextFieldInputIsActive = YES;

    if (document.attachEvent)
    {
        CPTextFieldCachedSelectStartFunction = [[self window] platformWindow]._DOMBodyElement.onselectstart;
        CPTextFieldCachedDragFunction = [[self window] platformWindow]._DOMBodyElement.ondrag;

        [[self window] platformWindow]._DOMBodyElement.ondrag = function () {};
        [[self window] platformWindow]._DOMBodyElement.onselectstart = function () {};
    }
#endif

    return YES;
}

/* @ignore */
- (BOOL)resignFirstResponder
{
    [self unsetThemeState:CPThemeStateEditing];

    [self _updatePlaceholderState];

    [self setNeedsLayout];

#if PLATFORM(DOM)

    var element = [self _inputElement];

    if ([self stringValue] !== element.value)
        [self _setStringValue:element.value];

    CPTextFieldInputResigning = YES;

    if (CPTextFieldInputIsActive)
        element.blur();

    if (!CPTextFieldInputDidBlur)
        CPTextFieldBlurFunction();

    CPTextFieldInputDidBlur = NO;
    CPTextFieldInputResigning = NO;

    if (element.parentNode == _DOMElement)
        element.parentNode.removeChild(element);

    CPTextFieldInputIsActive = NO;

    if (document.attachEvent)
    {
        [[self window] platformWindow]._DOMBodyElement.ondrag = CPTextFieldCachedDragFunction;
        [[self window] platformWindow]._DOMBodyElement.onselectstart = CPTextFieldCachedSelectStartFunction;

        CPTextFieldCachedSelectStartFunction = nil;
        CPTextFieldCachedDragFunction = nil;
    }

#endif

    //post CPControlTextDidEndEditingNotification
    if (_isEditing)
    {
        _isEditing = NO;
        [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidEndEditingNotification object:self userInfo:nil]];

        if ([self sendsActionOnEndEditing])
            [self sendAction:[self action] to:[self target]];
    }

    [self textDidBlur:[CPNotification notificationWithName:CPTextFieldDidBlurNotification object:self userInfo:nil]];

    return YES;
}

/*!
    Text fields require panels to become key window, so this returns \c YES.
*/
- (BOOL)needsPanelToBecomeKey
{
    return YES;
}

- (void)mouseDown:(CPEvent)anEvent
{
    // Don't track! (ever?)
    if ([self isEditable] && [self isEnabled])
    {
        _willBecomeFirstResponderByClick = YES;
        [[self window] makeFirstResponder:self];
    }
    else if ([self isSelectable])
    {
        if (document.attachEvent)
        {
            CPTextFieldCachedSelectStartFunction = [[self window] platformWindow]._DOMBodyElement.onselectstart;
            CPTextFieldCachedDragFunction = [[self window] platformWindow]._DOMBodyElement.ondrag;

            [[self window] platformWindow]._DOMBodyElement.ondrag = function () {};
            [[self window] platformWindow]._DOMBodyElement.onselectstart = function () {};
        }
        return [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:YES];
    }
    else
        return [[self nextResponder] mouseDown:anEvent];
}

- (void)mouseUp:(CPEvent)anEvent
{
    if (![self isSelectable] && (![self isEditable] || ![self isEnabled]))
        [[self nextResponder] mouseUp:anEvent];
    else if ([self isSelectable])
    {
        if (document.attachEvent)
        {
            [[self window] platformWindow]._DOMBodyElement.ondrag = CPTextFieldCachedDragFunction;
            [[self window] platformWindow]._DOMBodyElement.onselectstart = CPTextFieldCachedSelectStartFunction;

            CPTextFieldCachedSelectStartFunction = nil
            CPTextFieldCachedDragFunction = nil;
        }
        return [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:YES];
    }
}

- (void)mouseDragged:(CPEvent)anEvent
{
    if (![self isSelectable] && (![self isEditable] || ![self isEnabled]))
        [[self nextResponder] mouseDragged:anEvent];
    else if ([self isSelectable])
        return [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:YES];
}

- (void)keyUp:(CPEvent)anEvent
{
    var oldValue = [self stringValue];
    [self _setStringValue:[self _inputElement].value];

    if (oldValue !== [self stringValue])
    {
        if (!_isEditing)
        {
            _isEditing = YES;
            [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
        }

        [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
    }

    [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
}

- (void)keyDown:(CPEvent)anEvent
{
    if ([anEvent _couldBeKeyEquivalent] && [self performKeyEquivalent:anEvent])
        return;

    // CPTextField uses an HTML input element to take the input so we need to
    // propagate the dom event so the element is updated. This has to be done
    // before interpretKeyEvents: though so individual commands have a chance
    // to override this (escape to clear the text in a search field for example).
    [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];

    [self interpretKeyEvents:[anEvent]];

    [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
}

/*!
    Invoke the action specified by aSelector on the current responder.

    This is implemented by CPResponder and by default it passes any unrecognized
    actions on to the next responder but text fields apparently aren't supposed
    to do that according to this documentation by Apple:

    http://developer.apple.com/mac/library/documentation/cocoa/reference/NSTextInputClient_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/NSTextInputClient/doCommandBySelector:
*/
- (void)doCommandBySelector:(SEL)aSelector
{
    if ([self respondsToSelector:aSelector])
        [self performSelector:aSelector];
}

- (void)insertNewline:(id)sender
{
    if (_isEditing)
    {
        _isEditing = NO;
        [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidEndEditingNotification object:self userInfo:nil]];
    }

    [self sendAction:[self action] to:[self target]];
    [self selectText:nil];

    [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
}

- (void)insertNewlineIgnoringFieldEditor:(id)sender
{
    var oldValue = [self stringValue];

    [self _inputElement].value += CPNewlineCharacter;
    [self _setStringValue:[self _inputElement].value];

    if (oldValue !== [self stringValue])
    {
        if (!_isEditing)
        {
            _isEditing = YES;
            [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
        }

        [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
    }
}

- (void)insertTabIgnoringFieldEditor:(id)sender
{
    var oldValue = [self stringValue];

    [self _inputElement].value += CPTabCharacter;
    [self _setStringValue:[self _inputElement].value];

    if (oldValue !== [self stringValue])
    {
        if (!_isEditing)
        {
            _isEditing = YES;
            [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
        }

        [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
    }
}

- (void)textDidBlur:(CPNotification)note
{
    // this looks to prevent false propagation of notifications for other objects
    if ([note object] != self)
        return;

    [[CPNotificationCenter defaultCenter] postNotification:note];
}

- (void)textDidFocus:(CPNotification)note
{
    // this looks to prevent false propagation of notifications for other objects
    if ([note object] != self)
        return;

    [[CPNotificationCenter defaultCenter] postNotification:note];
}

- (void)sendAction:(SEL)anAction to:(id)anObject
{
    [self _reverseSetBinding];

    [CPApp sendAction:anAction to:anObject from:self];
}

/*!
    Returns the string the text field.
*/
- (id)objectValue
{
    return [super objectValue];
}

/*
    @ignore
    Sets the internal string value without updating the value in the input element
*/
- (void)_setStringValue:(id)aValue
{
    [self willChangeValueForKey:@"objectValue"];
    [super setObjectValue:String(aValue)];
    [self _updatePlaceholderState];
    [self didChangeValueForKey:@"objectValue"];
}

- (void)setObjectValue:(id)aValue
{
    [super setObjectValue:aValue];

#if PLATFORM(DOM)

    if (CPTextFieldInputOwner === self || [[self window] firstResponder] === self)
        [self _inputElement].value = aValue;
#endif

    [self _updatePlaceholderState];
}

- (void)_updatePlaceholderState
{
    var string = [self stringValue];

    if ((!string || string.length === 0) && ![self hasThemeState:CPThemeStateEditing])
        [self setThemeState:CPTextFieldStatePlaceholder];
    else
        [self unsetThemeState:CPTextFieldStatePlaceholder];
}

/*!
    Sets a placeholder string for the receiver.  The placeholder is displayed until editing begins,
    and after editing ends, if the text field has an empty string value
*/
- (void)setPlaceholderString:(CPString)aStringValue
{
    if (_placeholderString === aStringValue)
        return;

    _placeholderString = aStringValue;

    // Only update things if we need to show the placeholder
    if ([self hasThemeState:CPTextFieldStatePlaceholder])
    {
        [self setNeedsLayout];
        [self setNeedsDisplay:YES];
    }
}

/*!
    Returns the receiver's placeholder string
*/
- (CPString)placeholderString
{
    return _placeholderString;
}

/*!
    For non-bezeled text fields (typically a label), sizeToFit has two behaviors, depending
    on the line break mode of the receiver.

    For non-bezeled receivers with a non-wrapping line break mode, sizeToFit will change the frame of the
    receiver to perfectly fit the current text in stringValue in the current font, respecting
    the current theme value for content-inset. For receivers with a wrapping line break mode,
    sizeToFit will wrap the text within the current width (respecting the current content-inset),
    so it will ONLY change the HEIGHT.

    For bezeled text fields (typically editable fields), sizeToFit will ONLY change the HEIGHT
    of the text field. It will not change the width of the text field. sizeToFit will attempt to
    change the height to fit a single line of text, respecting the current theme values for min-size,
    max-size and content-inset.

    The logic behind this decision is that most of the time you do not know what content will be placed
    in a bezeled text field, so you want to just choose a fixed width and leave it at that size.
    However, since you don't know how tall it needs to be if you change the font, sizeToFit will still be
    useful for making the textfield an appropriate height.
*/

- (void)sizeToFit
{
    [self setFrameSize:[self _minimumFrameSize]];
}

- (CGSize)_minimumFrameSize
{
    var frameSize = [self frameSize],
        contentInset = [self currentValueForThemeAttribute:@"content-inset"],
        minSize = [self currentValueForThemeAttribute:@"min-size"],
        maxSize = [self currentValueForThemeAttribute:@"max-size"],
        lineBreakMode = [self lineBreakMode],
        text = ([self stringValue] || @" "),
        textSize = _CGSizeMakeCopy(frameSize),
        font = [self currentValueForThemeAttribute:@"font"];

    textSize.width -= contentInset.left + contentInset.right;
    textSize.height -= contentInset.top + contentInset.bottom;

    if (frameSize.width !== 0 &&
        ![self isBezeled]     &&
        (lineBreakMode === CPLineBreakByWordWrapping || lineBreakMode === CPLineBreakByCharWrapping))
    {
        textSize = [text sizeWithFont:font inWidth:textSize.width];
    }
    else
        textSize = [text sizeWithFont:font];

    frameSize.height = textSize.height + contentInset.top + contentInset.bottom;

    if ([self isBezeled])
    {
        frameSize.height = MAX(frameSize.height, minSize.height);

        if (maxSize.width > 0.0)
            frameSize.width = MIN(frameSize.width, maxSize.width);

        if (maxSize.height > 0.0)
            frameSize.height = MIN(frameSize.height, maxSize.height);
    }
    else
        frameSize.width = textSize.width + contentInset.left + contentInset.right;

    frameSize.width = MAX(frameSize.width, minSize.width);

    return frameSize;
}

/*!
    Make the receiver the first responder and select all the text in the field.
*/
- (void)selectText:(id)sender
{
    // FIXME Should this really make the text field the first responder?

    if (([self isEditable] || [self isSelectable]))
    {
#if PLATFORM(DOM)
        var element = [self _inputElement];

        if ([[self window] firstResponder] === self)
            window.setTimeout(function() { element.select(); }, 0);
        else if ([self window] !== nil && [[self window] makeFirstResponder:self])
            window.setTimeout(function() {[self selectText:sender];}, 0);
#else
        // Even if we can't actually select the text we need to preserve the first
        // responder side effect.
        if ([self window] !== nil && [[self window] firstResponder] !== self)
            [[self window] makeFirstResponder:self];
#endif
    }
}

- (void)copy:(id)sender
{
    if (![CPPlatform isBrowser])
    {
        var selectedRange = [self selectedRange];

        if (selectedRange.length < 1)
            return;

        var pasteboard = [CPPasteboard generalPasteboard],
            stringValue = [self stringValue],
            stringForPasting = [stringValue substringWithRange:selectedRange];

        [pasteboard declareTypes:[CPStringPboardType] owner:nil];
        [pasteboard setString:stringForPasting forType:CPStringPboardType];
    }
}

- (void)cut:(id)sender
{
    if (![CPPlatform isBrowser])
    {
        [self copy:sender];
        [self deleteBackward:sender];
    }
    else
        [CPTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(keyUp:) userInfo:nil repeats:NO];
}

- (void)paste:(id)sender
{
    if (![CPPlatform isBrowser])
    {
        var pasteboard = [CPPasteboard generalPasteboard];

        if (![[pasteboard types] containsObject:CPStringPboardType])
            return;

        [self deleteBackward:sender];

        var selectedRange = [self selectedRange],
            stringValue = [self stringValue],
            pasteString = [pasteboard stringForType:CPStringPboardType],
            newValue = [stringValue stringByReplacingCharactersInRange:selectedRange withString:pasteString];

        [self setStringValue:newValue];
        [self setSelectedRange:CPMakeRange(selectedRange.location + pasteString.length, 0)];
    }
    else
        [CPTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(keyUp:) userInfo:nil repeats:NO];
}

- (CPRange)selectedRange
{
    if ([[self window] firstResponder] !== self)
        return CPMakeRange(0, 0);

    // we wrap this in try catch because firefox will throw an exception in certain instances
    try
    {
        var inputElement = [self _inputElement],
            selectionStart = inputElement.selectionStart,
            selectionEnd = inputElement.selectionEnd;

        if ([selectionStart isKindOfClass:CPNumber])
            return CPMakeRange(selectionStart, selectionEnd - selectionStart);

        // browsers which don't support selectionStart/selectionEnd (aka IE).
        var theDocument = inputElement.ownerDocument || inputElement.document,
            selectionRange = theDocument.selection.createRange(),
            range = inputElement.createTextRange();

        if (range.inRange(selectionRange))
        {
            range.setEndPoint('EndToStart', selectionRange);
            return CPMakeRange(range.text.length, selectionRange.text.length);
        }
    }
    catch (e)
    {
        // fall through to the return
{'fraction_non_alphanumeric': 0.09509094963524577, 'fraction_numerical': 0.002612213691822497, 'mean_word_length': 3.31854450405833, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3906812', 'n_tokens_mistral': 18468, 'n_tokens_neox': 17422, 'n_words': 4424}