/* * CPTextView.j * AppKit * * Created by Daniel Boehringer on 27/12/2013. * All modifications copyright Daniel Boehringer 2013. * Extensive code formatting and review by Andrew Hankinson * Based on original work by * Created by Emmanuel Maillard on 27/02/2010. * Copyright Emmanuel Maillard 2010. * * 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 "CPText.j" @import "CPPasteboard.j" @import "CPColorPanel.j" @import "CPFontManager.j" @import "CPTextStorage.j" @import "CPTextContainer.j" @import "CPLayoutManager.j" @import "_CPRTFParser.j" @import "_CPRTFProducer.j" @class CPClipView; @class _CPSelectionBox; @class _CPCaret; @class _CPNativeInputManager; @protocol CPTextViewDelegate - (BOOL)textView:(CPTextView)aTextView doCommandBySelector:(SEL)aSelector; - (BOOL)textView:(CPTextView)aTextView shouldChangeTextInRange:(CPRange)affectedCharRange replacementString:(CPString)replacementString; - (CPDictionary)textView:(CPTextView)textView shouldChangeTypingAttributes:(CPDictionary)oldTypingAttributes toAttributes:(CPDictionary)newTypingAttributes; - (CPRange)textView:(CPTextView)aTextView willChangeSelectionFromCharacterRange:(CPRange)oldSelectedCharRange toCharacterRange:(CPRange)newSelectedCharRange; - (void)textViewDidChangeSelection:(CPNotification)aNotification; - (void)textViewDidChangeTypingAttributes:(CPNotification)aNotification; @end _MakeRangeFromAbs = function(a1, a2) { return (a1 < a2) ? CPMakeRange(a1, a2 - a1) : CPMakeRange(a2, a1 - a2); }; _MidRange = function(a1) { return Math.floor((CPMaxRange(a1) + a1.location) / 2); }; function _isWhitespaceCharacter(chr) { return (chr === '\n' || chr === '\r' || chr === ' ' || chr === '\t'); } _characterTripletFromStringAtIndex = function(string, index) { if ([string isKindOfClass:CPAttributedString]) string = string._string; var tripletRange = _MakeRangeFromAbs(MAX(0, index - 1), MIN(string.length, index + 2)); return [string substringWithRange:tripletRange]; } /* CPSelectionGranularity */ @typedef CPSelectionGranularity CPSelectByCharacter = 0; CPSelectByWord = 1; CPSelectByParagraph = 2; var kDelegateRespondsTo_textShouldBeginEditing = 1 << 0, kDelegateRespondsTo_textView_doCommandBySelector = 1 << 1, kDelegateRespondsTo_textView_willChangeSelectionFromCharacterRange_toCharacterRange = 1 << 2, kDelegateRespondsTo_textView_shouldChangeTextInRange_replacementString = 1 << 3, kDelegateRespondsTo_textView_shouldChangeTypingAttributes_toAttributes = 1 << 4, kDelegateRespondsTo_textView_textDidChange = 1 << 5, kDelegateRespondsTo_textView_didChangeSelection = 1 << 6, kDelegateRespondsTo_textView_didChangeTypingAttributes = 1 << 7; @class _CPCaret; /*! @ingroup appkit @class CPTextView */ @implementation CPTextView : CPText { BOOL _allowsUndo @accessors(property=allowsUndo); BOOL _isHorizontallyResizable @accessors(getter=isHorizontallyResizable, setter=setHorinzontallyResizable:); BOOL _isVerticallyResizable @accessors(getter=isVerticallyResizable, setter=setVerticallyResizable:); BOOL _usesFontPanel @accessors(property=usesFontPanel); CGPoint _textContainerOrigin @accessors(getter=textContainerOrigin); CGSize _minSize @accessors(property=minSize); CGSize _maxSize @accessors(property=maxSize); CGSize _textContainerInset @accessors(property=textContainerInset); CPColor _insertionPointColor @accessors(property=insertionPointColor); CPColor _textColor @accessors(property=textColor); CPDictionary _selectedTextAttributes @accessors(property=selectedTextAttributes); CPDictionary _typingAttributes @accessors(property=typingAttributes); CPFont _font @accessors(property=font); CPLayoutManager _layoutManager @accessors(getter=layoutManager); CPRange _selectionRange @accessors(getter=selectedRange); CPSelectionGranularity _selectionGranularity @accessors(property=selectionGranularity); CPSelectionGranularity _previousSelectionGranularity; // private CPSelectionGranularity _copySelectionGranularity; // private CPTextContainer _textContainer @accessors(property=textContainer); CPTextStorage _textStorage @accessors(getter=textStorage); id _delegate @accessors(property=delegate); unsigned _delegateRespondsToSelectorMask; int _startTrackingLocation; _CPCaret _caret; CPTimer _scrollingTimer; BOOL _scrollingDownward; int _stickyXLocation; CPArray _selectionSpans; CPTimer _scrollingTimer; } #pragma mark - #pragma mark Class methods /* FIXME just a testing characterSet all of this depend of the current language. Need some CPLocale support and maybe even a FSM... */ #pragma mark - #pragma mark Init methods - (id)initWithFrame:(CGRect)aFrame textContainer:(CPTextContainer)aContainer { if (self = [super initWithFrame:aFrame]) { [self _init]; [aContainer setTextView:self]; [self setEditable:YES]; [self setSelectable:YES]; [self setRichText:NO]; [self setBackgroundColor:[CPColor whiteColor]]; _usesFontPanel = YES; _allowsUndo = YES; _selectedTextAttributes = [CPDictionary dictionaryWithObject:[CPColor selectedTextBackgroundColor] forKey:CPBackgroundColorAttributeName]; _insertionPointColor = [CPColor blackColor]; _textColor = [CPColor blackColor]; _font = [CPFont systemFontOfSize:12.0]; [self setFont:_font]; _typingAttributes = [[CPDictionary alloc] initWithObjects:[_font, _textColor] forKeys:[CPFontAttributeName, CPForegroundColorAttributeName]]; } [self registerForDraggedTypes:[CPColorDragType]]; return self; } - (id)initWithFrame:(CGRect)aFrame { var container = [[CPTextContainer alloc] initWithContainerSize:CGSizeMake(aFrame.size.width, 1e7)]; return [self initWithFrame:aFrame textContainer:container]; } - (void)_init { #if PLATFORM(DOM) _DOMElement.style.cursor = "text"; #endif _selectionRange = CPMakeRange(0, 0); _textContainerInset = CGSizeMake(2, 0); _textContainerOrigin = CGPointMake(_bounds.origin.x, _bounds.origin.y); _selectionGranularity = CPSelectByCharacter; _minSize = CGSizeCreateCopy(_frame.size); _maxSize = CGSizeMake(_frame.size.width, 1e7); _isVerticallyResizable = YES; _isHorizontallyResizable = NO; _typingAttributes = [CPMutableDictionary new]; _selectedTextAttributes = [CPMutableDictionary new]; _caret = [[_CPCaret alloc] initWithTextView:self]; [_caret setRect:CGRectMake(0, 0, 1, 11)] } - (void)_setObserveWindowKeyNotifications:(BOOL)shouldObserve { if (shouldObserve) { [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_windowDidResignKey:) name:CPWindowDidResignKeyNotification object:[self window]]; [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_windowDidBecomeKey:) name:CPWindowDidBecomeKeyNotification object:[self window]]; } else { [[CPNotificationCenter defaultCenter] removeObserver:self name:CPWindowDidResignKeyNotification object:[self window]]; [[CPNotificationCenter defaultCenter] removeObserver:self name:CPWindowDidBecomeKeyNotification object:[self window]]; } } - (void)_removeObservers { if (!_isObserving) return; [super _removeObservers]; [self _setObserveWindowKeyNotifications:NO]; } - (void)_addObservers { if (_isObserving) return; [super _addObservers]; [self _setObserveWindowKeyNotifications:YES]; } - (void)_windowDidResignKey:(CPNotification)aNotification { if (![[self window] isKeyWindow]) [self resignFirstResponder]; } - (void)_windowDidBecomeKey:(CPNotification)aNotification { if ([self _isFocused]) [self _becomeFirstResponder]; } #pragma mark - #pragma mark Copy and paste methods - (void)copy:(id)sender { [_CPNativeInputManager setLastCopyWasNative:[sender isKindOfClass:[_CPNativeInputManager class]]]; _copySelectionGranularity = _previousSelectionGranularity; [super copy:sender]; if (![self isRichText]) return; var selectedRange = [self selectedRange], pasteboard = [CPPasteboard generalPasteboard], stringForPasting = [_textStorage attributedSubstringFromRange:CPMakeRangeCopy(selectedRange)]; // put rich representation on the pasteboad only if we have multliple attributes selected if (stringForPasting._rangeEntries.length > 1) { var richData = [_CPRTFProducer produceRTF:stringForPasting documentAttributes:@{}]; // [pasteboard declareTypes:[CPStringPboardType, CPRichStringPboardType] owner:nil]; // this does currently do not work due to limitations in cappuccino [pasteboard setString:richData forType:CPStringPboardType]; // crude hack to make rich pasting possible in chrome and firefox: put rtf on the plain pasteboard } } - (void)paste:(id)sender { var e = [CPApp currentEvent]._DOMEvent; if (e && e.currentTarget == document) // this is somehow necessary to prevent double pasting return; var stringForPasting = [self _stringForPasting]; if (!stringForPasting) return; if (_copySelectionGranularity > 0 && _selectionRange.location > 0) { if (!_isWhitespaceCharacter([[_textStorage string] characterAtIndex:_selectionRange.location - 1]) && _selectionRange.location != [_layoutManager numberOfCharacters]) { [self insertText:" "]; } } if (_copySelectionGranularity == CPSelectByParagraph) { var peekStr = stringForPasting, i = 0; if (![stringForPasting isKindOfClass:[CPString class]]) peekStr = stringForPasting._string; while (_isWhitespaceCharacter([peekStr characterAtIndex:i])) i++; if (i) { if ([stringForPasting isKindOfClass:[CPString class]]) stringForPasting = [stringForPasting stringByReplacingCharactersInRange:CPMakeRange(0, i) withString:'']; else [stringForPasting replaceCharactersInRange:CPMakeRange(0, i) withString:'']; } } [self insertText:stringForPasting]; if (_copySelectionGranularity > 0) { if (!_isWhitespaceCharacter([[_textStorage string] characterAtIndex:CPMaxRange(_selectionRange)]) && !_isNewlineCharacter([[_textStorage string] characterAtIndex:MAX(0, _selectionRange.location - 1)]) && _selectionRange.location != [_layoutManager numberOfCharacters]) { [self insertText:" "]; } } } #pragma mark - #pragma mark Responders method - (BOOL)acceptsFirstResponder { return [self isSelectable]; // editable textviews are automatically selectable } - (void)_becomeFirstResponder { [self updateInsertionPointStateAndRestartTimer:YES]; [[CPFontManager sharedFontManager] setSelectedFont:[self font] isMultiple:NO]; [self setNeedsDisplay:YES]; [[CPRunLoop currentRunLoop] performSelector:@selector(focusForTextView:) target:[_CPNativeInputManager class] argument:self order:0 modes:[CPDefaultRunLoopMode]]; } - (BOOL)becomeFirstResponder { [super becomeFirstResponder]; [self _becomeFirstResponder]; return YES; } - (BOOL)resignFirstResponder { [_caret stopBlinking]; [self setNeedsDisplay:YES]; [_CPNativeInputManager cancelCurrentInputSessionIfNeeded]; return YES; } #pragma mark - #pragma mark Delegate methods /*! TODO : documentation */ - (void)setDelegate:(id )aDelegate { if (aDelegate === _delegate) return; _delegateRespondsToSelectorMask = 0; _delegate = aDelegate; if (_delegate) { if ([_delegate respondsToSelector:@selector(textDidChange:)]) _delegateRespondsToSelectorMask |= kDelegateRespondsTo_textView_textDidChange; if ([_delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) _delegateRespondsToSelectorMask |= kDelegateRespondsTo_textView_didChangeSelection; if ([_delegate respondsToSelector:@selector(textViewDidChangeTypingAttributes:)]) _delegateRespondsToSelectorMask |= kDelegateRespondsTo_textView_didChangeTypingAttributes; if ([_delegate respondsToSelector:@selector(textView:doCommandBySelector:)]) _delegateRespondsToSelectorMask |= kDelegateRespondsTo_textView_doCommandBySelector; if ([_delegate respondsToSelector:@selector(textShouldBeginEditing:)]) _delegateRespondsToSelectorMask |= kDelegateRespondsTo_textShouldBeginEditing; if ([_delegate respondsToSelector:@selector(textView:willChangeSelectionFromCharacterRange:toCharacterRange:)]) _delegateRespondsToSelectorMask |= kDelegateRespondsTo_textView_willChangeSelectionFromCharacterRange_toCharacterRange; if ([_delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementString:)]) _delegateRespondsToSelectorMask |= kDelegateRespondsTo_textView_shouldChangeTextInRange_replacementString; if ([_delegate respondsToSelector:@selector(textView:shouldChangeTypingAttributes:toAttributes:)]) _delegateRespondsToSelectorMask |= kDelegateRespondsTo_textView_shouldChangeTypingAttributes_toAttributes; } } #pragma mark - #pragma mark Key window methods - (void)becomeKeyWindow { [self setNeedsDisplay:YES]; } /*! @ignore */ - (void)resignKeyWindow { [self setNeedsDisplay:YES]; } - (BOOL)_isFirstResponder { return [[self window] firstResponder] === self; } - (BOOL)_isFocused { return [[self window] isKeyWindow] && [self _isFirstResponder]; } #pragma mark - #pragma mark Undo redo methods - (void)undo:(id)sender { if (_allowsUndo) [[[self window] undoManager] undo]; } - (void)redo:(id)sender { if (_allowsUndo) [[[self window] undoManager] redo]; } #pragma mark - #pragma mark Accessors - (CPString)stringValue { return _textStorage._string; } // fixme: rich text should return attributed string, shouldn't it? - (CPString)objectValue { return [self stringValue]; } - (void)setString:(CPString)aString { [_textStorage replaceCharactersInRange:CPMakeRange(0, [_layoutManager numberOfCharacters]) withString:aString]; [self setSelectedRange:CPMakeRange(0, [aString length])]; [self didChangeText]; [_layoutManager _validateLayoutAndGlyphs]; [self sizeToFit]; [self setNeedsDisplay:YES]; } - (CPString)string { return [_textStorage string]; } // KVO support - (void)setValue:(CPString)aValue { [self setString:[aValue description]]; } - (id)value { [self string]; } - (void)setTextContainer:(CPTextContainer)aContainer { _textContainer = aContainer; _layoutManager = [_textContainer layoutManager]; _textStorage = [_layoutManager textStorage]; [_textStorage setFont:_font]; [_textStorage setForegroundColor:_textColor]; [self invalidateTextContainerOrigin]; } - (void)setTextContainerInset:(CGSize)aSize { _textContainerInset = aSize; [self invalidateTextContainerOrigin]; } - (void)invalidateTextContainerOrigin { _textContainerOrigin.x = _bounds.origin.x; _textContainerOrigin.x += _textContainerInset.width; _textContainerOrigin.y = _bounds.origin.y; _textContainerOrigin.y += _textContainerInset.height; } - (void)doCommandBySelector:(SEL)aSelector { if (![self _sendDelegateDoCommandBySelector:aSelector]) [super doCommandBySelector:aSelector]; } - (void)didChangeText { [[CPNotificationCenter defaultCenter] postNotificationName:CPTextDidChangeNotification object:self]; if (_delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_textDidChange) [_delegate textDidChange:[[CPNotification alloc] initWithName:CPTextDidChangeNotification object:self userInfo:nil]]; } - (BOOL)shouldChangeTextInRange:(CPRange)aRange replacementString:(CPString)aString { if (![self isEditable]) return NO; return [self _sendDelegateTextShouldBeginEditing] && [self _sendDelegateShouldChangeTextInRange:aRange replacementString:aString]; } #pragma mark - #pragma mark Insert characters methods - (void)_fixupReplaceForRange:(CPRange)aRange { [self setSelectedRange:aRange]; [_layoutManager _validateLayoutAndGlyphs]; [self sizeToFit]; [self scrollRangeToVisible:_selectionRange]; [self setNeedsDisplay:YES]; } - (void)_replaceCharactersInRange:aRange withAttributedString:(CPString)aString { [[[[self window] undoManager] prepareWithInvocationTarget:self] _replaceCharactersInRange:CPMakeRange(aRange.location, [aString length]) withAttributedString:[_textStorage attributedSubstringFromRange:CPMakeRangeCopy(aRange)]]; [_textStorage replaceCharactersInRange:aRange withAttributedString:aString]; [self _fixupReplaceForRange:CPMakeRange(CPMaxRange(aRange), 0)]; } - (void)_replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString { [[[[self window] undoManager] prepareWithInvocationTarget:self] _replaceCharactersInRange:CPMakeRange(aRange.location, [aString length]) withString:[[self string] substringWithRange:CPMakeRangeCopy(aRange)]]; [_textStorage replaceCharactersInRange:CPMakeRangeCopy(aRange) withString:aString]; [self _fixupReplaceForRange:CPMakeRange(CPMaxRange(aRange), 0)]; } - (void)insertText:(CPString)aString { var isAttributed = [aString isKindOfClass:CPAttributedString], string = isAttributed ? [aString string]:aString; if (![self shouldChangeTextInRange:CPMakeRangeCopy(_selectionRange) replacementString:string]) return; if (!isAttributed) aString = [[CPAttributedString alloc] initWithString:aString attributes:_typingAttributes]; var undoManager = [[self window] undoManager]; [undoManager setActionName:@"Replace/insert text"]; [[undoManager prepareWithInvocationTarget:self] _replaceCharactersInRange:CPMakeRange(_selectionRange.location, [aString length]) withAttributedString:[_textStorage attributedSubstringFromRange:CPMakeRangeCopy(_selectionRange)]]; [_textStorage replaceCharactersInRange:CPMakeRangeCopy(_selectionRange) withAttributedString:aString]; [self _setSelectedRange:CPMakeRange(_selectionRange.location + [string length], 0) affinity:0 stillSelecting:NO overwriteTypingAttributes:NO]; _startTrackingLocation = _selectionRange.location; [self didChangeText]; [_layoutManager _validateLayoutAndGlyphs]; [self sizeToFit]; [self scrollRangeToVisible:_selectionRange]; _stickyXLocation = MAX(0, _caret._rect.origin.x - 1); } #pragma mark - #pragma mark Drawing methods - (void)drawInsertionPointInRect:(CGRect)aRect color:(CPColor)aColor turnedOn:(BOOL)flag { [_caret setRect:aRect]; [_caret setVisibility:flag stop:NO]; } // remove unnecessary call to CGContextClearRect from CPView implementation - (void)displayRectIgnoringOpacity:(CGRect)aRect inContext:(CPGraphicsContext)aGraphicsContext { if ([self isHidden]) return; [self lockFocus]; [self drawRect:aRect]; [self unlockFocus]; } - (void)drawRect:(CGRect)aRect { #if PLATFORM(DOM) var range = [_layoutManager glyphRangeForBoundingRect:aRect inTextContainer:_textContainer]; for (var i = 0; i < [_selectionSpans count]; i++) [_selectionSpans[i] removeFromTextView]; _selectionSpans = []; if (_selectionRange.length) { var rects = [_layoutManager rectArrayForCharacterRange:_selectionRange withinSelectedCharacterRange:_selectionRange inTextContainer:_textContainer rectCount:nil], effectiveSelectionColor = [self _isFocused] ? [_selectedTextAttributes objectForKey:CPBackgroundColorAttributeName] : [CPColor _selectedTextBackgroundColorUnfocussed], lengthRect = rects.length; for (var i = 0; i < lengthRect; i++) { rects[i].origin.x += _textContainerOrigin.x; rects[i].origin.y += _textContainerOrigin.y; var newSpan = [[_CPSelectionBox alloc] initWithTextView:self rect:rects[i] color:effectiveSelectionColor]; [_selectionSpans addObject:newSpan]; } } if (range.length) [_layoutManager drawGlyphsForGlyphRange:range atPoint:_textContainerOrigin]; if ([self shouldDrawInsertionPoint]) { [self updateInsertionPointStateAndRestartTimer:NO]; [self drawInsertionPointInRect:_caret._rect color:_insertionPointColor turnedOn:_caret._drawCaret]; } else [_caret setVisibility:NO]; #endif } #pragma mark - #pragma mark Select methods - (void)selectAll:(id)sender { if ([self isSelectable]) { [_caret stopBlinking]; [self setSelectedRange:CPMakeRange(0, [_layoutManager numberOfCharacters])]; } } - (void)setSelectedRange:(CPRange)range { [_CPNativeInputManager cancelCurrentInputSessionIfNeeded]; [self setSelectedRange:range affinity:0 stillSelecting:NO]; } - (void)setSelectedRange:(CPRange)range affinity:(CPSelectionAffinity)affinity stillSelecting:(BOOL)selecting { [self _setSelectedRange:range affinity:affinity stillSelecting:selecting overwriteTypingAttributes:YES]; } - (void)_setSelectedRange:(CPRange)range affinity:(CPSelectionAffinity)affinity stillSelecting:(BOOL)selecting overwriteTypingAttributes:(BOOL)doOverwrite { var maxRange = CPMakeRange(0, [_layoutManager numberOfCharacters]); range = CPIntersectionRange(maxRange, range); if (!selecting && [self _delegateRespondsToWillChangeSelectionFromCharacterRangeToCharacterRange]) { _selectionRange = [self _sendDelegateWillChangeSelectionFromCharacterRange:_selectionRange toCharacterRange:range]; } else { _selectionRange = CPMakeRangeCopy(range); _selectionRange = [self selectionRangeForProposedRange:_selectionRange granularity:[self selectionGranularity]]; } if (_selectionRange.length) [_layoutManager invalidateDisplayForGlyphRange:_selectionRange]; else [self setNeedsDisplay:YES]; if (!selecting) { if ([self _isFirstResponder]) [self updateInsertionPointStateAndRestartTimer:((_selectionRange.length === 0) && ![_caret isBlinking])]; if (doOverwrite) [self setTypingAttributes:[_textStorage attributesAtIndex:CPMaxRange(range) effectiveRange:nil]]; [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeSelectionNotification object:self]; if (_delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_didChangeSelection) [_delegate textViewDidChangeSelection:[[CPNotification alloc] initWithName:CPTextViewDidChangeSelectionNotification object:self userInfo:nil]]; } if (!selecting && _selectionRange.length > 0) [_CPNativeInputManager focusForClipboardOfTextView:self]; } // interface to the _CPNativeInputManager - (void)_activateNativeInputElement:(DOMElemet)aNativeField { var attributes=[[self typingAttributes] copy]; [attributes setObject:[CPColor colorWithRed:1 green:1 blue:1 alpha:0] forKey:CPForegroundColorAttributeName]; // make it invisible var placeholderString = [[CPAttributedString alloc] initWithString:aNativeField.innerHTML attributes:attributes]; [self insertText:placeholderString]; // FIXME: this hack to provide the visual space for the inputmanager should at least bypass the undomanager var caretOrigin = [_layoutManager boundingRectForGlyphRange:CPMakeRange(MAX(0, _selectionRange.location - 1), 1) inTextContainer:_textContainer].origin; caretOrigin.y += [_layoutManager _characterOffsetAtLocation:MAX(0, _selectionRange.location - 1)]; caretOrigin.x += 2; // two pixel offset to the LHS character #if PLATFORM(DOM) aNativeField.style.left = caretOrigin.x+"px"; aNativeField.style.top = caretOrigin.y+"px"; aNativeField.style.font = [[_typingAttributes objectForKey:CPFontAttributeName] cssString]; aNativeField.style.color = [[_typingAttributes objectForKey:CPForegroundColorAttributeName] cssString]; #endif [_caret setVisibility:NO]; // hide our caret because now the system caret takes over } - (CPArray)selectedRanges { return [_selectionRange]; } - (CPRange)selectionRangeForProposedRange:(CPRange)proposedRange granularity:(CPSelectionGranularity)granularity { var textStorageLength = [_layoutManager numberOfCharacters]; if (textStorageLength == 0) return CPMakeRange(0, 0); if (proposedRange.location >= textStorageLength) proposedRange = CPMakeRange(textStorageLength, 0); if (CPMaxRange(proposedRange) > textStorageLength) proposedRange.length = textStorageLength - proposedRange.location; var string = [_textStorage string], regex, loc = proposedRange.location; switch (granularity) { case CPSelectByWord: regex = [[self class] _wordBoundaryRegex]; break; case CPSelectByParagraph: regex = [[self class] _paragraphBoundaryRegex]; break; default: return proposedRange; } if (loc > 0 && _isNewlineCharacter([string characterAtIndex:loc])) loc--; var granularRange = [self _characterRangeForIndex:loc inRange:proposedRange asDefinedByRegex:regex skip:YES]; if (proposedRange.length == 0 && _isNewlineCharacter([string characterAtIndex:proposedRange.location])) return _MakeRangeFromAbs(_isNewlineCharacter([string characterAtIndex:loc])? proposedRange.location : granularRange.location, proposedRange.location + 1); if (proposedRange.length) granularRange = CPUnionRange(granularRange, [self _characterRangeForIndex:CPMaxRange(proposedRange) inRange:proposedRange asDefinedByRegex:regex skip:YES]); switch (granularity) { case CPSelectByParagraph: if (_isNewlineCharacter([string characterAtIndex:CPMaxRange(granularRange)])) granularRange.length++; } return granularRange; } #pragma mark - #pragma mark Keyboard events - (void)keyDown:(CPEvent)event { var propagateFlag = CPBrowserIsEngine(CPGeckoBrowserEngine)? NO : YES; [[_window platformWindow] _propagateCurrentDOMEvent:propagateFlag]; // necessary for the _CPNativeInputManager to work in Chrome if ([_CPNativeInputManager isNativeInputFieldActive]) return; if ([event charactersIgnoringModifiers].charCodeAt(0) != 229) // filter out 229 because this would be inserted in chrome on each deadkey [self interpretKeyEvents:[event]]; [_caret setPermanentlyVisible:YES]; } - (void)keyUp:(CPEvent)event { [super keyUp:event]; [_caret setPermanentlyVisible:NO]; } #pragma mark - #pragma mark Mouse Events - (void)mouseDown:(CPEvent)event { [_CPNativeInputManager cancelCurrentInputSessionIfNeeded]; var fraction = [], point = [self convertPoint:[event locationInWindow] fromView:nil], granularities = [-1, CPSelectByCharacter, CPSelectByWord, CPSelectByParagraph]; [_caret setVisibility:NO]; // convert to container coordinate point.x -= _textContainerOrigin.x; point.y -= _textContainerOrigin.y; _startTrackingLocation = [_layoutManager glyphIndexForPoint:point inTextContainer:_textContainer fractionOfDistanceThroughGlyph:fraction]; if (_startTrackingLocation === CPNotFound) _startTrackingLocation = [_layoutManager numberOfCharacters]; if (fraction[0] > 0.5) _startTrackingLocation++; [self setSelectionGranularity:granularities[[event clickCount]]]; var setRange = CPMakeRange(_startTrackingLocation, 0); if ([event modifierFlags] & CPShiftKeyMask) setRange = _MakeRangeFromAbs(_startTrackingLocation < _MidRange(_selectionRange) ? CPMaxRange(_selectionRange) : _selectionRange.location, _startTrackingLocation); else _scrollingTimer = [CPTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(_supportScrolling:) userInfo:nil repeats:YES]; // fixme: only start if we are in the scrolling areas [self setSelectedRange:setRange affinity:0 stillSelecting:YES]; } - (void)_supportScrolling:(CPTimer)aTimer { [self mouseDragged:[CPApp currentEvent]]; } - (void)_clearRange:(CPRange)range { var rects = [_layoutManager rectArrayForCharacterRange:nil withinSelectedCharacterRange:range inTextContainer:_textContainer rectCount:nil], l = rects.length; for (var i = 0; i < l; i++) { rects[i].origin.x += _textContainerOrigin.x; rects[i].origin.y += _textContainerOrigin.y; [self setNeedsDisplayInRect:rects[i]]; } } - (void)mouseDragged:(CPEvent)event { var fraction = [], point = [self convertPoint:[event locationInWindow] fromView:nil]; // convert to container coordinate point.x -= _textContainerOrigin.x; point.y -= _textContainerOrigin.y; var oldRange = [self selectedRange], index = [_layoutManager glyphIndexForPoint:point inTextContainer:_textContainer fractionOfDistanceThroughGlyph:fraction]; if (index === CPNotFound) index = _scrollingDownward ? CPMaxRange(oldRange) : oldRange.location; if (fraction[0] > 0.5) index++; if (index > oldRange.location) { [self _clearRange:_MakeRangeFromAbs(oldRange.location,index)]; _scrollingDownward = YES; } if (index < CPMaxRange(oldRange)) { [self _clearRange:_MakeRangeFromAbs(index, CPMaxRange(oldRange))]; _scrollingDownward = NO; } if (index < _startTrackingLocation) [self setSelectedRange:CPMakeRange(index, _startTrackingLocation - index) affinity:0 stillSelecting:YES]; else [self setSelectedRange:CPMakeRange(_startTrackingLocation, index - _startTrackingLocation) affinity:0 stillSelecting:YES]; [self scrollRangeToVisible:CPMakeRange(index, 0)]; } // handle all the other methods from CPKeyBinding.j - (void)mouseUp:(CPEvent)event { /* will post CPTextViewDidChangeSelectionNotification */ _previousSelectionGranularity = [self selectionGranularity]; [self setSelectionGranularity:CPSelectByCharacter]; [self setSelectedRange:[self selectedRange] affinity:0 stillSelecting:NO]; var point = [_layoutManager locationForGlyphAtIndex:[self selectedRange].location]; _stickyXLocation = point.x; _startTrackingLocation = _selectionRange.location; if (_scrollingTimer) { [_scrollingTimer invalidate]; _scrollingTimer = nil; } } - (void)moveDown:(id)sender { if (![self isSelectable]) return; var fraction = [], nglyphs = [_layoutManager numberOfCharacters], sindex = CPMaxRange([self selectedRange]), rectSource = [_layoutManager boundingRectForGlyphRange:CPMakeRange(sindex, 1) inTextContainer:_textContainer], rectEnd = nglyphs ? [_layoutManager boundingRectForGlyphRange:CPMakeRange(nglyphs - 1, 1) inTextContainer:_textContainer] : rectSource, point = rectSource.origin; if (_stickyXLocation) point.x = _stickyXLocation; // FIXME: find a better way for getting the coordinates of the next line point.y += 2 + rectSource.size.height; var dindex= point.y >= CPRectGetMaxY(rectEnd) ? nglyphs : [_layoutManager glyphIndexForPoint:point inTextContainer:_textContainer fractionOfDistanceThroughGlyph:fraction], oldStickyLoc = _stickyXLocation; if (fraction[0] > 0.5) dindex++; [self _establishSelection:CPMakeRange(dindex, 0) byExtending:NO]; _stickyXLocation = oldStickyLoc; [self scrollRangeToVisible:CPMakeRange(dindex, 0)] } - (void)moveDownAndModifySelection:(id)sender { if (![self isSelectable]) return; var oldStartTrackingLocation = _startTrackingLocation; [self _performSelectionFixupForRange:CPMakeRange(_selectionRange.location < _startTrackingLocation ? _selectionRange.location : CPMaxRange(_selectionRange), 0)]; [self moveDown:sender]; _startTrackingLocation = oldStartTrackingLocation; [self _performSelectionFixupForRange:_MakeRangeFromAbs(_startTrackingLocation, (_selectionRange.location < _startTrackingLocation ? _selectionRange.location : CPMaxRange(_selectionRange)))]; } - (void)moveUp:(id)sender { if (![self isSelectable]) return; var dindex = [self selectedRange].location; if (dindex < 1) return; var rectSource = [_layoutManager boundingRectForGlyphRange:CPMakeRange(dindex, 1) inTextContainer:_textContainer]; if (!(dindex === [_layoutManager numberOfCharacters] && _isNewlineCharacter([[_textStorage string] characterAtIndex:dindex - 1]))) dindex = [_layoutManager glyphIndexForPoint:CGPointMake(0, rectSource.origin.y + 1) inTextContainer:_textContainer fractionOfDistanceThroughGlyph:nil]; if (dindex < 1) return; var fraction = []; rectSource = [_layoutManager boundingRectForGlyphRange:CPMakeRange(dindex - 1, 1) inTextContainer:_textContainer]; dindex = [_layoutManager glyphIndexForPoint:CGPointMake(_stickyXLocation, rectSource.origin.y + 1) inTextContainer:_textContainer fractionOfDistanceThroughGlyph:fraction]; if (fraction[0] > 0.5) dindex++; var oldStickyLoc = _stickyXLocation; [self _establishSelection:CPMakeRange(dindex,0) byExtending:NO]; _stickyXLocation = oldStickyLoc; [self scrollRangeToVisible:CPMakeRange(dindex, 0)]; } - (void)moveUpAndModifySelection:(id)sender { if (![self isSelectable]) return; var oldStartTrackingLocation = _startTrackingLocation; [self _performSelectionFixupForRange:CPMakeRange(_selectionRange.location < _startTrackingLocation ? _selectionRange.location : CPMaxRange(_selectionRange), 0)]; [self moveUp:sender]; _startTrackingLocation = oldStartTrackingLocation; [self _performSelectionFixupForRange:_MakeRangeFromAbs(_startTrackingLocation, (_selectionRange.location < _startTrackingLocation ? _selectionRange.location : CPMaxRange(_selectionRange)))]; } - (void)_performSelectionFixupForRange:(CPRange)aSel { aSel.location = MAX(0, aSel.location); if (CPMaxRange(aSel) > [_layoutManager numberOfCharacters]) aSel = CPMakeRange([_layoutManager numberOfCharacters], 0); [self setSelectedRange:aSel]; var point = [_layoutManager locationForGlyphAtIndex:aSel.location]; _stickyXLocation = point.x; } - (void)_establishSelection:(CPSelection)aSel byExtending:(BOOL)flag { if (flag) aSel = CPUnionRange(aSel, _selectionRange); [self _performSelectionFixupForRange:aSel]; _startTrackingLocation = _selectionRange.location; } - (unsigned)_calculateMoveSelectionFromRange:(CPRange)aRange intoDirection:(integer)move granularity:(CPSelectionGranularity)granularity { var inWord = ![self _isCharacterAtIndex:(move > 0 ? CPMaxRange(aRange) : aRange.location) + move granularity:granularity], aSel = [self selectionRangeForProposedRange:CPMakeRange((move > 0 ? CPMaxRange(aRange) : aRange.location) + move, 0) granularity:granularity], bSel = [self selectionRangeForProposedRange:CPMakeRange((move > 0 ? CPMaxRange(aSel) : aSel.location) + move, 0) granularity:granularity]; return move > 0 ? CPMaxRange(inWord? aSel:bSel) : (inWord? aSel:bSel).location; } - (void)_moveSelectionIntoDirection:(integer)move granularity:(CPSelectionGranularity)granularity { var pos = [self _calculateMoveSelectionFromRange:_selectionRange intoDirection:move granularity:granularity]; [self _performSelectionFixupForRange:CPMakeRange(pos, 0)]; _startTrackingLocation = _selectionRange.location; } - (void)_extendSelectionIntoDirection:(integer)move granularity:(CPSelectionGranularity)granularity { var aSel = CPMakeRangeCopy(_selectionRange); if (granularity !== CPSelectByCharacter) { var pos = [self _calculateMoveSelectionFromRange:CPMakeRange(aSel.location < _startTrackingLocation ? aSel.location : CPMaxRange(aSel), 0) intoDirection:move granularity:granularity]; aSel = CPMakeRange(pos, 0); } else { aSel = CPMakeRange((aSel.location < _startTrackingLocation? aSel.location : CPMaxRange(aSel)) + move, 0); } aSel = _MakeRangeFromAbs(_startTrackingLocation, aSel.location); [self _performSelectionFixupForRange:aSel]; } - (void)moveLeftAndModifySelection:(id)sender { if ([self isSelectable]) [self _extendSelectionIntoDirection:-1 granularity:CPSelectByCharacter]; } - (void)moveBackward:(id)sender { [self moveLeft:sender]; } - (void)moveBackwardAndModifySelection:(id)sender { [self moveLeftAndModifySelection:sender]; } - (void)moveRightAndModifySelection:(id)sender { if ([self isSelectable]) [self _extendSelectionIntoDirection:1 granularity:CPSelectByCharacter]; } - (void)moveLeft:(id)sender { if ([self isSelectable]) [self _establishSelection:CPMakeRange(_selectionRange.location - (_selectionRange.length ? 0 : 1), 0) byExtending:NO]; } - (void)moveToEndOfParagraph:(id)sender { if ([self isSelectable]) [self _moveSelectionIntoDirection:1 granularity:CPSelectByParagraph]; } - (void)moveToEndOfParagraphAndModifySelection:(id)sender { if ([self isSelectable]) [self _extendSelectionIntoDirection:1 granularity:CPSelectByParagraph]; } - (void)moveParagraphForwardAndModifySelection:(id)sender { if ([self isSelectable]) [self _extendSelectionIntoDirection:1 granularity:CPSelectByParagraph]; } - (void)moveParagraphForward:(id)sender { if ([self isSelectable]) [self _moveSelectionIntoDirection:1 granularity:CPSelectByParagraph]; } - (void)moveWordBackwardAndModifySelection:(id)sender { [self moveWordLeftAndModifySelection:sender]; } - (void)moveWordBackward:(id)sender { [self moveWordLeft:sender]; } - (void)moveWordForwardAndModifySelection:(id)sender { [self moveWordRightAndModifySelection:sender]; } - (void)moveWordForward:(id)sender { [self moveWordRight:sender]; } - (void)moveToBeginningOfDocument:(id)sender { if ([self isSelectable]) [self _establishSelection:CPMakeRange(0, 0) byExtending:NO]; } - (void)moveToBeginningOfDocumentAndModifySelection:(id)sender { if ([self isSelectable]) [self _establishSelection:CPMakeRange(0, 0) byExtending:YES]; } - (void)moveToEndOfDocument:(id)sender { if ([self isSelectable]) [self _establishSelection:CPMakeRange([_layoutManager numberOfCharacters], 0) byExtending:NO]; } - (void)moveToEndOfDocumentAndModifySelection:(id)sender { if ([self isSelectable]) [self _establishSelection:CPMakeRange([_layoutManager numberOfCharacters], 0) byExtending:YES]; } - (void)moveWordRight:(id)sender { if ([self isSelectable]) [self _moveSelectionIntoDirection:1 granularity:CPSelectByWord]; } - (void)moveToBeginningOfParagraph:(id)sender { if ([self isSelectable]) [self _moveSelectionIntoDirection:-1 granularity:CPSelectByParagraph]; } - (void)moveToBeginningOfParagraphAndModifySelection:(id)sender { if ([self isSelectable]) [self _extendSelectionIntoDirection:-1 granularity:CPSelectByParagraph]; } - (void)moveParagraphBackward:(id)sender { if ([self isSelectable]) [self _moveSelectionIntoDirection:-1 granularity:CPSelectByParagraph]; } - (void)moveParagraphBackwardAndModifySelection:(id)sender { if ([self isSelectable]) [self _extendSelectionIntoDirection:-1 granularity:CPSelectByParagraph]; } - (void)moveWordRightAndModifySelection:(id)sender { if ([self isSelectable]) [self _extendSelectionIntoDirection:+1 granularity:CPSelectByWord]; } - (void)deleteToEndOfParagraph:(id)sender { if (![self isSelectable] || ![self isEditable]) return; [self moveToEndOfParagraphAndModifySelection:self]; [self delete:self]; } - (void)deleteToBeginningOfParagraph:(id)sender { if (![self isSelectable] || ![self isEditable]) return; [self moveToBeginningOfParagraphAndModifySelection:self]; [self delete:self]; } - (void)deleteToBeginningOfLine:(id)sender { if (![self isSelectable] || ![self isEditable]) return; [self moveToLeftEndOfLineAndModifySelection:self]; [self delete:self]; } - (void)deleteToEndOfLine:(id)sender { if (![self isSelectable] || ![self isEditable]) return; [self moveToRightEndOfLineAndModifySelection:self]; [self delete:self]; } - (void)deleteWordBackward:(id)sender { if (![self isSelectable] || ![self isEditable]) return; [self moveWordLeftAndModifySelection:self]; [self delete:self]; } - (void)deleteWordForward:(id)sender { if (![self isSelectable] || ![self isEditable]) return; [self moveWordRightAndModifySelection:self]; [self delete:self]; } - (void)moveToLeftEndOfLine:(id)sender byExtending:(BOOL)flag { if (![self isSelectable]) return; var nglyphs = [_layoutManager numberOfCharacters], loc = nglyphs == _selectionRange.location ? MAX(0, _selectionRange.location - 1) : _selectionRange.location, fragment = [_layoutManager _firstLineFragmentForLineFromLocation:loc]; if (fragment) [self _establishSelection:CPMakeRange(fragment._range.location, 0) byExtending:flag]; } - (void)moveToLeftEndOfLine:(id)sender { [self moveToLeftEndOfLine:sender byExtending:NO]; } - (void)moveToLeftEndOfLineAndModifySelection:(id)sender { [self moveToLeftEndOfLine:sender byExtending:YES]; } - (void)moveToRightEndOfLine:(id)sender byExtending:(BOOL)flag { if (![self isSelectable]) return; var fragment = [_layoutManager _lastLineFragmentForLineFromLocation:_selectionRange.location]; if (!fragment) return; var nglyphs = [_layoutManager numberOfCharacters], loc = nglyphs == CPMaxRange(fragment._range) ? nglyphs : MAX(0, CPMaxRange(fragment._range) - 1); [self _establishSelection:CPMakeRange(loc, 0) byExtending:flag]; } - (void)moveToRightEndOfLine:(id)sender { [self moveToRightEndOfLine:sender byExtending:NO]; } - (void)moveToRightEndOfLineAndModifySelection:(id)sender { [self moveToRightEndOfLine:sender byExtending:YES]; } - (void)moveWordLeftAndModifySelection:(id)sender { if ([self isSelectable]) [self _extendSelectionIntoDirection:-1 granularity:CPSelectByWord]; } - (void)moveWordLeft:(id)sender { if ([self isSelectable]) [self _moveSelectionIntoDirection:-1 granularity:CPSelectByWord] } - (void)moveRight:(id)sender { if ([self isSelectable]) [self _establishSelection:CPMakeRange(CPMaxRange(_selectionRange) + (_selectionRange.length ? 0 : 1), 0) byExtending:NO]; } - (void)_deleteForRange:(CPRange)changedRange { if (![self shouldChangeTextInRange:changedRange replacementString:@""]) return; changedRange = CPIntersectionRange(CPMakeRange(0, [_layoutManager numberOfCharacters]), changedRange); [[[_window undoManager] prepareWithInvocationTarget:self] _replaceCharactersInRange:CPMakeRange(changedRange.location, 0) withAttributedString:[_textStorage attributedSubstringFromRange:CPMakeRangeCopy(changedRange)]]; [_textStorage deleteCharactersInRange:CPMakeRangeCopy(changedRange)]; [self setSelectedRange:CPMakeRange(changedRange.location, 0)]; [self didChangeText]; [_layoutManager _validateLayoutAndGlyphs]; [self sizeToFit]; _stickyXLocation = _caret._rect.origin.x; } - (void)cancelOperation:(id)sender { [_CPNativeInputManager cancelCurrentInputSessionIfNeeded]; // handle ESC during native input } - (void)deleteBackward:(id)sender ignoreSmart:(BOOL)ignoreFlag { var changedRange; if (CPEmptyRange(_selectionRange) && _selectionRange.location > 0) changedRange = CPMakeRange(_selectionRange.location - 1, 1); else changedRange = _selectionRange; // smart delete if (!ignoreFlag && _copySelectionGranularity > 0 && changedRange.location > 0 && _isWhitespaceCharacter([[_textStorage string] characterAtIndex:_selectionRange.location - 1]) && changedRange.location < [[self string] length] && _isWhitespaceCharacter([[_textStorage string] characterAtIndex:CPMaxRange(changedRange)])) changedRange.length++; [self _deleteForRange:changedRange]; _startTrackingLocation = _selectionRange.location; } - (void)deleteBackward:(id)sender { _copySelectionGranularity = _previousSelectionGranularity; // smart delete [self deleteBackward:self ignoreSmart:_selectionRange.length > 0? NO:YES]; } - (void)deleteForward:(id)sender { var changedRange; if (CPEmptyRange(_selectionRange) && _selectionRange.location < [_layoutManager numberOfCharacters]) changedRange = CPMakeRange(_selectionRange.location, 1); else changedRange = _selectionRange; [self _deleteForRange:changedRange]; } - (void)cut:(id)sender { var selectedRange = [self selectedRange]; if (selectedRange.length < 1) return; [self copy:sender]; [self deleteBackward:sender ignoreSmart:NO]; } - (void)insertLineBreak:(id)sender { [self insertText:@"\n"]; } - (void)insertTab:(id)sender { [self insertText:@"\t"]; } - (void)insertTabIgnoringFieldEditor:(id)sender { [self insertTab:sender]; } - (void)insertNewlineIgnoringFieldEditor:(id)sender { [self insertLineBreak:sender]; } - (void)insertNewline:(id)sender { [self insertLineBreak:sender]; } - (void)_enrichEssentialTypingAttributes:(CPDictionary)attributes { if (![attributes containsKey:CPFontAttributeName]) [attributes setObject:[self font] forKey:CPFontAttributeName]; if (![attributes containsKey:CPForegroundColorAttributeName]) [attributes setObject:[self textColor] forKey:CPForegroundColorAttributeName]; } - (void)setTypingAttributes:(CPDictionary)attributes { if (!attributes) attributes = [CPDictionary dictionary]; if ([self _delegateRespondsToShouldChangeTypingAttributesToAttributes]) { _typingAttributes = [self _sendDelegateShouldChangeTypingAttributes:_typingAttributes toAttributes:attributes]; } else { _typingAttributes = [attributes copy]; [self _enrichEssentialTypingAttributes:_typingAttributes]; } [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; if (_delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_didChangeTypingAttributes) [_delegate textViewDidChangeTypingAttributes:[[CPNotification alloc] initWithName:CPTextViewDidChangeTypingAttributesNotification object:self userInfo:nil]]; } - (CPDictionary)_attributesForFontPanel { var attributes = [[_textStorage attributesAtIndex:CPMaxRange(_selectionRange) effectiveRange:nil] copy]; [self _enrichEssentialTypingAttributes:attributes]; return attributes; } - (void)delete:(id)sender { [self deleteBackward:sender]; } #pragma mark - #pragma mark Font methods - (void)setFont:(CPFont)font { _font = font; var length = [_layoutManager numberOfCharacters]; if (length) { [_textStorage addAttribute:CPFontAttributeName value:_font range:CPMakeRange(0, length)]; [_textStorage setFont:_font]; [self scrollRangeToVisible:CPMakeRange(length, 0)]; } } - (void)setFont:(CPFont)font range:(CPRange)range { if (![self isRichText]) { _font = font; [_textStorage setFont:_font]; } [_textStorage addAttribute:CPFontAttributeName value:font range:CPMakeRangeCopy(range)]; [_layoutManager _validateLayoutAndGlyphs]; [self scrollRangeToVisible:CPMakeRange(CPMaxRange(range), 0)]; } - (void)changeFont:(id)sender { var currRange = CPMakeRange(_selectionRange.location, 0), oldFont, attributes, scrollRange = CPMakeRange(CPMaxRange(_selectionRange), 0); if ([self isRichText]) { if (!CPEmptyRange(_selectionRange)) { while (CPMaxRange(currRange) < CPMaxRange(_selectionRange)) // iterate all "runs" { attributes = [_textStorage attributesAtIndex:CPMaxRange(currRange) longestEffectiveRange:currRange inRange:_selectionRange]; oldFont = [attributes objectForKey:CPFontAttributeName] || [self font]; [self setFont:[sender convertFont:oldFont] range:currRange]; } } else { [_typingAttributes setObject:[sender selectedFont] forKey:CPFontAttributeName]; } } else { var length = [_textStorage length]; oldFont = [self font]; [self setFont:[sender convertFont:oldFont] range:CPMakeRange(0, length)]; scrollRange = CPMakeRange(length, 0); } [_layoutManager _validateLayoutAndGlyphs]; [self sizeToFit]; [self setNeedsDisplay:YES]; [self scrollRangeToVisible:scrollRange]; } #pragma mark - #pragma mark Color methods - (void)changeColor:(id)sender { [self setTextColor:[sender color] range:_selectionRange]; } - (void)setTextColor:(CPColor)aColor { _textColor = aColor; if (_textColor) [_textStorage addAttribute:CPForegroundColorAttributeName value:_textColor range:CPMakeRange(0, [_layoutManager numberOfCharacters])]; else [_textStorage removeAttribute:CPForegroundColorAttributeName range:CPMakeRange(0, [_layoutManager numberOfCharacters])]; [_layoutManager _validateLayoutAndGlyphs]; [self scrollRangeToVisible:CPMakeRange([_layoutManager numberOfCharacters], 0)]; } - (void)setTextColor:(CPColor)aColor range:(CPRange)range { if (![self isRichText]) return; if (!CPEmptyRange(_selectionRange)) { if (aColor) [_textStorage addAttribute:CPForegroundColorAttributeName value:aColor range:CPMakeRangeCopy(range)]; else [_textStorage removeAttribute:CPForegroundColorAttributeName range:CPMakeRangeCopy(range)]; } else { [_typingAttributes setObject:aColor forKey:CPForegroundColorAttributeName]; } [_layoutManager _validateLayoutAndGlyphs]; [self setNeedsDisplay:YES]; [self scrollRangeToVisible:CPMakeRange(CPMaxRange(range), 0)]; } - (void)underline:(id)sender { if (![self shouldChangeTextInRange:_selectionRange replacementString:nil]) return; if (!CPEmptyRange(_selectionRange)) { var attrib = [_textStorage attributesAtIndex:_selectionRange.location effectiveRange:nil]; if ([attrib containsKey:CPUnderlineStyleAttributeName] && [[attrib objectForKey:CPUnderlineStyleAttributeName] intValue]) [_textStorage removeAttribute:CPUnderlineStyleAttributeName range:_selectionRange]; else [_textStorage addAttribute:CPUnderlineStyleAttributeName value:[CPNumber numberWithInt:1] range:CPMakeRangeCopy(_selectionRange)]; } else { if ([_typingAttributes containsKey:CPUnderlineStyleAttributeName] && [[_typingAttributes objectForKey:CPUnderlineStyleAttributeName] intValue]) [_typingAttributes setObject:[CPNumber numberWithInt:0] forKey:CPUnderlineStyleAttributeName]; else [_typingAttributes setObject:[CPNumber numberWithInt:1] forKey:CPUnderlineStyleAttributeName]; } } - (CPSelectionAffinity)selectionAffinity { return 0; } - (BOOL)isRulerVisible { return NO; } - (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString { [_textStorage replaceCharactersInRange:aRange withString:aString]; } - (void)setConstrainedFrameSize:(CGSize)desiredSize { [self setFrameSize:desiredSize]; } - (void)sizeToFit { [self setFrameSize:[self frameSize]]; } - (void)setFrameSize:(CGSize)aSize { var minSize = [self minSize], maxSize = [self maxSize], desiredSize = aSize, rect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(0, MAX(0, [_layoutManager numberOfCharacters] - 1)) inTextContainer:_textContainer], myClipviewSize = nil; if ([[self superview] isKindOfClass:[CPClipView class]]) myClipviewSize = [[self superview] frame].size; if ([_layoutManager extraLineFragmentTextContainer] === _textContainer) rect = CGRectUnion(rect, [_layoutManager extraLineFragmentRect]); if (_isHorizontallyResizable) { desiredSize.width = rect.size.width + 2 * _textContainerInset.width; if (desiredSize.width < minSize.width) desiredSize.width = minSize.width; else if (desiredSize.width > maxSize.width) desiredSize.width = maxSize.width; } if (_isVerticallyResizable) { desiredSize.height = rect.size.height + 2 * _textContainerInset.height; if (desiredSize.height < minSize.height) desiredSize.height = minSize.height; else if (desiredSize.height > maxSize.height) desiredSize.height = maxSize.height; } if (myClipviewSize) { if (desiredSize.width < myClipviewSize.width) desiredSize.width = myClipviewSize.width; if (desiredSize.height < myClipviewSize.height) desiredSize.height = myClipviewSize.height; } [super setFrameSize:desiredSize]; } - (void)scrollRangeToVisible:(CPRange)aRange { var rect; if (CPEmptyRange(aRange)) { if (aRange.location >= [_layoutManager numberOfCharacters]) rect = [_layoutManager extraLineFragmentRect]; else rect = [_layoutManager lineFragmentRectForGlyphAtIndex:aRange.location effectiveRange:nil]; } else { rect = [_layoutManager boundingRectForGlyphRange:aRange inTextContainer:_textContainer]; } rect.origin.x += _textContainerOrigin.x; rect.origin.y += _textContainerOrigin.y; [self scrollRectToVisible:rect]; } - (BOOL)_isCharacterAtIndex:(unsigned)index granularity:(CPSelectionGranularity)granularity { var characterSet; switch (granularity) { case CPSelectByWord: characterSet = [[self class] _wordBoundaryRegex]; break; case CPSelectByParagraph: characterSet = [[self class] _paragraphBoundaryRegex]; break; default: // FIXME if (!characterSet) croak! } return characterSet.exec(_characterTripletFromStringAtIndex([_textStorage string], index)) !== null; } + (CPArray)_wordBoundaryRegex { return /^(.|[\r\n])\W/m; } + (CPArray)_paragraphBoundaryRegex { return /^(.|[\r\n])[\n\r]/m; } - (CPRange)_characterRangeForIndex:(unsigned)index inRange:(CPRange) aRange asDefinedByRegex:(JSObject)regex skip:(BOOL)flag { var wordRange = CPMakeRange(index, 0), numberOfCharacters = [_layoutManager numberOfCharacters], string = [_textStorage string]; // do we start on a boundary character? if (flag && regex.exec(_characterTripletFromStringAtIndex([_textStorage string], index)) !== null) { // -> extend to the left for (var searchIndex = index - 1; searchIndex > 0 && regex.exec(_characterTripletFromStringAtIndex(string, searchIndex)) !== null; searchIndex--) wordRange.location = searchIndex; // -> extend to the right searchIndex = index + 1; while (searchIndex < numberOfCharacters && regex.exec(_characterTripletFromStringAtIndex(string, searchIndex)) !== null) searchIndex++; return _MakeRangeFromAbs(wordRange.location, MIN(MAX(0, numberOfCharacters - 1), searchIndex)); } // -> extend to the left for (var searchIndex = index - 1; searchIndex >= 0 && regex.exec(_characterTripletFromStringAtIndex(string, searchIndex)) === null; searchIndex--) wordRange.location = searchIndex; // -> extend to the right index++; while (index < numberOfCharacters && regex.exec(_characterTripletFromStringAtIndex(string, index)) === null) index++; return _MakeRangeFromAbs(wordRange.location, MIN(MAX(0, numberOfCharacters), index)); } - (CPRange)selectionRangeForProposedRange:(CPRange)proposedRange granularity:(CPSelectionGranularity)granularity { var textStorageLength = [_layoutManager numberOfCharacters]; if (textStorageLength == 0) return CPMakeRange(0, 0); if (proposedRange.location > textStorageLength) proposedRange = CPMakeRange(textStorageLength, 0); if (CPMaxRange(proposedRange) > textStorageLength) proposedRange.length = textStorageLength - proposedRange.location; var string = [_textStorage string]; switch (granularity) { case CPSelectByWord: var wordRange = [self _characterRangeForIndex:proposedRange.location inRange:proposedRange asDefinedByRegex:[[self class] _wordBoundaryRegex] skip:YES]; if (proposedRange.length) wordRange = CPUnionRange(wordRange, [self _characterRangeForIndex:CPMaxRange(proposedRange) inRange:proposedRange asDefinedByRegex:[[self class] _wordBoundaryRegex] skip:NO]); return wordRange; case CPSelectByParagraph: var parRange = [self _characterRangeForIndex:proposedRange.location inRange:proposedRange asDefinedByRegex:[[self class] _paragraphBoundaryRegex] skip:YES]; if (proposedRange.length) parRange = CPUnionRange(parRange, [self _characterRangeForIndex:CPMaxRange(proposedRange) inRange:proposedRange asDefinedByRegex:[[self class] _paragraphBoundaryRegex] skip:YES]); // mac-like paragraph selection with triple clicks if ([self _isCharacterAtIndex:CPMaxRange(parRange) granularity:CPSelectByParagraph]) parRange.length++; if (parRange.location > 0 && _isNewlineCharacter([[_textStorage string] characterAtIndex:parRange.location])) parRange = CPUnionRange(parRange, [self _characterRangeForIndex:parRange.location - 1 inRange:proposedRange asDefinedByRegex:[[self class] _paragraphBoundaryRegex] skip:YES]) return parRange; default: return proposedRange; } } - (BOOL)shouldDrawInsertionPoint { return (_selectionRange.length === 0 && [self _isFocused]); } - (void)updateInsertionPointStateAndRestartTimer:(BOOL)flag { var caretRect, nglyphs= [_layoutManager numberOfCharacters]; if (_selectionRange.length) [_caret setVisibility:NO]; if (_selectionRange.location >= nglyphs) // cursor is "behind" the last chacacter { caretRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(MAX(0,_selectionRange.location - 1), 1) inTextContainer:_textContainer]; if (!nglyphs) { var f = [_typingAttributes objectForKey:CPFontAttributeName]; caretRect.size.height = [f size]; caretRect.origin.y = ([f ascender] - [f descender]) * 0.5; } caretRect.origin.x += caretRect.size.width; if (_selectionRange.location > 0 && [[_textStorage string] characterAtIndex:_selectionRange.location - 1] === '\n') { caretRect.origin.y += caretRect.size.height; caretRect.origin.x = 0; } } else caretRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(_selectionRange.location, 1) inTextContainer:_textContainer]; var loc = (_selectionRange.location === nglyphs && nglyphs > 0) ? _selectionRange.location - 1 : _selectionRange.location, caretOffset = [_layoutManager _characterOffsetAtLocation:loc], oldYPosition = CGRectGetMaxY(caretRect), caretDescend = [_layoutManager _descentAtLocation:loc]; if (caretOffset > 0) { caretRect.origin.y += caretOffset; caretRect.size.height = oldYPosition - caretRect.origin.y; } if (caretDescend < 0) caretRect.size.height -= caretDescend; caretRect.origin.x += _textContainerOrigin.x; caretRect.origin.y += _textContainerOrigin.y; caretRect.size.width = 1; [_caret setRect:caretRect]; if (flag) [_caret startBlinking]; } #pragma mark - #pragma mark Dragging operation - (void)performDragOperation:(CPDraggingInfo)aSender { var location = [self convertPoint:[aSender draggingLocation] fromView:nil], pasteboard = [aSender draggingPasteboard]; if (![pasteboard availableTypeFromArray:[CPColorDragType]]) return NO; [self setTextColor:[CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:CPColorDragType]] range:_selectionRange]; } @end @implementation CPTextView (CPTextViewDelegate) - (BOOL)_delegateRespondsToShouldChangeTypingAttributesToAttributes { return _delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_shouldChangeTypingAttributes_toAttributes; } - (BOOL)_delegateRespondsToWillChangeSelectionFromCharacterRangeToCharacterRange { return _delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_willChangeSelectionFromCharacterRange_toCharacterRange; } - (BOOL)_sendDelegateDoCommandBySelector:(SEL)aSelector { if (!(_delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_doCommandBySelector)) return NO; return [_delegate textView:self doCommandBySelector:aSelector]; } - (BOOL)_sendDelegateTextShouldBeginEditing { if (!(_delegateRespondsToSelectorMask & kDelegateRespondsTo_textShouldBeginEditing)) return YES; return [_delegate textShouldBeginEditing:self]; } - (BOOL)_sendDelegateShouldChangeTextInRange:(CPRange)aRange replacementString:(CPString)aString { if (!(_delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_shouldChangeTextInRange_replacementString)) return YES; return [_delegate textView:self shouldChangeTextInRange:aRange replacementString:aString]; } - (CPDictionary)_sendDelegateShouldChangeTypingAttributes:(CPDictionary)typingAttributes toAttributes:(CPDictionary)attributes { if (!(_delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_doCommandBySelector)) return [CPDictionary dictionary]; return [_delegate textView:self shouldChangeTypingAttributes:typingAttributes toAttributes:attributes]; } - (CPRange)_sendDelegateWillChangeSelectionFromCharacterRange:(CPRange)selectionRange toCharacterRange:(CPRange)range { if (!(_delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_willChangeSelectionFromCharacterRange_toCharacterRange)) return CPMakeRange(0, 0); return [_delegate textView:self willChangeSelectionFromCharacterRange:selectionRange toCharacterRange:range]; } @end var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey", CPTextViewUsesFontPanelKey = @"CPTextViewUsesFontPanelKey", CPTextViewContainerKey = @"CPTextViewContainerKey", CPTextViewLayoutManagerKey = @"CPTextViewLayoutManagerKey", CPTextViewTextStorageKey = @"CPTextViewTextStorageKey", CPTextViewInsertionPointColorKey = @"CPTextViewInsertionPointColorKey", CPTextViewSelectedTextAttributesKey = @"CPTextViewSelectedTextAttributesKey", CPTextViewDelegateKey = @"CPTextViewDelegateKey"; @implementation CPTextView (CPCoding) - (id)initWithCoder:(CPCoder)aCoder { self = [super initWithCoder:aCoder]; if (self) { [self _init]; [self setInsertionPointColor:[aCoder decodeObjectForKey:CPTextViewInsertionPointColorKey]]; var selectedTextAttributes = [aCoder decodeObjectForKey:CPTextViewSelectedTextAttributesKey], enumerator = [selectedTextAttributes keyEnumerator], key; while (key = [enumerator nextObject]) [_selectedTextAttributes setObject:[selectedTextAttributes valueForKey:key] forKey:key]; if (![_selectedTextAttributes valueForKey:CPBackgroundColorAttributeName]) [_selectedTextAttributes setObject:[CPColor selectedTextBackgroundColor] forKey:CPBackgroundColorAttributeName]; [self setAllowsUndo:[aCoder decodeBoolForKey:CPTextViewAllowsUndoKey]]; [self setUsesFontPanel:[aCoder decodeBoolForKey:CPTextViewUsesFontPanelKey]]; [self setDelegate:[aCoder decodeObjectForKey:CPTextViewDelegateKey]]; var container = [aCoder decodeObjectForKey:CPTextViewContainerKey]; [container setTextView:self]; _typingAttributes = [[_textStorage attributesAtIndex:0 effectiveRange:nil] copy]; if (![_typingAttributes valueForKey:CPForegroundColorAttributeName]) [_typingAttributes setObject:[CPColor blackColor] forKey:CPForegroundColorAttributeName]; _textColor = [_typingAttributes valueForKey:CPForegroundColorAttributeName]; [self setFont:[_typingAttributes valueForKey:CPFontAttributeName]]; [self setString:[_textStorage string]]; } return self; } - (void)encodeWithCoder:(CPCoder)aCoder { [super encodeWithCoder:aCoder]; [aCoder encodeObject:_delegate forKey:CPTextViewDelegateKey]; [aCoder encodeObject:_textContainer forKey:CPTextViewContainerKey]; [aCoder encodeObject:_insertionPointColor forKey:CPTextViewInsertionPointColorKey]; [aCoder encodeObject:_selectedTextAttributes forKey:CPTextViewSelectedTextAttributesKey]; [aCoder encodeBool:_allowsUndo forKey:CPTextViewAllowsUndoKey]; [aCoder encodeBool:_usesFontPanel forKey:CPTextViewUsesFontPanelKey]; } @end @implementation _CPSelectionBox : CPObject { DOMElement _selectionBoxDOM; CGRect _rect; CPColor _color CPTextView _textView; } - (id)initWithTextView:(CPTextView)aTextView rect:(CGRect)aRect color:(CPColor)aColor { if (self = [super init]) { _textView = aTextView; _rect = aRect; _color = aColor; [self _createSpan]; _textView._DOMElement.appendChild(_selectionBoxDOM); } return self; } - (void)removeFromTextView { _textView._DOMElement.removeChild(_selectionBoxDOM); } - (void)_createSpan { #if PLATFORM(DOM) _selectionBoxDOM = document.createElement("span"); _selectionBoxDOM.style.position = "absolute"; _selectionBoxDOM.style.visibility = "visible"; _selectionBoxDOM.style.padding = "0px"; _selectionBoxDOM.style.margin = "0px"; _selectionBoxDOM.style.whiteSpace = "pre"; _selectionBoxDOM.style.backgroundColor = [_color cssString]; _selectionBoxDOM.style.width = (_rect.size.width) + "px"; _selectionBoxDOM.style.left = (_rect.origin.x) + "px"; _selectionBoxDOM.style.top = (_rect.origin.y) + "px"; _selectionBoxDOM.style.height = (_rect.size.height) + "px"; _selectionBoxDOM.style.zIndex = -1000; _selectionBoxDOM.oncontextmenu = _selectionBoxDOM.onmousedown = _selectionBoxDOM.onselectstart = function () { return false; }; #endif } @end @implementation _CPCaret : CPObject { BOOL _drawCaret; BOOL _permanentlyVisible @accessors(property=permanentlyVisible); CGRect _rect; CPTextView _textView; CPTimer _caretTimer; DOMElement _caretDOM; } - (void)setRect:(CGRect)aRect { _rect = CGRectCreateCopy(aRect); #if PLATFORM(DOM) _caretDOM.style.left = (aRect.origin.x) + "px"; _caretDOM.style.top = (aRect.origin.y) + "px"; _caretDOM.style.height = (aRect.size.height) + "px"; #endif } - (id)initWithTextView:(CPTextView)aView { if (self = [super init]) { #if PLATFORM(DOM) var style; if (!_caretDOM) { _caretDOM = document.createElement("span"); style = _caretDOM.style; style.position = "absolute"; style.visibility = "visible"; style.padding = "0px"; style.margin = "0px"; style.whiteSpace = "pre"; style.backgroundColor = "black"; _caretDOM.style.width = "1px"; _textView = aView; _textView._DOMElement.appendChild(_caretDOM); } #endif } return self; } - (void)setVisibility:(BOOL)visibilityFlag stop:(BOOL)stopFlag { #if PLATFORM(DOM) _caretDOM.style.visibility = visibilityFlag ? "visible" : "hidden"; #endif if (! visibilityFlag && stopFlag) [self stopBlinking]; } - (void)setVisibility:(BOOL)visibilityFlag { [self setVisibility:visibilityFlag stop:YES]; } - (void)_blinkCaret:(CPTimer)aTimer { _drawCaret = (!_drawCaret) || _permanentlyVisible; [_textView setNeedsDisplayInRect:_rect]; } - (void)startBlinking { _drawCaret = YES; if ([self isBlinking]) return; _caretTimer = [CPTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(_blinkCaret:) userInfo:nil repeats:YES]; } - (void)isBlinking { return [_caretTimer isValid]; } - (void)stopBlinking { _drawCaret = NO; if (_caretTimer) { [_caretTimer invalidate]; _caretTimer = nil; } } @end var _CPNativeInputField, _CPNativeInputFieldLastValue, _CPNativeInputFieldKeyUpCalled, _CPNativeInputFieldKeyPressedCalled, _CPNativeInputFieldActive, _CPNativeInputFieldLastCopyWasNative = 1; var _CPCopyPlaceholder = '-'; @implementation _CPNativeInputManager : CPObject + (BOOL)lastCopyWasNative { return _CPNativeInputFieldLastCopyWasNative; } + (void)setLastCopyWasNative:(BOOL)flag { _CPNativeInputFieldLastCopyWasNative = flag; } + (BOOL)isNativeInputFieldActive { return _CPNativeInputFieldActive; } + (void)cancelCurrentNativeInputSession { #if PLATFORM(DOM) if (_CPNativeInputField.innerHTML.length > 2) _CPNativeInputField.innerHTML = ''; #endif [self _endInputSessionWithString:_CPNativeInputField.innerHTML]; } + (void)cancelCurrentInputSessionIfNeeded { if (!_CPNativeInputFieldActive) return; [self cancelCurrentNativeInputSession]; } + (void)_endInputSessionWithString:(CPString)aStr { _CPNativeInputFieldActive = NO; var currentFirstResponder = [[CPApp keyWindow] firstResponder], placeholderRange = CPMakeRange([currentFirstResponder selectedRange].location - 1, 1); [currentFirstResponder setSelectedRange:placeholderRange]; [currentFirstResponder insertText:aStr]; [self hideInputElement]; [currentFirstResponder updateInsertionPointStateAndRestartTimer:YES]; } + (void)initialize { #if PLATFORM(DOM) _CPNativeInputField = document.createElement("div"); _CPNativeInputField.contentEditable = YES; _CPNativeInputField.style.width="64px"; _CPNativeInputField.style.zIndex = 10000; _CPNativeInputField.style.position = "absolute"; _CPNativeInputField.style.visibility = "visible"; _CPNativeInputField.style.padding = "0px"; _CPNativeInputField.style.margin = "0px"; _CPNativeInputField.style.whiteSpace = "pre"; _CPNativeInputField.style.outline = "0px solid transparent"; _CPNativeInputField.addEventListener("keyup", function(e) { _CPNativeInputFieldKeyUpCalled = YES; // filter out the shift-up, cursor keys and friends used to access the deadkeys // fixme: e.which is depreciated(?) -> find a better way to identify the modifier-keyups if (e.which < 27 || e.which == 91 || e.which == 93) // include apple command keys { if (_CPNativeInputField.innerHTML.length == 0 || _CPNativeInputField.innerHTML.length > 2) // backspace [self cancelCurrentInputSessionIfNeeded]; return false; // prevent the default behaviour } var currentFirstResponder = [[CPApp keyWindow] firstResponder]; if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)]) return false; // prevent the default behaviour var charCode = _CPNativeInputField.innerHTML.charCodeAt(0); if (charCode == 229 || charCode == 197) // å and Å need to be filtered out in keyDown: due to chrome inserting 229 on a deadkey { [currentFirstResponder insertText:_CPNativeInputField.innerHTML]; _CPNativeInputField.innerHTML = ''; return; } if (!_CPNativeInputFieldActive && _CPNativeInputFieldKeyPressedCalled == NO && _CPNativeInputField.innerHTML.length && _CPNativeInputField.innerHTML != _CPCopyPlaceholder && _CPNativeInputField.innerHTML.length < 3 && _CPNativeInputFieldLastValue !== _CPNativeInputField.innerHTML) // chrome-trigger: keypressed is omitted for deadkeys and cursor keys { _CPNativeInputFieldActive = YES; [currentFirstResponder _activateNativeInputElement:_CPNativeInputField]; } else { if (_CPNativeInputFieldActive) [self _endInputSessionWithString:_CPNativeInputField.innerHTML]; _CPNativeInputField.innerHTML = ''; } return false; // prevent the default behaviour }, true); _CPNativeInputField.addEventListener("keydown", function(e) { _CPNativeInputFieldLastValue = _CPNativeInputField.innerHTML; _CPNativeInputFieldKeyUpCalled = NO; _CPNativeInputFieldKeyPressedCalled = NO; var currentFirstResponder = [[CPApp keyWindow] firstResponder]; if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)]) return; // FF-trigger: here the best way to detect a dead key is the missing keyup event if (CPBrowserIsEngine(CPGeckoBrowserEngine)) setTimeout(function(){ if (!_CPNativeInputFieldActive && _CPNativeInputFieldKeyUpCalled == NO && _CPNativeInputField.innerHTML.length && _CPNativeInputField.innerHTML != _CPCopyPlaceholder && _CPNativeInputField.innerHTML.length < 3 && !e.repeat) { _CPNativeInputFieldActive = YES; [currentFirstResponder _activateNativeInputElement:_CPNativeInputField]; } else if (!_CPNativeInputFieldActive) [self hideInputElement]; }, 200); return false; }, true); // capture mode _CPNativeInputField.addEventListener("keypress", function(e) { _CPNativeInputFieldKeyUpCalled = YES; _CPNativeInputFieldKeyPressedCalled = YES; return false; }, true); // capture mode if (CPBrowserIsEngine(CPGeckoBrowserEngine)) { _CPNativeInputField.onpaste = function(e) { var pasteboard = [CPPasteboard generalPasteboard]; [pasteboard declareTypes:[CPStringPboardType] owner:nil]; var data = e.clipboardData.getData('text/plain'); [pasteboard setString:data forType:CPStringPboardType]; var currentFirstResponder = [[CPApp keyWindow] firstResponder]; setTimeout(function(){ // prevent dom-flickering [currentFirstResponder paste:self]; }, 20); return false; } _CPNativeInputField.oncopy = function(e) { var pasteboard = [CPPasteboard generalPasteboard], string, currentFirstResponder = [[CPApp keyWindow] firstResponder]; [currentFirstResponder copy:self]; // dataForPasting = [pasteboard dataForType:CPRichStringPboardType], stringForPasting = [pasteboard stringForType:CPStringPboardType]; e.clipboardData.setData('text/plain', stringForPasting); // e.clipboardData.setData('application/rtf', stringForPasting); // does not seem to work return false; } _CPNativeInputField.oncut = function(e) { var pasteboard = [CPPasteboard generalPasteboard], string, currentFirstResponder = [[CPApp keyWindow] firstResponder]; setTimeout(function(){ // prevent dom-flickering [currentFirstResponder cut:self]; }, 20); [currentFirstResponder copy:self]; // this is necessary because cut will only execute in the future // dataForPasting = [pasteboard dataForType:CPRichStringPboardType], stringForPasting = [pasteboard stringForType:CPStringPboardType]; e.clipboardData.setData('text/plain', stringForPasting); // e.clipboardData.setData('application/rtf', stringForPasting); // does not seem to work return false; } } #endif } + (void)focusForTextView:(CPTextView)currentFirstResponder { if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)]) return; [self hideInputElement]; #if PLATFORM(DOM) // only append the _CPNativeInputField if it is not already there var children = currentFirstResponder._DOMElement.childNodes, l = children.length; for (var i = 0; i < l; i++) { if (children[i] === _CPNativeInputField) // we are (almost) done { if (document.activeElement !== _CPNativeInputField) // focus the _CPNativeInputField if necessary _CPNativeInputField.focus(); return; } } currentFirstResponder._DOMElement.appendChild(_CPNativeInputField); _CPNativeInputField.focus(); #endif } + (void)focusForClipboardOfTextView:(CPTextView)textview { #if PLATFORM(DOM) if (!_CPNativeInputFieldActive && _CPNativeInputField.innerHTML.length == 0) _CPNativeInputField.innerHTML = _CPCopyPlaceholder; // make sure we have a selection to allow the native pasteboard work in safari [self focusForTextView:textview]; // select all in the contenteditable div (http://stackoverflow.com/questions/12243898/how-to-select-all-text-in-contenteditable-div) if (document.body.createTextRange) { var range = document.body.createTextRange(); range.moveToElementText(_CPNativeInputField); range.select(); } else if (window.getSelection) { var selection = window.getSelection(), range = document.createRange(); range.selectNodeContents(_CPNativeInputField); selection.removeAllRanges(); selection.addRange(range); } #endif } + (void)hideInputElement { #if PLATFORM(DOM) _CPNativeInputField.style.top="-10000px"; _CPNativeInputField.style.left="-10000px"; #endif } @end