diff --git a/AppKit/CPFont.j b/AppKit/CPFont.j index c9d53ebe2..0364ee72c 100644 --- a/AppKit/CPFont.j +++ b/AppKit/CPFont.j @@ -24,6 +24,7 @@ @import @import "CPView.j" +@import "CPFontDescriptor.j" CPFontDefaultSystemFontFace = @"Arial, sans-serif"; CPFontDefaultSystemFontSize = 12; @@ -433,6 +434,45 @@ following: @end +@implementation CPFont(DescriptorAdditions) + +- (id)_initWithFontDescriptor:(CPFontDescriptor)fontDescriptor +{ + var aName = [fontDescriptor objectForKey: CPFontNameAttribute] , + aSize = [fontDescriptor pointSize], + isBold = [fontDescriptor symbolicTraits] & CPFontBoldTrait, + isItalic = [fontDescriptor symbolicTraits] & CPFontItalicTrait; + + return [self _initWithName:aName size:aSize bold:isBold italic:isItalic system:NO]; +} + ++ (CPFont)fontWithDescriptor:(CPFontDescriptor)fontDescriptor size:(float)aSize +{ + var aName = [fontDescriptor objectForKey: CPFontNameAttribute], + isBold = [fontDescriptor symbolicTraits] & CPFontBoldTrait, + isItalic = [fontDescriptor symbolicTraits] & CPFontItalicTrait; + + return [self _fontWithName:aName size:aSize || [fontDescriptor pointSize] bold:isBold italic:isItalic]; +} + +- (CPFontDescriptor)fontDescriptor +{ + var traits = 0; + + if ([self isBold]) + traits |= CPFontBoldTrait; + + if ([self isItalic]) + traits |= CPFontItalicTrait; + + var descriptor = [[CPFontDescriptor fontDescriptorWithName:_name size:_size] fontDescriptorWithSymbolicTraits:traits]; + + return descriptor; +} + +@end + + var CPFontNameKey = @"CPFontNameKey", CPFontSizeKey = @"CPFontSizeKey", CPFontIsBoldKey = @"CPFontIsBoldKey", diff --git a/AppKit/CPFontManager.j b/AppKit/CPFontManager.j index b470025ba..4cb17da0e 100644 --- a/AppKit/CPFontManager.j +++ b/AppKit/CPFontManager.j @@ -23,6 +23,8 @@ @import @import "CPFont.j" +@import "CPFontPanel.j" +@import "CPFontDescriptor.j" @global CPApp @@ -41,7 +43,20 @@ CPUnitalicFontMask = 1 << 24; var CPSharedFontManager = nil, - CPFontManagerFactory = Nil; + CPFontManagerFactory = Nil, + CPFontPanelFactory = Nil; + +/* + modifyFont: sender's tag +*/ +CPNoFontChangeAction = 0; +CPViaPanelFontAction = 1; +CPAddTraitFontAction = 2; +CPSizeUpFontAction = 3; +CPSizeDownFontAction = 4; +CPHeavierFontAction = 5; +CPLighterFontAction = 6; +CPRemoveTraitFontAction = 7; /*! @ingroup appkit @@ -219,6 +234,174 @@ var CPSharedFontManager = nil, return [CPApp sendAction:_action to:_target from:self]; } + +/*! + This method open the font panel, create it if necessary. + @param sender The object that sent the message. +*/ +- (CPFontPanel)fontPanel:(BOOL)createIt +{ + var panel = nil, + panelExists = [CPFontPanelFactory sharedFontPanelExists]; + + if ((panelExists) || (!panelExists && createIt)) + panel = [CPFontPanelFactory sharedFontPanel]; + + return panel; +} + +/*! + Convert a font to have the specified Font traits. The font is unchanged expect for the specified Font traits. + Using CPUnboldFontMask or CPUnitalicFontMask will respectively remove Bold and Italic traits. + @param aFont The font to convert. + @param fontTrait The new font traits mask. + @result The converted font or \c aFont if the conversion failed. +*/ +- (CPFont)convertFont:(CPFont)aFont toHaveTrait:(CPFontTraitMask)fontTrait +{ + var attributes = [[[aFont fontDescriptor] fontAttributes] copy], + symbolicTrait = [[aFont fontDescriptor] symbolicTraits]; + + if (fontTrait & CPBoldFontMask) + symbolicTrait |= CPFontBoldTrait; + + if (fontTrait & CPItalicFontMask) + symbolicTrait |= CPFontItalicTrait; + + if (fontTrait & CPUnboldFontMask) /* FIXME: this only change CPFontSymbolicTrait what about CPFontWeightTrait */ + symbolicTrait &= ~CPFontBoldTrait; + + if (fontTrait & CPUnitalicFontMask) + symbolicTrait &= ~CPFontItalicTrait; + + if (fontTrait & CPExpandedFontMask) + symbolicTrait |= CPFontExpandedTrait; + + if (fontTrait & CPSmallCapsFontMask) + symbolicTrait |= CPFontSmallCapsTrait; + + if (![attributes containsKey:CPFontTraitsAttribute]) + [attributes setObject:[CPDictionary dictionaryWithObject:[CPNumber numberWithUnsignedInt:symbolicTrait] + forKey:CPFontSymbolicTrait] forKey:CPFontTraitsAttribute]; + else + [[attributes objectForKey:CPFontTraitsAttribute] setObject:[CPNumber numberWithUnsignedInt:symbolicTrait] + forKey:CPFontSymbolicTrait]; + + return [[aFont class] fontWithDescriptor:[CPFontDescriptor fontDescriptorWithFontAttributes:attributes] size:0.0]; +} + +/*! + Convert a font to not have the specified Font traits. The font is unchanged expect for the specified Font traits. + @param aFont The font to convert. + @param fontTrait The font traits mask to remove. + @result The converted font or \c aFont if the conversion failed. +*/ +- (CPFont)convertFont:(CPFont)aFont toNotHaveTrait:(CPFontTraitMask)fontTrait +{ + var attributes = [[[aFont fontDescriptor] fontAttributes] copy], + symbolicTrait = [[aFont fontDescriptor] symbolicTraits]; + + if ((fontTrait & CPBoldFontMask) || (fontTrait & CPUnboldFontMask)) /* FIXME: see convertFont:toHaveTrait: about CPFontWeightTrait */ + symbolicTrait &= ~CPFontBoldTrait; + + if ((fontTrait & CPItalicFontMask) || (fontTrait & CPUnitalicFontMask)) + symbolicTrait &= ~CPFontItalicTrait; + + if (fontTrait & CPExpandedFontMask) + symbolicTrait &= ~CPFontExpandedTrait; + + if (fontTrait & CPSmallCapsFontMask) + symbolicTrait &= ~CPFontSmallCapsTrait; + + if (![attributes containsKey:CPFontTraitsAttribute]) + [attributes setObject:[CPDictionary dictionaryWithObject:[CPNumber numberWithUnsignedInt:symbolicTrait] forKey:CPFontSymbolicTrait] forKey:CPFontTraitsAttribute]; + else + [[attributes objectForKey:CPFontTraitsAttribute] setObject:[CPNumber numberWithUnsignedInt:symbolicTrait] forKey:CPFontSymbolicTrait]; + + return [[aFont class] fontWithDescriptor:[CPFontDescriptor fontDescriptorWithFontAttributes:attributes] size:0.0]; +} + +/*! + Convert a font to have specified size. The font is unchanged expect for the specified size. + @param aFont The font to convert. + @param aSize The new font size. + @result The converted font or \c aFont if the conversion failed. +*/ +- (CPFont)convertFont:(CPFont)aFont toSize:(float)aSize +{ + var descriptor = [aFont fontDescriptor]; + + return [[aFont class] fontWithDescriptor: descriptor size:aSize] +} + +- (void)orderFrontFontPanel:(id)sender +{ + [[self fontPanel:YES] orderFront:sender]; +} + +- (void)modifyFont:(id)sender +{ + _fontAction = [sender tag]; + [self sendAction]; + + if (_selectedFont) + [self setSelectedFont:[self convertFont:_selectedFont] isMultiple:NO]; +} + +/*! + This method causes the receiver to send its action message. + @param sender The object that sent the message. (a Font panel) +*/ +- (void)modifyFontViaPanel:(id)sender +{ + _fontAction = CPViaPanelFontAction; + if (_selectedFont) + [self setSelectedFont:[self convertFont:_selectedFont] isMultiple:NO]; + + [self sendAction]; +} + +/*! + Convert a font according to current font changes, provided by the object that initiated the font change. + @param aFont The font to convert. + @result The converted font or \c aFont if the conversion failed. +*/ +- (CPFont)convertFont:(CPFont)aFont +{ + var newFont = nil; + switch (_fontAction) + { + case CPNoFontChangeAction: + newFont = aFont; + break; + + case CPViaPanelFontAction: + newFont = [[self fontPanel:NO] panelConvertFont:aFont]; + break; + + case CPAddTraitFontAction: + newFont = [self convertFont:aFont toHaveTrait:_currentFontTrait]; + break; + + case CPSizeUpFontAction: + newFont = [self convertFont:aFont toSize:[aFont size] + 1.0]; /* any limit ? */ + break; + + case CPSizeDownFontAction: + if ([aFont size] > 1) + newFont = [self convertFont:aFont toSize:[aFont size] - 1.0]; + /* else CPBeep() :-p */ + break; + + default: + CPLog.trace(@"-[" + [self className] + " " + _cmd + "] unsupported font action: " + _fontAction + " aFont unchanged"); + newFont = aFont; + break; + } + + return newFont; +} + @end var _CPFontDetectSpan, @@ -300,3 +483,4 @@ var _CPFontDetectPickTwoDifferentFonts = function(candidates) }; [CPFontManager setFontManagerFactory:[CPFontManager class]]; +[CPFontManager setFontPanelFactory:CPFontPanel]; diff --git a/AppKit/CPText.j b/AppKit/CPText.j index dda60f133..bbe6af959 100644 --- a/AppKit/CPText.j +++ b/AppKit/CPText.j @@ -5,6 +5,15 @@ * Created by Alexander Ljungberg. * Copyright 2010, WireLoad, LLC. * + * additions from + * + * Daniel Boehringer on 8/02/2014. + * Copyright Daniel Boehringer on 8/02/2014. + * + * Emmanuel Maillard on 28/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 @@ -20,6 +29,13 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +@import "CPView.j" +@import "RTFProducer.j" +@import "RTFParser.j" + + +CPParagraphSeparatorCharacter = 0x2029; +CPLineSeparatorCharacter = 0x2028; CPEnterCharacter = "\u0003"; CPBackspaceCharacter = "\u0008"; CPTabCharacter = "\u0009"; @@ -38,4 +54,205 @@ CPLeftTextMovement = 19; CPRightTextMovement = 20; CPUpTextMovement = 21; CPDownTextMovement = 22; -CPCancelTextMovement = 23; \ No newline at end of file +CPCancelTextMovement = 23; + +/* + CPText notifications +*/ +CPTextDidBeginEditingNotification = @"CPTextDidBeginEditingNotification"; +CPTextDidChangeNotification = @"CPTextDidChangeNotification"; +CPTextDidEndEditingNotification = @"CPTextDidEndEditingNotification"; + +@implementation CPText : CPControl +{ +} + +- (void)changeFont:(id)sender +{ + CPLog.error(@"-[CPText " + _cmd + "] subclass responsibility"); +} + +- (void)copy:(id)sender +{ + var selectedRange = [self selectedRange]; + + if (selectedRange.length < 1) + return; + + var pasteboard = [CPPasteboard generalPasteboard], + stringForPasting = [[self stringValue] substringWithRange:selectedRange]; + + [pasteboard declareTypes:[CPStringPboardType] owner:nil]; + + if ([self isRichText]) + { + // crude hack to make rich pasting possible in chrome and firefox. this requires a RTF roundtrip, unfortunately + var richData = [RTFProducer produceRTF:[[self textStorage] attributedSubstringFromRange:selectedRange] documentAttributes: @{}]; + [pasteboard setString:richData forType:CPStringPboardType]; + } + else + [pasteboard setString:stringForPasting forType:CPStringPboardType]; + +} +- (void)paste:(id)sender +{ + var pasteboard = [CPPasteboard generalPasteboard], + // dataForPasting = [pasteboard dataForType:CPRichStringPboardType], + stringForPasting = [pasteboard stringForType:CPStringPboardType]; + + if ([stringForPasting hasPrefix:"{\\rtf1\\ansi"]) + stringForPasting = [[_RTFParser new] parseRTF:stringForPasting]; + + if (![self isRichText] && [stringForPasting isKindOfClass:[CPAttributedString class]]) + stringForPasting = stringForPasting._string; + + if (stringForPasting) + [self insertText:stringForPasting]; +} + +- (void)copyFont:(id)sender +{ + CPLog.error(@"-[CPText " + _cmd + "] subclass responsibility"); +} + +- (void)cut:(id)sender +{ + [self copy:sender]; + + var loc = [self selectedRange].location; + + [self replaceCharactersInRange:[self selectedRange] withString:""]; + [self setSelectedRange:CPMakeRange(loc,0) ]; +} + +- (void)delete:(id)sender +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (CPFont)font:(CPFont)aFont +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); + return nil; +} + +- (BOOL)isHorizontallyResizable +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); + return NO; +} + +- (BOOL)isRichText +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); + return NO; +} + +- (BOOL)isRulerVisible +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); + return NO; +} + +- (BOOL)isVerticallyResizable +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); + return NO; +} + +- (CPSize)maxSize +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); + return CPMakeSize(0,0); +} + +- (CPSize)minSize +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); + return CPMakeSize(0,0); +} + +- (void)pasteFont:(id)sender +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (void)scrollRangeToVisible:(CPRange)aRange +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (void)selectedAll:(id)sender +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (CPRange)selectedRange +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); + return CPMakeRange(CPNotFound, 0); +} + +- (void)setFont:(CPFont)aFont +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (void)setFont:(CPFont)aFont rang:(CPRange)aRange +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (void)setHorizontallyResizable:(BOOL)flag +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (void)setMaxSize:(CPSize)aSize +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (void)setMinSize:(CPSize)aSize +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (void)setString:(CPString)aString +{ + [self replaceCharactersInRange: CPMakeRange(0, [[self string] length]) withString:aString]; +} + +- (void)setUsesFontPanel:(BOOL)flag +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (void)setVerticallyResizable:(BOOL)flag +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (CPString)string +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); + return nil; +} + +- (void)underline:(id)sender +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); +} + +- (BOOL)usesFontPanel +{ + CPLog.error(@"-[CPText "+_cmd+"] subclass responsibility"); + return NO; +} + +@end diff --git a/AppKit/CPTextView/CPFontDescriptor.j b/AppKit/CPTextView/CPFontDescriptor.j new file mode 100755 index 000000000..1fce66c63 --- /dev/null +++ b/AppKit/CPTextView/CPFontDescriptor.j @@ -0,0 +1,328 @@ +/* + * CPFontDescriptor.j + * AppKit + * + * Created by Emmanuel Maillard on 07/03/10. + * 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 +/* + Font descriptor dictionary keys +*/ + +/* + CPFontNameAttribute contains a CPString that specified the font name + (may be an name list like: 'Marker Felt, Lucida Grande, Helvetica') +*/ +CPFontNameAttribute = @"CPFontNameAttribute"; +/* + CPFontSizeAttribute contains a CPString that specified the font size + (as a float value) +*/ +CPFontSizeAttribute = @"CPFontSizeAttribute"; +/* + CPFontTraitsAttribute a CPDictionary that contains font traits keys + (CPFontSymbolicTrait or CPFontWeightTrait) +*/ +CPFontTraitsAttribute = @"CPFontTraitsAttribute"; + +// Font traits dictionary keys +/* + CPFontSymbolicTrait a CPNumber that contains CPFontFamilyClass and + typeface information flags. +*/ +CPFontSymbolicTrait = @"CPFontSymbolicTrait"; + +/* + CPFontWeightTrait + We use CPString with CSS string values for font weight + (normal | bold | bolder | lighter | 100 | 200 | 300 | 400 + | 500 | 600 | 700 | 800 | 900) + NOTE: Cocoa compatibility issue: NSFontWeightTrait are NSNumber for + font weight (from -1.0 to 1.0, 0.0 for normal weight). +*/ +CPFontWeightTrait = @"CPFontWeightTrait"; + +/* + CPFontFamilyClass +*/ +CPFontUnknownClass = (0 << 28); +CPFontOldStyleSerifsClass = (1 << 28); +CPFontTransitionalSerifsClass = (2 << 28); +CPFontModernSerifsClass = (3 << 28); +CPFontClarendonSerifsClass = (4 << 28); +CPFontSlabSerifsClass = (5 << 28); +CPFontFreeformSerifsClass = (7 << 28); +CPFontSansSerifClass = (8 << 28); + +CPFontSerifClass = (CPFontOldStyleSerifsClass | CPFontTransitionalSerifsClass | + CPFontModernSerifsClass | CPFontClarendonSerifsClass | + CPFontSlabSerifsClass | CPFontFreeformSerifsClass); + +CPFontFamilyClassMask = 0xF0000000; + +/* + Typeface information +*/ +CPFontItalicTrait = (1 << 0); +CPFontBoldTrait = (1 << 1); +CPFontExpandedTrait = (1 << 5); /* TODO: CCS 3 font-stretch */ +CPFontCondensedTrait = (1 << 6); + +CPFontSmallCapsTrait = (1 << 7); + +/*! + @ingroup appkit + @class CPFontDescriptor +*/ +@implementation CPFontDescriptor : CPObject +{ + CPDictionary _attributes; +} + +/*! + Returns a font descriptor with the specified attributes. + + @param attributes a dictionary that describe the desired font descriptor + @return the requested font descriptor +*/ ++ (CPFontDescriptor)fontDescriptorWithFontAttributes:(CPDictionary)attributes +{ + return [[CPFontDescriptor alloc] initWithFontAttributes:attributes]; +} + +/*! + Returns a font descriptor with the specified name and size. + + @param fontName the name of the font + @param aSize the size of the font (in points) + @return the requested font descriptor +*/ ++ (CPFontDescriptor)fontDescriptorWithName:(CPString)fontName size:(float)size +{ + return [[CPFontDescriptor alloc] initWithFontAttributes:[CPDictionary dictionaryWithObjects:[fontName, [CPString stringWithString:size + '']] forKeys:[CPFontNameAttribute,CPFontSizeAttribute]]]; +} + +/*! + Initialize a font descriptor with the specified attributes. + + @param attributes a dictionary that describe the desired font descriptor + @return the requested font descriptor +*/ +- (id)initWithFontAttributes:(CPDictionary)attributes +{ + self = [super init]; + + if (self) + { + _attributes = [[CPMutableDictionary alloc] init]; + + if (attributes) + [_attributes addEntriesFromDictionary:attributes]; + } + + return self; +} + +/*! + Returns a new font descriptor that is the same as the receiver but with the + specified attributes taking precedence over the existing ones. + + @param attributes a dictionary that describe the desired font descriptor + @return the new font descriptor +*/ +- (CPFontDescriptor)fontDescriptorByAddingAttributes:(CPDictionary)attributes +{ + var attrib = [_attributes copy]; + [attrib addEntriesFromDictionary:attributes]; + + return [[CPFontDescriptor alloc] initWithFontAttributes:attrib]; +} + +/*! + Returns a new font descriptor that is the same as the receiver but with the specified size taking precedence over the existing ones. + + @param aSize the new size + @return the new font descriptor +*/ +- (CPFontDescriptor)fontDescriptorWithSize:(float)aSize +{ + var attrib = [_attributes copy]; + [attrib setObject:[CPString stringWithString:aSize + ''] forKey:CPFontSizeAttribute]; + + return [[CPFontDescriptor alloc] initWithFontAttributes:attrib]; +} + +/*! + Returns a new font descriptor that is the same as the receiver but with + the specified symbolic traits taking precedence over the existing ones. + + @param symbolicTraits the desired new symbolic traits + @return the new font descriptor +*/ +- (CPFontDescriptor)fontDescriptorWithSymbolicTraits:(CPFontSymbolicTraits)symbolicTraits +{ + var attrib = [_attributes copy]; + + if ([attrib objectForKey:CPFontTraitsAttribute]) + [[attrib objectForKey:CPFontTraitsAttribute] setObject:[CPNumber numberWithUnsignedInt:symbolicTraits] + forKey:CPFontSymbolicTrait]; + else + [attrib setObject:[CPDictionary dictionaryWithObject:[CPNumber numberWithUnsignedInt:symbolicTraits] + forKey:CPFontSymbolicTrait] forKey:CPFontTraitsAttribute]; + + return [[CPFontDescriptor alloc] initWithFontAttributes:attrib]; +} + +- (id)objectForKey:(id)aKey +{ + return [_attributes objectForKey:aKey]; +} + +- (CPDictionary)fontAttributes +{ + return _attributes; +} + +- (float)pointSize +{ + var value = [_attributes objectForKey:CPFontSizeAttribute]; + + if (value) + return [value floatValue]; + + return 0.0; +} + +- (CPFontSymbolicTraits)symbolicTraits +{ + var traits = [_attributes objectForKey:CPFontTraitsAttribute]; + + if (traits && [traits objectForKey:CPFontSymbolicTrait]) + return [[traits objectForKey:CPFontSymbolicTrait] unsignedIntValue]; + + return 0; +} + +@end + +var CPFontDescriptorAttributesKey = @"CPFontDescriptorAttributesKey"; + +@implementation CPFontDescriptor (CPCoding) + +/*! + Initializes the font descriptor from a coder. + + @param aCoder the coder from which to read the font descriptor data + @return the initialized font +*/ +- (id)initWithCoder:(CPCoder)aCoder +{ + return [self initWithFontAttributes:[aCoder decodeObjectForKey:CPFontDescriptorAttributesKey]]; +} + +/*! + Writes the font descriptor to a coder. + + @param aCoder the coder to which the data will be written +*/ +- (void)encodeWithCoder:(CPCoder)aCoder +{ + [aCoder encodeObject:_attributes forKey:CPFontDescriptorAttributesKey]; +} + +@end + +var _wrapNameRegEx = new RegExp(/(\w+\s+\w+)(,*)/g); + +/* + Helper methods to CPFont for generating CSS font style +*/ +@implementation CPFontDescriptor (CPFontCSSHelper) + +- (CPString)fontStyleCSSString +{ + if ([self symbolicTraits] & CPFontItalicTrait) + return @"italic"; + + return @"normal"; +} + +- (CPString)fontWeightCSSString +{ + var traitsAttributes = [_attributes objectForKey:CPFontTraitsAttribute]; + + if (traitsAttributes) + { + /* give preference to CPFontWeightTrait */ + if ([traitsAttributes objectForKey:CPFontWeightTrait]) + return [traitsAttributes objectForKey:CPFontWeightTrait]; + /* else fallback to facetype symbolic traits */ + if ([self symbolicTraits] & CPFontBoldTrait) + return @"bold"; + } + + return @"normal"; +} + +- (CPString)fontSizeCSSString +{ + if ([_attributes objectForKey:CPFontSizeAttribute]) + return [[_attributes objectForKey:CPFontSizeAttribute] intValue] + "px"; + + return @""; +} + +- (CPString)fontFamilyCSSString +{ + var aName = @""; + + if ([_attributes objectForKey:CPFontNameAttribute]) + aName += [_attributes objectForKey:CPFontNameAttribute].replace(_wrapNameRegEx, '"$1"$2'); + + var symbolicTraits = [self symbolicTraits]; + + if (symbolicTraits) + { + if ((symbolicTraits & CPFontFamilyClassMask) & CPFontSansSerifClass) + aName += @", sans-serif"; + else if ((symbolicTraits & CPFontFamilyClassMask) & CPFontSerifClass) + aName += @", serif"; + } + + return aName; +} + +- (CPString)fontVariantCSSString +{ + if ([self symbolicTraits] & CPFontSmallCapsTrait) + return @"small-caps"; + + return @"normal"; +} + +- (CPString)cssString +{ + return [CPString stringWithString:[self fontStyleCSSString] + " " + + [self fontVariantCSSString] + " " + + [self fontWeightCSSString] + " " + + [self fontSizeCSSString] + " " + + [self fontFamilyCSSString]]; +} + +@end diff --git a/AppKit/CPTextView/CPFontPanel.j b/AppKit/CPTextView/CPFontPanel.j new file mode 100755 index 000000000..fac94ed4b --- /dev/null +++ b/AppKit/CPTextView/CPFontPanel.j @@ -0,0 +1,487 @@ +/* + * CPFontPanel.j + * AppKit + * + * TODOs: + * 1. make browser-width for size smaller and fix columns + * 2. sampleview is currently not shown + * 3. add all the missing features from the MacOS X counterpart + * + * + * Created by Daniel Boehringer on 2/JAN/2014. + * All modifications copyright Daniel Boehringer 2013. + * Based on original work by + * Created by Emmanuel Maillard on 06/03/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 "CPFontManager.j" +@import "CPPanel.j" +@import "CPLayoutManager.j" + +/* + Collection indexes +*/ +var kTypefaceIndex_Normal = 0, + kTypefaceIndex_Italic = 1, + kTypefaceIndex_Bold = 2, + kTypefaceIndex_BoldItalic = 3; + +var kToolbarHeight = 32, + kBorderSpacing = 6, + kInnerSpacing = 2; + +var kNothingChanged = 0, + kFontNameChanged = 1, + kTypefaceChanged = 2, + kSizeChanged = 3, + kTextColorChanged = 4, + kBackgroundColorChanged = 5, + kUnderlineChanged = 6, + kWeightChanged = 7; + +var _sharedFontPanel = nil; + + +// FIXME Locale support +var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], + _availableSizes = [@"9", @"10", @"11", @"12", @"13", @"14", @"18", @"24", @"36", @"48", @"72", @"96"]; + +@implementation _CPFontPanelSampleView : CPView +{ + CPLayoutManager _layoutManager; + CPTextStorage _textStorage; + CPTextContainer _textContainer; +} + +- (id)initWithFrame:(CPRect)rect +{ + self = [super initWithFrame:rect]; + + if (self) + { + _textStorage = [[CPTextStorage alloc] init]; + _layoutManager = [[CPLayoutManager alloc] init]; + + _textContainer = [[CPTextContainer alloc] init]; + [_layoutManager addTextContainer:_textContainer]; + + [_textStorage addLayoutManager:_layoutManager]; + } + + return self; +} + +- (void)setAttributedString:(CPAttributedString)aSting +{ + [_textStorage replaceCharactersInRange:CPMakeRange(0, [_textStorage length]) + withAttributedString:aSting]; + + [self setNeedsDisplay:YES]; +} + +- (void)drawRect:(CPRect)rect +{ + var ctx = [[CPGraphicsContext currentContext] graphicsPort], + glyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer], + usedRect = [_layoutManager usedRectForTextContainer:_textContainer], + bounds = [self bounds], + pos = CPMakePoint((bounds.size.width - usedRect.size.width) / 2.0, (bounds.size.height - usedRect.size.height) / 2.0); + + CGContextSaveGState(ctx); + CGContextSetFillColor(ctx, [CPColor whiteColor]); + CGContextFillRect(ctx, bounds); + CGContextRestoreGState(ctx); + + [_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:pos]; +} + +@end + +/*! + @ingroup appkit + @class CPFontPanel +*/ +@implementation CPFontPanel : CPPanel +{ + CPView _toolbarView; + id _fontBrowser; + id _traitBrowser; + id _sizeBrowser; + CPArray _availableFonts; + id _textColorWell; + CPColor _textColor; + int _currentColorButtonTag; + BOOL _setupDone; + int _fontChanges; + + _CPFontPanelSampleView _sampleView; +} + +/*! + Check if the shared Font panel exists. +*/ ++ (BOOL)sharedFontPanelExists +{ + return _sharedFontPanel !== nil; +} + +/*! + Return the shared Font panel. +*/ ++ (CPFontPanel)sharedFontPanel +{ + if (!_sharedFontPanel) + _sharedFontPanel = [[CPFontPanel alloc] init]; + + return _sharedFontPanel; +} + +/*! @ignore */ +- (id)init +{ + self = [super initWithContentRect:CGRectMake(100, 90, 450, 394) styleMask:(CPTitledWindowMask | CPClosableWindowMask /*| CPResizableWindowMask*/ )]; + + if (self) + { + [[self contentView] setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]]; + + [self setTitle:@"Font Panel"]; + [self setLevel:CPFloatingWindowLevel]; + + [self setFloatingPanel:YES]; + [self setBecomesKeyOnlyIfNeeded:YES]; + + [self setMinSize:CGSizeMake(378, 394)]; + + _availableFonts = [[CPFontManager sharedFontManager] availableFonts]; + + _textColor = [CPColor blackColor]; + + _setupDone = NO; + _fontChanges = kNothingChanged; + } + + return self; +} + +/*! @ignore */ +- (void)_setupToolbarView +{ + _toolbarView = [[CPView alloc] initWithFrame:CGRectMake(0, kBorderSpacing, CGRectGetWidth([self frame]), kToolbarHeight)]; + [_toolbarView setAutoresizingMask: CPViewWidthSizable]; + + /* text color */ + _textColorWell = [[CPColorWell alloc] initWithFrame:CGRectMake(10, 0, 25, 25)]; + [_textColorWell setColor:_textColor]; // FIXME: use bindings + [_toolbarView addSubview:_textColorWell]; + var colorPanel = [CPColorPanel sharedColorPanel]; + [colorPanel setTarget:self]; + [colorPanel setAction:@selector(changeColor:)]; +} + +- (void)_setupBrowser: aBrowser +{ + [aBrowser setTarget:self]; + [aBrowser setAction:@selector(browserClicked:)]; + [aBrowser setDoubleAction:@selector(dblClicked:)]; + [aBrowser setAllowsEmptySelection:NO]; + [aBrowser setAllowsMultipleSelection: NO]; + [aBrowser setDelegate:self]; + [[self contentView] addSubview:aBrowser]; +} + +- (void)_setupContents +{ + if (_setupDone) + return; + + _setupDone = YES; + + [self _setupToolbarView]; + + var contentView = [self contentView], + label = [CPTextField labelWithTitle:@"Font name"], + contentBounds = [contentView bounds], + upperView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(contentBounds), CGRectGetHeight(contentBounds) - (kBorderSpacing + kToolbarHeight + kInnerSpacing))]; + + [contentView addSubview:_toolbarView]; + _fontBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(10, 35, 150, 350)]; + _traitBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(155, 35, 150, 350)]; + _sizeBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(300, 35, 140, 350)]; + [self _setupBrowser:_fontBrowser]; + [self _setupBrowser:_traitBrowser]; + [self _setupBrowser:_sizeBrowser]; + [[CPNotificationCenter defaultCenter] addObserver:self + selector:@selector(textViewDidChangeSelection:) + name:CPTextViewDidChangeSelectionNotification + object:nil]; +} + +- (void)textViewDidChangeSelection:(CPNotification)notification +{ + [self _refreshWithTextView:[notification object]]; + +} + +- (void)_refreshWithTextView: textView +{ + if ([self isVisible]) + { + var attribs = [textView typingAttributes], + font = [attribs objectForKey:CPFontAttributeName] || [[textView textStorage] font] || [CPFont systemFontOfSize:12.0]; + + if (font) + { + var trait = kTypefaceIndex_Normal; + + if ([font isItalic] && [font isBold]) + trait = kTypefaceIndex_BoldItalic; + else if ([font isItalic]) + trait = kTypefaceIndex_Italic; + else if ([font isBold]) + trait = kTypefaceIndex_Bold; + + [self setCurrentFont: font]; + [self setCurrentTrait: trait]; + [self setCurrentSize: [font size] + ""]; //cast to string + } + } +} + +- (void)orderFront:(id)sender +{ + [self _setupContents]; + [super orderFront:sender]; + [self _refreshWithTextView: [[CPApp keyWindow] firstResponder]]; +} + +- (void)reloadDefaultFontFamilies +{ + _availableFonts = [[CPFontManager sharedFontManager] availableFonts]; +} + +- (BOOL)worksWhenModal +{ + return YES; +} + +/*! + @param aFont the font to convert. + @return The converted font or \c aFont if failed to convert. +*/ +- (CPFont)panelConvertFont:(CPFont)aFont +{ + var newFont = aFont, + index = 0; + + switch (_fontChanges) + { + case kFontNameChanged: + newFont = [CPFont fontWithDescriptor:[[aFont fontDescriptor] fontDescriptorByAddingAttributes: + [CPDictionary dictionaryWithObject: [self currentFont] forKey:CPFontNameAttribute]] size:0.0]; + break; + + case kTypefaceChanged: + index = [self currentTrait]; + if (index == kTypefaceIndex_BoldItalic) + newFont = [[CPFontManager sharedFontManager] convertFont:aFont toHaveTrait:CPBoldFontMask | CPItalicFontMask]; + else if (index == kTypefaceIndex_Bold) + newFont = [[CPFontManager sharedFontManager] convertFont:aFont toHaveTrait:CPBoldFontMask]; + else if (index == kTypefaceIndex_Italic) + newFont = [[CPFontManager sharedFontManager] convertFont:aFont toHaveTrait:CPItalicFontMask]; + else + newFont = [[CPFontManager sharedFontManager] convertFont:aFont toNotHaveTrait:CPBoldFontMask | CPItalicFontMask]; + break; + + case kSizeChanged: + newFont = [[CPFontManager sharedFontManager] convertFont:aFont toSize:[self currentSize]]; + break; + + case kNothingChanged: + break; + + default: + CPLog.trace(@"FIXME: -[" + [self className] + " " + _cmd + "] unhandled _fontChanges: " + _fontChanges); + break; + } + + return newFont; +} + +- (void)setCurrentSize: aSize +{ + [_sizeBrowser selectRow: [_availableSizes indexOfObject: aSize] inColumn:0]; +} + +- (CPString)currentSize +{ + return [_sizeBrowser selectedItem]; +} + +- (void)setCurrentFont: aFont +{ + [_fontBrowser selectRow: [_availableFonts indexOfObject: [aFont familyName]] inColumn:0]; +} + +- (CPString)currentFont +{ + return [_fontBrowser selectedItem]; +} + +- (void)setCurrentTrait: aTrait +{ + var row = 0; + + switch (aTrait) + { + case kTypefaceIndex_Italic: + row = 1; + break; + + case kTypefaceIndex_Bold: + row = 2; + break; + + case kTypefaceIndex_BoldItalic: + row = 3; + break; + } + + [_traitBrowser selectRow: row inColumn:0]; +} + +// FIXME Locale support +- (void)currentTrait +{ + var sel = [_traitBrowser selectedItem]; + + if (sel === "Italic") + return kTypefaceIndex_Italic; + + if (sel === "Bold") + return kTypefaceIndex_Bold; + + if (sel === "Bold Italic") + return kTypefaceIndex_BoldItalic; + + return kTypefaceIndex_Normal; +} + +/*! + Set the selected font in Font panel. + @param font the selected font + @param flag if \c the current selection have multiple fonts. +*/ +- (void)setPanelFont:(CPFont)font isMultiple:(BOOL)flag +{ + [self _setupContents]; + + if ([self currentFont] !== [font familyName]) + [self setCurrentFont:[font familyName]]; + + if ([self currentSize] != [font size]) + [self setCurrentSize:[font size]]; + + var typefaceIndex = kTypefaceIndex_Normal, + symbolicTraits = [[font fontDescriptor] symbolicTraits]; + + if ((symbolicTraits & CPFontItalicTrait) && (symbolicTraits & CPFontBoldTrait)) + typefaceIndex = kTypefaceIndex_BoldItalic; + else if (symbolicTraits & CPFontItalicTrait) + typefaceIndex = kTypefaceIndex_Italic; + else if (symbolicTraits & CPFontBoldTrait) + typefaceIndex = kTypefaceIndex_Bold; + + if ([self currentTrait] != typefaceIndex) + [self setCurrentTrait: typefaceIndex ]; + + [_sampleView setAttributedString: + [[CPAttributedString alloc] initWithString:[font familyName] + attributes:[CPDictionary dictionaryWithObjects:[font, [CPColor blackColor]] forKeys:[CPFontAttributeName, CPForegroundColorAttributeName]]] + ]; + + _fontChanges = kNothingChanged; +} + +- (void)changeColor:(id)sender +{ + _textColor = [sender color]; + _fontChanges = kTextColorChanged; + [[CPFontManager sharedFontManager] modifyFontViaPanel:self]; +} + +//////////////////////////////////////////////////////////////////// +// TODO: ask CPFontManager for traits // +- (void)browserClicked:(id)aBrowser +{ + if (aBrowser === _fontBrowser) + { + _fontChanges = kFontNameChanged; + [[CPFontManager sharedFontManager] modifyFontViaPanel:self]; + } + else if (aBrowser === _traitBrowser) + { + _fontChanges = kTypefaceChanged; + [[CPFontManager sharedFontManager] modifyFontViaPanel:self]; + } + else if (aBrowser === _sizeBrowser) + { + _fontChanges = kSizeChanged; + [[CPFontManager sharedFontManager] modifyFontViaPanel:self]; + } +} + +- (void)dblClicked:(id)sender +{ + // alert("DOUBLE"); +} + +- (id)browser:(id)aBrowser numberOfChildrenOfItem:(id)anItem +{ + if (aBrowser === _fontBrowser) + return [_availableFonts count]; + + if (aBrowser === _traitBrowser) + return [_availableTraits count] + else + return [_availableSizes count] +} + +- (id)browser:(id)aBrowser child:(int)index ofItem:(id)anItem +{ + if (aBrowser === _fontBrowser) + return [_availableFonts objectAtIndex:index]; + + if (aBrowser === _traitBrowser) + return [_availableTraits objectAtIndex:index]; + + return [_availableSizes objectAtIndex:index]; +} + +- (id)browser:(id)aBrowser objectValueForItem:(id)anItem +{ + return anItem; +} + +- (BOOL)browser:(id)aBrowser isLeafItem:(id)anItem +{ + return YES; +} + +@end diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j new file mode 100755 index 000000000..1e18b58d5 --- /dev/null +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -0,0 +1,1419 @@ +/* + * CPLayoutManager.j + * AppKit + * + * FIXME remove from DOM when scrolled out of visible area? (as done in CPTableView) + * + * + * Created by Daniel Boehringer on 27/12/2013. + * All modifications copyright Daniel Boehringer 2013. + * Based on original work 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 "CPTextStorage.j" +@import "CPTextContainer.j" +@import "CPTypesetter.j" + +function _RectEqualToRectHorizontally(lhsRect, rhsRect) +{ + return (lhsRect.origin.x == rhsRect.origin.x && + lhsRect.size.width == rhsRect.size.width && + lhsRect.size.height == rhsRect.size.height); +} + +_oncontextmenuhandler = function () { return false; }; + + +@implementation CPArray(SortedSearching) + +- (unsigned)indexOfObject:(id)anObject sortedByFunction:(Function)aFunction context:(id)aContext +{ + var result = [self _indexOfObject:anObject sortedByFunction:aFunction context:aContext]; + + return (result >= 0) ? result : CPNotFound; +} + +- (unsigned)_indexOfObject:(id)anObject sortedByFunction:(Function)aFunction context:(id)aContext +{ + var length= [self count]; + + if (!aFunction) + return CPNotFound; + + if (length === 0) + return -1; + + var mid, + c, + first = 0, + last = length - 1; + + while (first <= last) + { + mid = FLOOR((first + last) / 2); + c = aFunction(anObject, self[mid], aContext); + + if (c > 0) + first = mid + 1; + else if (c < 0) + last = mid - 1; + else + { + while (mid < length - 1 && aFunction(anObject, self[mid + 1], aContext) == CPOrderedSame) + mid++; + + return mid; + } + } + + return -first - 1; +} + +@end + +var _sortRange = function(location, anObject) +{ + if (CPLocationInRange(location, anObject._range)) + return CPOrderedSame; + else if (CPMaxRange(anObject._range) <= location) + return CPOrderedDescending; + else + return CPOrderedAscending; +} + +var _objectWithLocationInRange = function(aList, aLocation) +{ + var index = [aList indexOfObject: aLocation sortedByFunction:_sortRange context:nil]; + + if (index != CPNotFound) + return aList[index]; + + return nil; +} + +var _objectsInRange = function(aList, aRange) +{ + var list = [], + c = aList.length, + location = aRange.location; + + for (var i = 0; i < c; i++) + { + if (CPLocationInRange(location, aList[i]._range)) + { + list.push(aList[i]); + if (CPMaxRange(aList[i]._range) <= CPMaxRange(aRange)) + location = CPMaxRange(aList[i]._range); + else + break; + } + else if (CPLocationInRange(CPMaxRange(aRange), aList[i]._range)) + { + list.push(aList[i]); + break; + } + else if (CPRangeInRange(aRange, aList[i]._range)) + { + list.push(aList[i]); + } + } + + return list; +} + +@implementation _CPLineFragment : CPObject +{ + CPRect _fragmentRect; + CPRect _usedRect; + CPPoint _location; + CPRange _range; + CPTextContainer _textContainer; + BOOL _isInvalid; + CPMutableArray _runs; + + /* 'Glyphs' frames */ + CPArray _glyphsFrames; +} + +- (id)createDOMElementWithText:aString andFont:aFont andColor:aColor +{ + var style, + span = document.createElement("span"); + + span.oncontextmenu = span.onmousedown = span.onselectstart = _oncontextmenuhandler; + // span.contentEditable = true; // this unfortunately does not work to make native pasting work on safari + + style = span.style; + style.position = "absolute"; + style.visibility = "visible"; + style.padding = "0px"; + style.margin = "0px"; + style.whiteSpace = "pre"; + style.backgroundColor = "transparent"; + style.font = [aFont cssString]; + + if (aColor) + style.color = [aColor cssString]; + + if (CPFeatureIsCompatible(CPJavaScriptInnerTextFeature)) + span.innerText = aString; + else if (CPFeatureIsCompatible(CPJavaScriptTextContentFeature)) + span.textContent = aString; +// FIXME aString.replace(/&/g,'&') + return span; +} + +- (id)initWithRange:(CPRange)aRange textContainer:(CPTextContainer)aContainer textStorage:(CPTextStorage)textStorage +{ + self = [super init]; + + if (self) + { + _fragmentRect = CGRectMakeZero(); + _usedRect = CGRectMakeZero(); + _location = CPPointMakeZero(); + _range = CPMakeRangeCopy(aRange); + _textContainer = aContainer; + _isInvalid = NO; + + _runs = [[CPMutableArray alloc] init]; + var effectiveRange = CPMakeRange(0,0), + location; + + for (location = aRange.location; location < CPMaxRange(aRange); location = CPMaxRange(effectiveRange)) + { + var attributes = [textStorage attributesAtIndex:location effectiveRange:effectiveRange]; + effectiveRange = attributes ? CPIntersectionRange(aRange, effectiveRange) : aRange; + + var string = [textStorage._string substringWithRange:effectiveRange], + font = [textStorage font] || [CPFont systemFontOfSize:12.0]; + + if ([attributes containsKey:CPFontAttributeName]) + font = [attributes objectForKey:CPFontAttributeName]; + + var color = [attributes objectForKey:CPForegroundColorAttributeName], + elem = [self createDOMElementWithText:string andFont:font andColor:color], + run = {_range:CPMakeRangeCopy(effectiveRange), elem:elem, string:string}; + + _runs.push(run); + } + } + + return self; +} + +- (void)setAdvancements:someAdvancements +{ + _glyphsFrames = []; + + var count = someAdvancements.length, + origin = CPPointMake(_fragmentRect.origin.x + _location.x, _fragmentRect.origin.y); // FIXME _location.y + + for (var i = 0; i < count; i++) + { + _glyphsFrames.push(CPRectMake(origin.x, origin.y, someAdvancements[i], _usedRect.size.height)); + origin.x += someAdvancements[i]; + } +} + +- (CPString)description +{ + return [super description] + + "\n\t_fragmentRect="+CPStringFromRect(_fragmentRect) + + "\n\t_usedRect="+CPStringFromRect(_usedRect) + + "\n\t_location="+CPStringFromPoint(_location) + + "\n\t_range="+CPStringFromRange(_range); +} + +- (CPArray)glyphFrames +{ + return _glyphsFrames; +} + +- (void)drawUnderlineForGlyphRange:(CPRange)glyphRange + underlineType:(int)underlineVal + baselineOffset:(float)baselineOffset + containerOrigin:(CPPoint)containerOrigin +{ +// FIXME +} + +- (void)invalidate +{ + _isInvalid = YES; +} + +- (void)_deinvalidate +{ + _isInvalid = NO; +} + +- (void)_removeFromDOM +{ + var i, + l = _runs.length; + + for (var i = 0; i < l; i++) + { + if (_runs[i].elem && _runs[i].DOMactive) + _textContainer._textView._DOMElement.removeChild(_runs[i].elem); + + _runs[i].elem = nil; + _runs[i].DOMactive = NO; + } +} + +- (void)drawInContext:(CGContext)context atPoint:(CPPoint)aPoint forRange:(CPRange)aRange +{ + var runs = _objectsInRange(_runs, aRange), + c = runs.length, + orig = CPPointMake(_location.x, _location.y + _fragmentRect.origin.y); + + orig.y += aPoint.y; + + for (var i = 0; i < c; i++) + { + var run = runs[i]; + + if (run.DOMactive && !run.DOMpatched) + { + continue; + } + + orig.x = _glyphsFrames[run._range.location - _runs[0]._range.location].origin.x + aPoint.x; + + run.elem.style.left = (orig.x) + "px"; + run.elem.style.top = (orig.y - _usedRect.size.height + 4) + "px"; // FIXME: consolidate this strange constant + + if (!run.DOMactive) + _textContainer._textView._DOMElement.appendChild(run.elem); + + run.DOMactive = YES; + run.DOMpatched = NO; + + if (run.underline) + { + // FIXME + } + } +} + +- (void)backgroundColorForGlyphAtIndex:(unsigned)index +{ + var run = _objectWithLocationInRange(_runs, index); + + if (run) + return run.backgroundColor; + + return [CPColor clearColor]; +} + +- (BOOL)isVisuallyIdenticalToFragment:(_CPLineFragment)newLineFragment +{ + var newFragmentRuns= newLineFragment._runs, + oldFragmentRuns= _runs; + + if (!oldFragmentRuns || !newFragmentRuns || oldFragmentRuns.length !== newFragmentRuns.length) + return NO; + + var l = oldFragmentRuns.length; + + for (var i = 0; i < l; i++) + { + if (newFragmentRuns[i].string !== oldFragmentRuns[i].string || + !_RectEqualToRectHorizontally(newLineFragment._fragmentRect, _fragmentRect)) + // FIXME newFragmentRuns[i].elem.style.left !== oldFragmentRuns[i].elem.style.left && compare CSS-strings + { + return NO; + } + } + + return YES; +} + +- (void)_relocateVerticallyByY:(double) verticalOffset rangeOffset:(unsigned) rangeOffset +{ + _range.location += rangeOffset; + var l = _runs.length; + + for (var i = 0; i < l; i++) + { + _runs[i]._range.location += rangeOffset; + + if (verticalOffset) + { + _runs[i].elem.top = (_runs[i].elem.top + verticalOffset) + 'px'; + _runs[i].DOMpatched = YES; + } + } + + if (!verticalOffset) + return NO; + + _fragmentRect.origin.y += verticalOffset; + _usedRect.origin.y += verticalOffset; + + var l = _glyphsFrames.length; + + for (var i = 0; i < l ; i++) + { + _glyphsFrames[i].origin.y += verticalOffset; + } +} + +@end + +@implementation _CPTemporaryAttributes : CPObject +{ + CPDictionary _attributes; + CPRange _range; +} + +- (id)initWithRange:(CPRange)aRange attributes:(CPDictionary)attributes +{ + self = [super init]; + + if (self) + { + _attributes = attributes; + _range = CPMakeRangeCopy(aRange); + } + + return self; +} + +- (CPString)description +{ + return [super description] + + "\n\t_range="+CPStringFromRange(_range) + + "\n\t_attributes="+[_attributes description]; +} + +@end + +/*! + @ingroup appkit + @class CPLayoutManager +*/ +@implementation CPLayoutManager : CPObject +{ + CPTextStorage _textStorage; + id _delegate; + CPMutableArray _textContainers; + CPTypesetter _typesetter; + + CPMutableArray _lineFragments; + CPMutableArray _lineFragmentsForRescue; + id _extraLineFragment; + Class _lineFragmentFactory; + + CPMutableArray _temporaryAttributes; + + BOOL _isValidatingLayoutAndGlyphs; + var _removeInvalidLineFragmentsRange; +} + +- (id)init +{ + self = [super init]; + + if (self) + { + _textContainers = [[CPMutableArray alloc] init]; + _lineFragments = [[CPMutableArray alloc] init]; + _typesetter = [CPTypesetter sharedSystemTypesetter]; + _isValidatingLayoutAndGlyphs = NO; + _lineFragmentFactory = [_CPLineFragment class]; + } + + return self; +} + +- (void)setTextStorage:(CPTextStorage)textStorage +{ + if (_textStorage === textStorage) + return; + + _textStorage = textStorage; +} + +- (CPTextStorage)textStorage +{ + return _textStorage; +} + +- (void)insertTextContainer:(CPTextContainer)aContainer atIndex:(int)index +{ + [_textContainers insertObject:aContainer atIndex:index]; + [aContainer setLayoutManager:self]; +} + +- (void)addTextContainer:(CPTextContainer)aContainer +{ + [_textContainers addObject:aContainer]; + [aContainer setLayoutManager:self]; +} + +- (void)removeTextContainerAtIndex:(int)index +{ + var container = [_textContainers objectAtIndex:index]; + [container setLayoutManager:nil]; + [_textContainers removeObjectAtIndex:index]; +} + +- (CPArray)textContainers +{ + return _textContainers; +} + +// fixme +- (int)numberOfGlyphs +{ + return [_textStorage length]; +} +- (int)numberOfCharacters +{ + return [_textStorage length]; +} + +- (CPTextView)firstTextView +{ + return [_textContainers[0] textView]; +} + +// from cocoa (?) +- (CPTextView)textViewForBeginningOfSelection +{ + return [[_textContainers objectAtIndex:0] textView]; +} + +- (BOOL)layoutManagerOwnsFirstResponderInWindow:(CPWindow)aWindow +{ + var firstResponder = [aWindow firstResponder], + c = [_textContainers count]; + + for (var i = 0; i < c; i++) + { + if ([_textContainers[i] textView] === firstResponder) + return YES; + } + + return NO; +} + +- (CPRect)boundingRectForGlyphRange:(CPRange)aRange inTextContainer:(CPTextContainer)container +{ + if (![self numberOfGlyphs]) + return CPRectMake(0, 0, 1, 12); // crude hack to give a cursor in an empty doc. + + if (CPMaxRange(aRange) >= [self numberOfGlyphs]) + aRange = CPMakeRange([self numberOfGlyphs] - 1, 1); + + var fragments = _objectsInRange(_lineFragments, aRange), + rect = nil, + c = [fragments count]; + + for (var i = 0; i < c; i++) + { + var fragment = fragments[i]; + if (fragment._textContainer === container) + { + var frames = [fragment glyphFrames], + l = frames.length; + + for (var j = 0; j < l; j++) + { + if (CPLocationInRange(fragment._range.location + j, aRange)) + { + if (!rect) + rect = CPRectCreateCopy(frames[j]); + else + rect = CPRectUnion(rect, frames[j]); + } + } + } + } + return (rect) ? rect : CGRectMakeZero(); +} + +- (CPRange)glyphRangeForTextContainer:(CPTextContainer)aTextContainer +{ + var range = nil, + c = [_lineFragments count]; + for (var i = 0; i < c; i++) + { + var fragment = _lineFragments[i]; + if (fragment._textContainer === aTextContainer) + { + if (!range) + range = CPMakeRangeCopy(fragment._range); + else + range = CPUnionRange(range, fragment._range); + } + } + return (range)?range:CPMakeRange(CPNotFound, 0); +} + +- (void)_removeInvalidLineFragments +{ + _lineFragmentsForRescue = [_lineFragments copy]; + [_lineFragmentsForRescue makeObjectsPerformSelector:@selector(_deinvalidate)]; + + if (_removeInvalidLineFragmentsRange && _removeInvalidLineFragmentsRange.length && _lineFragments.length) + { + [[_lineFragments subarrayWithRange:_removeInvalidLineFragmentsRange] makeObjectsPerformSelector:@selector(invalidate)]; + [_lineFragments removeObjectsInRange:_removeInvalidLineFragmentsRange]; + [[_lineFragmentsForRescue subarrayWithRange:_removeInvalidLineFragmentsRange] makeObjectsPerformSelector:@selector(invalidate)]; + } + +} + +- (void)_cleanUpDOM +{ + var l = _lineFragmentsForRescue.length; + + for (var i = 0; i < l; i++) + { + if (_lineFragmentsForRescue[i]._isInvalid) + [_lineFragmentsForRescue[i] _removeFromDOM]; + } +} + +- (void)_validateLayoutAndGlyphs +{ + if (_isValidatingLayoutAndGlyphs) + return; + + _isValidatingLayoutAndGlyphs = YES; + + var startIndex = CPNotFound, + removeRange = CPMakeRange(0,0); + + var l = _lineFragments.length; + if (l) + { + for (var i = 0; i < l; i++) + { + if (_lineFragments[i]._isInvalid) + { + startIndex = _lineFragments[i]._range.location; + removeRange.location = i; + removeRange.length = l - i; + break; + } + } + + if (startIndex == CPNotFound && CPMaxRange (_lineFragments[l - 1]._range) < [_textStorage length]) + startIndex = CPMaxRange(_lineFragments[l - 1]._range); // start one line above current line to make sure that a word can jump up + } + else + startIndex = 0; + + /* nothing to validate and layout */ + if (startIndex == CPNotFound) + { + _isValidatingLayoutAndGlyphs = NO; + return; + } + + if (removeRange.length) + _removeInvalidLineFragmentsRange = CPMakeRangeCopy(removeRange); + + if (!startIndex) // We erased all lines + [self setExtraLineFragmentRect:CPRectMake(0,0) usedRect:CPRectMake(0,0) textContainer:nil]; + + // document.title=startIndex; + [_typesetter layoutGlyphsInLayoutManager:self startingAtGlyphIndex:startIndex maxNumberOfLineFragments:-1 nextGlyphIndex:nil]; + [self _cleanUpDOM]; + _isValidatingLayoutAndGlyphs = NO; +} + +- (BOOL)_rescuingInvalidFragmentsWasPossibleForGlyphRange:(CPRange)aRange +{ + var l = _lineFragments.length, + location = aRange.location, + found = NO; + + // try to find the first linefragment of the desired range + for (var i = 0; i < l; i++) + { + if (CPLocationInRange(location, _lineFragments[i]._range)) + { found = YES; + break; + } + } + + if (!found) + return NO; + + if (!_lineFragmentsForRescue[i]) + return NO; + + var startLineForDOMRemoval = i, + isIdentical = YES, + newLineFragment= _lineFragments[i], + oldLineFragment = _lineFragmentsForRescue[i], + oldLength = CPMaxRange([_lineFragmentsForRescue lastObject]._range), + newLength = [[_textStorage string].length]; + + if (![oldLineFragment isVisuallyIdenticalToFragment: newLineFragment]) + { + isIdentical = NO; + if (newLength < oldLength && oldLineFragment._range.length == 1 && newLineFragment._range.length > 1 && newLineFragment._range.location === oldLineFragment._range.location) // deleting newline in its own line-> move up instead of re.layouting + { + isIdentical = YES; + i--; + startLineForDOMRemoval--; + } + if (newLength > oldLength && newLineFragment._range.length == 1 && oldLineFragment._range.length > 1 && newLineFragment._range.location === oldLineFragment._range.location) // newline entered in its own line-> move down instead of re.layouting + { + isIdentical = YES; + startLineForDOMRemoval--; + } + } + + if (isIdentical) // patch the linefragments instead of re-layoutung + { + var rangeOffset = CPMaxRange(_lineFragments[i]._range) - CPMaxRange(_lineFragmentsForRescue[startLineForDOMRemoval]._range); + + if (!rangeOffset) + return NO; + + var verticalOffset = _lineFragments[i]._usedRect.origin.y - _lineFragmentsForRescue[startLineForDOMRemoval]._usedRect.origin.y, + l = _lineFragmentsForRescue.length; + + for (var i = startLineForDOMRemoval + 1; i < l; i++) + { + _lineFragmentsForRescue[i]._isInvalid = NO; // protect them from final removal + [_lineFragmentsForRescue[i] _relocateVerticallyByY:verticalOffset rangeOffset:rangeOffset]; + _lineFragments.push(_lineFragmentsForRescue[i]); + } + } + + return isIdentical; +} + +- (void)invalidateDisplayForGlyphRange:(CPRange)range +{ + var lineFragments = _objectsInRange(_lineFragments, range); + + for (var i = 0; i < lineFragments.length; i++) + [[lineFragments[i]._textContainer textView] setNeedsDisplayInRect: lineFragments[i]._fragmentRect]; +} + +- (void)invalidateLayoutForCharacterRange:(CPRange)aRange isSoft:(BOOL)flag actualCharacterRange:(CPRangePointer)actualCharRange +{ + var firstFragmentIndex = _lineFragments.length? [_lineFragments indexOfObject: aRange.location sortedByFunction:_sortRange context:nil]:CPNotFound; + + if (firstFragmentIndex == CPNotFound) + { + if (_lineFragments.length) + firstFragmentIndex = _lineFragments.length - 1; + else + { + if (actualCharRange) + { + actualCharRange.length = aRange.length; + actualCharRange.location = 0; + } + + return; + } + } + else + firstFragmentIndex = firstFragmentIndex + (firstFragmentIndex ? - 1 : 0); + + var fragment = _lineFragments[firstFragmentIndex], + range = CPMakeRangeCopy(fragment._range); + + fragment._isInvalid = YES; + + /* invalidated all fragments that follow */ + for (var i = firstFragmentIndex + 1; i < _lineFragments.length; i++) + { + _lineFragments[i]._isInvalid = YES; + range = CPUnionRange(range, _lineFragments[i]._range); + } + + if (CPMaxRange(range) < CPMaxRange(aRange)) + range = CPUnionRange(range, aRange); + + if (actualCharRange) + { actualCharRange.length = range.length; + actualCharRange.location = range.location; + } +} + +- (void)textStorage:(CPTextStorage)textStorage edited:(unsigned)mask range:(CPRange)charRange changeInLength:(int)delta invalidatedRange:(CPRange)invalidatedRange +{ + var actualRange = CPMakeRange(CPNotFound,0); + [self invalidateLayoutForCharacterRange: invalidatedRange isSoft:NO actualCharacterRange:actualRange]; + [self invalidateDisplayForGlyphRange: actualRange]; +} + +- (CPRange)glyphRangeForBoundingRect:(CPRect)aRect inTextContainer:(CPTextContainer)container +{ + var range = nil, + i, + c = [_lineFragments count]; + + for (i = 0; i < c; i++) + { + var fragment = _lineFragments[i]; + + if (fragment._textContainer === container) + { + if (CPRectContainsRect(aRect, fragment._usedRect)) + { + if (!range) + range = CPMakeRangeCopy(fragment._range); + else + range = CPUnionRange(range, fragment._range); + } + else + { + var glyphRange = CPMakeRange(CPNotFound, 0), + frames = [fragment glyphFrames]; + + for (var j = 0; j < frames.length; j++) + { + if (CPRectIntersectsRect(aRect, frames[j])) + { + if (glyphRange.location == CPNotFound) + glyphRange.location = fragment._range.location + j; + else + glyphRange.length++; + } + } + if (glyphRange.location != CPNotFound) + { + if (!range) + range = CPMakeRangeCopy(glyphRange); + else + range = CPUnionRange(range, glyphRange); + } + } + } + } + return (range)?range:CPMakeRange(0,0); +} + +- (void)drawBackgroundForGlyphRange:(CPRange)aRange atPoint:(CPPoint)aPoint +{ +} + +- (void)drawUnderlineForGlyphRange:(CPRange)glyphRange + underlineType:(int)underlineVal + baselineOffset:(float)baselineOffset + lineFragmentRect:(CGRect)lineFragmentRect + lineFragmentGlyphRange:(CPRange)lineGlyphRange + containerOrigin:(CPPoint)containerOrigin +{ +// FIXME +} + +- (void)drawGlyphsForGlyphRange:(CPRange)aRange atPoint:(CPPoint)aPoint +{ + var lineFragments = _objectsInRange(_lineFragments, aRange); + + if (!lineFragments.length) + return; + + var ctx = nil, + paintedRange = CPMakeRangeCopy(aRange), + lineFragmentIndex, + l= lineFragments.length; + + for (lineFragmentIndex = 0; lineFragmentIndex < l; lineFragmentIndex++) + { + var currentFragment = lineFragments[lineFragmentIndex]; + [currentFragment drawInContext:ctx atPoint:aPoint forRange:paintedRange]; + } +} + +- (unsigned)glyphIndexForPoint:(CPPoint)point inTextContainer:(CPTextContainer)container fractionOfDistanceThroughGlyph:(FloatArray)partialFraction +{ + var c = [_lineFragments count]; + for (var i = 0; i < c; i++) + { + var fragment = _lineFragments[i]; + if (fragment._textContainer === container) + { + var frames = [fragment glyphFrames]; + var len = fragment._range.length; + for (var j = 0; j < len; j++) + { + if (CPRectContainsPoint(frames[j], point)) + { + if (partialFraction) + partialFraction[0] = (point.x - frames[j].origin.x) / frames[j].size.width; + + return fragment._range.location + j; + } + } + } + } + // not found, maybe a point left to the last character was clicked->search again with broader constraints + if ([[_textStorage string] length]) + { + for (var i = 0; i < c; i++) + { + var fragment = _lineFragments[i]; + + if (fragment._textContainer === container) + { + if (fragment._range.length > 0 && point.y > fragment._fragmentRect.origin.y && + point.y <= fragment._fragmentRect.origin.y + fragment._fragmentRect.size.height) + { + var nlLoc = CPMaxRange(fragment._range) - 1, + lastFrame = [fragment glyphFrames][fragment._range.length-1], + firstFrame = [fragment glyphFrames][0]; + + // skip tabs and move on the last fragment in this line + if (i < c - 1 && _lineFragments[i + 1]._fragmentRect.origin.y === fragment._fragmentRect.origin.y) + continue; + // this allows clicking before and after the (invisible) return character + if (point.x > CPRectGetMaxX(lastFrame) && fragment.length > 0 && + [[_textStorage string] characterAtIndex: nlLoc] === '\n' || i === c - 1) + return nlLoc + 1; + else if (point.x <= CPRectGetMinX(firstFrame)) + return fragment._range.location; + else + return nlLoc; + } + } + } + } + return CPNotFound; +} + +- (unsigned)glyphIndexForPoint:(CPPoint)point inTextContainer:(CPTextContainer)container +{ + return [self glyphIndexForPoint:point inTextContainer:container fractionOfDistanceThroughGlyph:nil]; +} + +- (void)_setAttributes:(CPDictionary)attributes toTemporaryAttributes:(_CPTemporaryAttributes)tempAttributes +{ + tempAttributes._attributes = attributes; +} + +- (void)_addAttributes:(CPDictionary)attributes toTemporaryAttributes:(_CPTemporaryAttributes)tempAttributes +{ + [tempAttributes._attributes addEntriesFromDictionary:attributes]; +} + +// i did not touch this monster (yet) +- (void)_handleTemporaryAttributes:(CPDictionary)attributes forCharacterRange:(CPRange)charRange withSelector:(SEL)attributesOperation +{ + if (!_temporaryAttributes) + _temporaryAttributes = [[CPMutableArray alloc] init]; + + var location = charRange.location, + length = 0, + dirtyRange = nil; + + while (length != charRange.length) + { + var tempAttributesIndex = [_temporaryAttributes indexOfObject: location sortedByFunction:_sortRange context:nil]; + + if (tempAttributesIndex != CPNotFound) + { + var tempAttributes = _temporaryAttributes[tempAttributesIndex]; + + if (CPRangeInRange(charRange, tempAttributes._range)) + { + [self performSelector:attributesOperation withObject:attributes withObject:tempAttributes]; + dirtyRange = (dirtyRange)?CPUnionRange(dirtyRange, tempAttributes._range):CPMakeRangeCopy(tempAttributes._range); + location += tempAttributes._range.length; + length += tempAttributes._range.length; + } + else if (location == tempAttributes._range.location && CPMaxRange(tempAttributes._range) > CPMaxRange(charRange)) + { + var maxRange = CPMaxRange(charRange), + splittedAttribute = [[_CPTemporaryAttributes alloc] initWithRange:CPMakeRange(maxRange, CPMaxRange(tempAttributes._range) - maxRange) + attributes:[tempAttributes._attributes copy]]; + + if ([_temporaryAttributes count] == tempAttributesIndex + 1) + [_temporaryAttributes addObject:splittedAttribute]; + else + [_temporaryAttributes insertObject:splittedAttribute atIndex:tempAttributesIndex + 1]; + + tempAttributes._range = CPMakeRange(tempAttributes._range.location, maxRange - tempAttributes._range.location); + [self performSelector:attributesOperation withObject:attributes withObject:tempAttributes]; + + location += tempAttributes._range.length; + length += tempAttributes._range.length; + + dirtyRange = (dirtyRange)?CPUnionRange(dirtyRange, tempAttributes._range):CPMakeRangeCopy(tempAttributes._range); + dirtyRange = CPUnionRange(dirtyRange, splittedAttribute._range); + } + else + { + var splittedAttribute = [[_CPTemporaryAttributes alloc] initWithRange:CPMakeRange(location, CPMaxRange(tempAttributes._range) - location) + attributes:[tempAttributes._attributes copy]]; + + if ([_temporaryAttributes count] == tempAttributesIndex + 1) + [_temporaryAttributes addObject:splittedAttribute]; + else + [_temporaryAttributes insertObject:splittedAttribute atIndex:tempAttributesIndex + 1]; + + tempAttributes._range = CPMakeRange(tempAttributes._range.location, location - tempAttributes._range.location); + dirtyRange = (dirtyRange)?CPUnionRange(dirtyRange, tempAttributes._range):CPMakeRangeCopy(tempAttributes._range); + dirtyRange = CPUnionRange(dirtyRange, splittedAttribute._range); + + if (splittedAttribute._range.length <= charRange.length) + { + location += splittedAttribute._range.length; + length += splittedAttribute._range.length; + } + else + { + var nextLocation = location + charRange.length, + nextAttribute = [[_CPTemporaryAttributes alloc] initWithRange:CPMakeRange(nextLocation, CPMaxRange(splittedAttribute._range) - nextLocation) + attributes:[tempAttributes._attributes copy]]; + + splittedAttribute._range = CPMakeRange(splittedAttribute._range.location, nextLocation - splittedAttribute._range.location); + + var insertIndex = [_temporaryAttributes indexOfObject:splittedAttribute]; + + if ([_temporaryAttributes count] == insertIndex + 1) + [_temporaryAttributes addObject:nextAttribute]; + else + [_temporaryAttributes insertObject:nextAttribute atIndex:insertIndex + 1]; + + length = charRange.length; + } + [self performSelector:attributesOperation withObject:attributes withObject:splittedAttribute]; + } + } + else + { + [_temporaryAttributes addObject:[[_CPTemporaryAttributes alloc] initWithRange:charRange attributes:attributes]]; + dirtyRange = CPMakeRangeCopy(charRange); + break; + } + } + + if (dirtyRange) + [self invalidateDisplayForGlyphRange:dirtyRange]; +} + +- (void)setTemporaryAttributes:(CPDictionary)attributes forCharacterRange:(CPRange)charRange +{ + [self _handleTemporaryAttributes:attributes forCharacterRange:charRange withSelector:@selector(_setAttributes:toTemporaryAttributes:)]; +} + +- (void)addTemporaryAttributes:(CPDictionary)attributes forCharacterRange:(CPRange)charRange +{ + [self _handleTemporaryAttributes:attributes forCharacterRange:charRange withSelector:@selector(_addAttributes:toTemporaryAttributes:)]; +} + +// i did not touch this monster (yet) +- (void)removeTemporaryAttribute:(CPString)attributeName forCharacterRange:(CPRange)charRange +{ + if (!_temporaryAttributes) + return; + + var location = charRange.location, + length = 0, + dirtyRange = nil; + while (length != charRange.length) + { + var tempAttributesIndex = [_temporaryAttributes indexOfObject: location sortedByFunction:_sortRange context:nil]; + + if (tempAttributesIndex != CPNotFound) + { + var tempAttributes = _temporaryAttributes[tempAttributesIndex]; + + if (CPRangeInRange(charRange, tempAttributes._range)) + { + location += tempAttributes._range.length; + length += tempAttributes._range.length; + dirtyRange = (dirtyRange)?CPUnionRange(dirtyRange, tempAttributes._range):CPMakeRangeCopy(tempAttributes._range); + + [tempAttributes._attributes removeObjectForKey:attributeName]; + + if ([[tempAttributes._attributes allKeys] count] == 0) + [_temporaryAttributes removeObjectAtIndex:tempAttributesIndex]; + } + else if (location == tempAttributes._range.location && CPMaxRange(tempAttributes._range) > CPMaxRange(charRange)) + { + var maxRange = CPMaxRange(charRange), + splittedAttribute = [[_CPTemporaryAttributes alloc] initWithRange:CPMakeRange(maxRange, CPMaxRange(tempAttributes._range) - maxRange) + attributes:[tempAttributes._attributes copy]]; + + if ([_temporaryAttributes count] == tempAttributesIndex + 1) + [_temporaryAttributes addObject:splittedAttribute]; + else + [_temporaryAttributes insertObject:splittedAttribute atIndex:tempAttributesIndex + 1]; + + tempAttributes._range = CPMakeRange(tempAttributes._range.location, maxRange - tempAttributes._range.location); + location += tempAttributes._range.length; + length += tempAttributes._range.length; + + [tempAttributes._attributes removeObjectForKey:attributeName]; + if ([[tempAttributes._attributes allKeys] count] == 0) + [_temporaryAttributes removeObjectAtIndex:tempAttributesIndex]; + + dirtyRange = (dirtyRange)?CPUnionRange(dirtyRange, tempAttributes._range):CPMakeRangeCopy(tempAttributes._range); + dirtyRange = CPUnionRange(dirtyRange, splittedAttribute._range); + } + else + { + var splittedAttribute = [[_CPTemporaryAttributes alloc] initWithRange:CPMakeRange(location, CPMaxRange(tempAttributes._range) - location) + attributes:[tempAttributes._attributes copy]]; + + if ([_temporaryAttributes count] == tempAttributesIndex + 1) + [_temporaryAttributes addObject:splittedAttribute]; + else + [_temporaryAttributes insertObject:splittedAttribute atIndex:tempAttributesIndex + 1]; + + tempAttributes._range = CPMakeRange(tempAttributes._range.location, location - tempAttributes._range.location); + + dirtyRange = (dirtyRange)?CPUnionRange(dirtyRange, tempAttributes._range):CPMakeRangeCopy(tempAttributes._range); + dirtyRange = CPUnionRange(dirtyRange, splittedAttribute._range); + + if (splittedAttribute._range.length < charRange.length) + { + location += splittedAttribute._range.length; + length += splittedAttribute._range.length; + } + else + { + var nextLocation = location + charRange.length, + nextAttribute = [[_CPTemporaryAttributes alloc] initWithRange:CPMakeRange(nextLocation, CPMaxRange(splittedAttribute._range) - nextLocation) + attributes:[tempAttributes._attributes copy]]; + + splittedAttribute._range = CPMakeRange(splittedAttribute._range.location, nextLocation - splittedAttribute._range.location); + var insertIndex = [_temporaryAttributes indexOfObject:splittedAttribute]; + + if ([_temporaryAttributes count] == insertIndex + 1) + [_temporaryAttributes addObject:nextAttribute]; + else + [_temporaryAttributes insertObject:nextAttribute atIndex:insertIndex + 1]; + + length = charRange.length; + } + + [splittedAttribute._attributes removeObjectForKey:attributeName]; + if ([[splittedAttribute._attributes allKeys] count] == 0) + [_temporaryAttributes removeObject:splittedAttribute]; + } + } + else + break; + } + + if (dirtyRange) + [self invalidateDisplayForGlyphRange:dirtyRange]; + +} + +- (CPDictionary)temporaryAttributesAtCharacterIndex:(unsigned)index effectiveRange:(CPRangePointer)effectiveRange +{ + var tempAttribute = _objectWithLocationInRange(_runs, index); // _runs is wild guess + + if (!tempAttribute) + return nil; + + if (effectiveRange) + { + effectiveRange.location = tempAttribute._range.location; + effectiveRange.length = tempAttribute._range.length; + } + + return tempAttribute._attributes; +} + +- (void)textContainerChangedTextView:(CPTextContainer)aContainer +{ + /* FIXME: stub */ +} + +- (CPTypesetter)typesetter +{ + return _typesetter; +} + +- (void)setTypesetter:(CPTypesetter)aTypesetter +{ + _typesetter = aTypesetter; +} + +- (void)setTextContainer:(CPTextContainer)aTextContainer forGlyphRange:(CPRange)glyphRange +{ + var fragments = _objectsInRange(_lineFragments, glyphRange), + l = fragments.length; + + for (var i = 0; i < l; i++) + { + [fragments[i] invalidate]; + } + + var lineFragment = [[_lineFragmentFactory alloc] initWithRange:glyphRange textContainer:aTextContainer textStorage:_textStorage]; + _lineFragments.push(lineFragment); +} + +- (id) _lineFragmentForLocation:(unsigned) aLoc +{ + var fragments = _objectsInRange(_lineFragments, CPMakeRange(aLoc,0)), + l = fragments.length; + + if (l > 0) + return fragments[0]; + + return nil; +} +- (void)setLineFragmentRect:(CPRect)fragmentRect forGlyphRange:(CPRange)glyphRange usedRect:(CPRect)usedRect +{ + var lineFragment = _objectWithLocationInRange(_lineFragments, glyphRange.location); + + if (lineFragment) + { + lineFragment._fragmentRect = CPRectCreateCopy(fragmentRect); + lineFragment._usedRect = CPRectCreateCopy(usedRect); + } +} + +- (void) _setAdvancements:(CPArray)someAdvancements forGlyphRange:(CPRange)glyphRange +{ + var lineFragment = _objectWithLocationInRange(_lineFragments, glyphRange.location); + + if (lineFragment) + [lineFragment setAdvancements: someAdvancements]; +} + +- (void)setLocation:(CPPoint)aPoint forStartOfGlyphRange:(CPRange)glyphRange +{ + var lineFragment = _objectWithLocationInRange(_lineFragments, glyphRange.location); + if (lineFragment) + lineFragment._location = CPPointCreateCopy(aPoint); +} + +- (CPRect)extraLineFragmentRect +{ + if (_extraLineFragment) + return CPRectCreateCopy(_extraLineFragment._fragmentRect); + + return CGRectMakeZero(); +} + +- (CPTextContainer)extraLineFragmentTextContainer +{ + if (_extraLineFragment) + return _extraLineFragment._textContainer; + + return nil; +} + +- (CPRect)extraLineFragmentUsedRect +{ + if (_extraLineFragment) + return CPRectCreateCopy(_extraLineFragment._usedRect); + + return CGRectMakeZero(); +} + +- (void)setExtraLineFragmentRect:(CPRect)rect usedRect:(CPRect)usedRect textContainer:(CPTextContainer)textContainer +{ + if (textContainer) + { + _extraLineFragment = {}; + _extraLineFragment._fragmentRect = CPRectCreateCopy(rect); + _extraLineFragment._usedRect = CPRectCreateCopy(usedRect); + _extraLineFragment._textContainer = textContainer; + } + else + _extraLineFragment = nil; +} + +/*! + NOTE: will not validate glyphs and layout +*/ +- (CPRect)usedRectForTextContainer:(CPTextContainer)textContainer +{ + var rect = nil; + + for (var i = 0; i < _lineFragments.length; i++) + { + if (_lineFragments[i]._textContainer === textContainer) + { + if (rect) + rect = CPRectUnion(rect, _lineFragments[i]._usedRect); + else + rect = CPRectCreateCopy(_lineFragments[i]._usedRect); + } + } + + return (rect)?rect:CGRectMakeZero(); +} + +- (CPRect)lineFragmentRectForGlyphAtIndex:(unsigned)glyphIndex effectiveRange:(CPRangePointer)effectiveGlyphRange +{ + var lineFragment = _objectWithLocationInRange(_lineFragments, glyphIndex); + + if (!lineFragment) + return CGRectMakeZero(); + + if (effectiveGlyphRange) + { + effectiveGlyphRange.location = lineFragment._range.location; + effectiveGlyphRange.length = lineFragment._range.length; + } + + return CPRectCreateCopy(lineFragment._fragmentRect); +} + +- (CPRect)lineFragmentUsedRectForGlyphAtIndex:(unsigned)glyphIndex effectiveRange:(CPRangePointer)effectiveGlyphRange +{ + var lineFragment = _objectWithLocationInRange(_lineFragments, glyphIndex); + + if (!lineFragment) + return CGRectMakeZero(); + + if (effectiveGlyphRange) + { + effectiveGlyphRange.location = lineFragment._range.location; + effectiveGlyphRange.length = lineFragment._range.length; + } + + return CPRectCreateCopy(lineFragment._usedRect); +} + +- (CPPoint)locationForGlyphAtIndex:(unsigned)index +{ + if (_lineFragments.length > 0 && index >= [self numberOfGlyphs] - 1) + { + var lineFragment= _lineFragments[_lineFragments.length-1], + glyphFrames = [lineFragment glyphFrames]; + + if (glyphFrames.length > 0) + return CPPointCreateCopy(glyphFrames[glyphFrames.length - 1].origin); + } + + var lineFragment = _objectWithLocationInRange(_lineFragments, index); + + if (lineFragment) + { + if (index == lineFragment._range.location) + return CPPointCreateCopy(lineFragment._location); + + var glyphFrames = [lineFragment glyphFrames]; + + return CPPointCreateCopy(glyphFrames[index - lineFragment._range.location].origin); + } + + return CPPointMakeZero(); +} + +- (CPTextContainer)textContainerForGlyphAtIndex:(unsigned)index effectiveRange:(CPRangePointer)effectiveGlyphRange withoutAdditionalLayout:(BOOL)flag +{ + var lineFragment = _objectWithLocationInRange(_lineFragments, index); + + if (lineFragment) + { + if (effectiveGlyphRange) + { + effectiveGlyphRange.location = lineFragment._range.location; + effectiveGlyphRange.length = lineFragment._range.length; + } + + return lineFragment._textContainer; + } + + return [_textContainers lastObject]; +} + +- (CPTextContainer)textContainerForGlyphAtIndex:(unsigned)index effectiveRange:(CPRangePointer)effectiveGlyphRange +{ + return [self textContainerForGlyphAtIndex:index effectiveRange:effectiveGlyphRange withoutAdditionalLayout:NO]; +} + +- (CPRange)characterRangeForGlyphRange:(CPRange)aRange actualGlyphRange:(CPRangePointer)actualRange +{ + return _MakeRangeFromAbs([self characterIndexForGlyphAtIndex:aRange.location], + [self characterIndexForGlyphAtIndex:CPMaxRange(aRange)]); +} + +- (unsigned)characterIndexForGlyphAtIndex:(unsigned)index +{ + /* FIXME: stub */ + return index; +} + +- (void)setLineFragmentFactory:(Class)lineFragmentFactory +{ + _lineFragmentFactory = lineFragmentFactory; +} + +- (CPArray)rectArrayForCharacterRange:(CPRange)charRange + withinSelectedCharacterRange:(CPRange)selectedCharRange + inTextContainer:(CPTextContainer)container + rectCount:(CPRectPointer)rectCount +{ + + var rectArray = [], + lineFragments = _objectsInRange(_lineFragments, selectedCharRange); + + if (!lineFragments.length) + return rectArray; + + var containerSize = [container containerSize]; + + for (var i = 0; i < lineFragments.length; i++) + { + var fragment = lineFragments[i]; + if (fragment._textContainer === container) + { + var frames = [fragment glyphFrames], + rect = nil, + len = fragment._range.length; + + for (var j = 0; j < len; j++) + { + if (CPLocationInRange(fragment._range.location + j, selectedCharRange)) + { + if (!rect) + rect = CPRectCreateCopy(frames[j]); + else + rect = CPRectUnion(rect, frames[j]); + + if ([[_textStorage string] characterAtIndex:MAX(0, CPMaxRange(selectedCharRange)-1)] === '\n' ) + { + rect.size.width = containerSize.width - rect.origin.x; + } + } + } + + if (rect) + rectArray.push(rect); + } + } + + var len = rectArray.length; + for (var i = 0; i < len - 1; i++) // extend the width of all but the last one + { + if (rectArray[i].origin.y == rectArray[i + 1].origin.y) + continue; + rectArray[i].size.width = containerSize.width - rectArray[i].origin.x; + } + + return rectArray; +} +@end diff --git a/AppKit/CPTextView/CPParagraphStyle.j b/AppKit/CPTextView/CPParagraphStyle.j new file mode 100755 index 000000000..b325dc4dd --- /dev/null +++ b/AppKit/CPTextView/CPParagraphStyle.j @@ -0,0 +1,186 @@ +/* + * CPParagraphStyle.j + * AppKit + * + * FIXME + * This is basically a stub. + * We need to store all the spacing informations as well as writing direction (among others) + * + * Created by Daniel Boehringer on 11/01/2014 + * Copyright Daniel Boehringer 2014. + * + * 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 + +var _sharedDefaultParagraphStyle, + _defaultTabStopArray; + +CPLeftTabStopType = 0; + +/* +CPLeftTextAlignment = 0; +CPCenterTextAlignment = 1; +CPRightTextAlignment = 2; +*/ + +CPParagraphStyleAttributeName = @"CPParagraphStyleAttributeName"; + +@implementation CPTextTab : CPObject +{ + int _type @accessors(property=tabStopType); + double _location @accessors(property=location); +} + +- (id)initWithType:(CPTabStopType) aType location:(double) aLocation +{ + if ([self = [super init]]) + { + _type = aType; + _location = aLocation; + } + + return self; +} + +- (id)initWithCoder:(id)aCoder +{ + self = [self init]; + + if (self) + { + _type = [aCoder decodeIntForKey:"_type"]; + _location = [aCoder decodeDoubleForKey:"_location"]; + } + + return self; +} + +- (void)encodeWithCoder:(id)aCoder +{ + [aCoder encodeInt:_type forKey:"_type"]; + [aCoder encodeDouble:_location forKey:"_location"]; +} + +@end + +@implementation CPParagraphStyle : CPObject +{ + CPArray _tabStops @accessors(property = tabStops); + CPTextAlignment _alignment @accessors(property = alignment); + unsigned _firstLineHeadIndent @accessors(property = firstLineHeadIndent); + unsigned _headIndent @accessors(property = headIndent); + unsigned _tailIndent @accessors(property = tailIndent); + unsigned _paragraphSpacing @accessors(property = paragraphSpacing); + unsigned _minimumLineHeight @accessors(property = minimumLineHeight); + unsigned _maximumLineHeight @accessors(property = maximumLineHeight); + unsigned _lineSpacing @accessors(property = lineSpacing); +} + ++ (CPParagraphStyle)defaultParagraphStyle +{ + if (!_sharedDefaultParagraphStyle) + _sharedDefaultParagraphStyle = [self new]; + + return _sharedDefaultParagraphStyle; +} + ++ (CPArray)_defaultTabStops +{ + if (!_defaultTabStopArray) + { + var i; + _defaultTabStopArray = []; + + // FIXME: Define constants for these magic numbers: 13, 28 + for (i = 1; i < 16 ; i++) + { + _defaultTabStopArray.push([[CPTextTab alloc] initWithType:CPLeftTabStopType location:i * 28]); + } + } + return _defaultTabStopArray; +} +- (void)addTabStop:(CPTextTab)aStop +{ + _tabStops.push(aStop); +} + +- (void)_initWithDefaults +{ + _alignment = CPLeftTextAlignment; + _tabStops = [[[self class] _defaultTabStops] copy]; +} + +- (id)init +{ + [self _initWithDefaults]; + + return self; +} +- (id)copy +{ + var other = [[self class] alloc]; + return [other initWithParagraphStyle:self]; +} +- initWithParagraphStyle:(CPParagraphStyle) other +{ + other._tabStops = [_tabStops copy]; + other._alignment = _alignment; + other._firstLineHeadIndent = _firstLineHeadIndent; + other._headIndent = _headIndent; + other._tailIndent = _tailIndent; + other._paragraphSpacing = _paragraphSpacing; + other._minimumLineHeight = _minimumLineHeight; + other._maximumLineHeight = _maximumLineHeight; + other._lineSpacing = _lineSpacing; + + return self; +} + +- (id)initWithCoder:(id)aCoder +{ + self = [self init]; + + if (self) + { + _tabStops = [aCoder decodeObjectForKey:"_tabStops"]; + _alignment = [aCoder decodeIntForKey:"_alignment"]; + _firstLineHeadIndent = [aCoder decodeIntForKey:"_firstLineHeadIndent"]; + _headIndent = [aCoder decodeIntForKey:"_headIndent"]; + _tailIndent = [aCoder decodeIntForKey:"_tailIndent"]; + _paragraphSpacing = [aCoder decodeIntForKey:"_paragraphSpacing"]; + _minimumLineHeight = [aCoder decodeIntForKey:"_minimumLineHeight"]; + _maximumLineHeight = [aCoder decodeIntForKey:"_maximumLineHeight"]; + _lineSpacing = [aCoder decodeIntForKey:"_lineSpacing"]; + } + + return self; +} + +- (void)encodeWithCoder:(id)aCoder +{ + [aCoder encodeInt:_alignment forKey:"_alignment"]; + [aCoder encodeObject:_tabStops forKey:"_tabStops"]; + [aCoder encodeInt:_firstLineHeadIndent forKey:"_firstLineHeadIndent"]; + [aCoder encodeInt:_headIndent forKey:"_headIndent"]; + [aCoder encodeInt:_tailIndent forKey:"_tailIndent"]; + [aCoder encodeInt:_paragraphSpacing forKey:"_paragraphSpacing"]; + [aCoder encodeInt:_minimumLineHeight forKey:"_minimumLineHeight"]; + [aCoder encodeInt:_maximumLineHeight forKey:"_maximumLineHeight"]; + [aCoder encodeInt:_lineSpacing forKey:"_lineSpacing"]; +} + +@end diff --git a/AppKit/CPTextView/CPTextContainer.j b/AppKit/CPTextView/CPTextContainer.j new file mode 100755 index 000000000..e5bbc68d5 --- /dev/null +++ b/AppKit/CPTextView/CPTextContainer.j @@ -0,0 +1,209 @@ +/* + * CPTextContainer.j + * AppKit + * + * 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 "CPLayoutManager.j" + +/* + @global + @group CPLineSweepDirection +*/ +CPLineSweepLeft = 0; +/* + @global + @group CPLineSweepDirection +*/ +CPLineSweepRight = 1; +/* + @global + @group CPLineSweepDirection +*/ +CPLineSweepDown = 2; +/* + @global + @group CPLineSweepDirection +*/ +CPLineSweepUp = 3; + +/* + @global + @group CPLineMovementDirection +*/ +CPLineDoesntMoves = 0; +/* + @global + @group CPLineMovementDirection +*/ +CPLineMovesLeft = 1; +/* + @global + @group CPLineMovementDirection +*/ +CPLineMovesRight = 2; +/* + @global + @group CPLineMovementDirection +*/ +CPLineMovesDown = 3; +/* + @global + @group CPLineMovementDirection +*/ +CPLineMovesUp = 4; + +/*! + @ingroup appkit + @class CPTextContainer +*/ +@implementation CPTextContainer : CPObject +{ + CPSize _size; + CPTextView _textView; + CPLayoutManager _layoutManager; + float _lineFragmentPadding; +} + +- (id)initWithContainerSize:(CPSize)aSize +{ + self = [super init]; + + if (self) + { + _size = aSize; + _lineFragmentPadding = 0.0; + } + + return self; +} + +- (id)init +{ + return [self initWithContainerSize:CPMakeSize(1e7, 1e7)]; +} + +- (CPSize)containerSize +{ + return _size; +} + +- (void)setContainerSize:(CPSize)someSize +{ + var oldSize = _size; + + _size = someSize; + + if (oldSize.width != _size.width) + [_layoutManager invalidateLayoutForCharacterRange:CPMakeRange(0,[[_layoutManager textStorage] length]) + isSoft:NO + actualCharacterRange:NULL]; + +} + +- (void)setWidthTracksTextView:(BOOL)flag +{ + // fixme: Controls whether the receiver adjusts the width of its bounding rectangle when its text view is resized. +} + +- (void)setTextView:(CPTextView)aTextView +{ + if (_textView) + { + [self _removeAllLines]; + [_textView setTextContainer:nil]; + } + + _textView = aTextView; + + if (_textView != nil) + [_textView setTextContainer:self]; + + [_layoutManager textContainerChangedTextView:self]; +} + +- (CPTextView)textView +{ + return _textView; +} + +- (void)setLayoutManager:(CPLayoutManager)aManager +{ + if (_layoutManager === aManager) + return; + + _layoutManager = aManager; +} + +- (CPLayoutManager)layoutManager +{ + return _layoutManager; +} + +- (void)setLineFragmentPadding:(float)aFloat +{ + _lineFragmentPadding = aFloat; +} + +- (float)lineFragmentPadding +{ + return _lineFragmentPadding; +} + +- (BOOL)containsPoint:(CPPoint)aPoint +{ + return CPRectContainsPoint(CPRectMake(0, 0, _size.width, _size.height), aPoint); +} + +- (BOOL)isSimpleRectangularTextContainer +{ + return YES; +} + +- (CPRect)lineFragmentRectForProposedRect:(CPRect)proposedRect + sweepDirection:(CPLineSweepDirection)sweep + movementDirection:(CPLineMovementDirection)movement + remainingRect:(CPRectPointer)remainingRect +{ + var resultRect = CPRectCreateCopy(proposedRect); + + if (sweep != CPLineSweepRight || movement != CPLineMovesDown) + { + CPLog.trace(@"FIXME: unsupported sweep ("+sweep+") or movement ("+movement+")"); + return CPRectMakeZero(); + } + + if (resultRect.origin.x + resultRect.size.width > _size.width) + resultRect.size.width = _size.width - resultRect.origin.x; + + if (resultRect.size.width < 0) + resultRect = CPRectMakeZero(); + + if (remainingRect) + { + remainingRect.origin.x = resultRect.origin.x + resultRect.size.width; + remainingRect.origin.y = resultRect.origin.y; + remainingRect.size.height = resultRect.size.height; + remainingRect.size.width = _size.width - (resultRect.origin.x + resultRect.size.width); + } + + return resultRect; +} + +@end diff --git a/AppKit/CPTextView/CPTextStorage.j b/AppKit/CPTextView/CPTextStorage.j new file mode 100755 index 000000000..bf2fc3855 --- /dev/null +++ b/AppKit/CPTextView/CPTextStorage.j @@ -0,0 +1,295 @@ +/* + * CPTextStorage.j + * AppKit + * + * 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 +@import "CPLayoutManager.j" + + +CPTextStorageEditedAttributes = 1; +CPTextStorageEditedCharacters = 2; + +CPTextStorageWillProcessEditingNotification = @"CPTextStorageWillProcessEditingNotification"; +CPTextStorageDidProcessEditingNotification = @"CPTextStorageDidProcessEditingNotification"; + +/* + FIXME: move these to CPAttributed string + Make use of attributed keys in AppKit +*/ +CPFontAttributeName = @"CPFontAttributeName"; +CPForegroundColorAttributeName = @"CPForegroundColorAttributeName"; +CPBackgroundColorAttributeName = @"CPBackgroundColorAttributeName"; +CPShadowAttributeName = @"CPShadowAttributeName"; +CPUnderlineStyleAttributeName = @"CPUnderlineStyleAttributeName"; +CPSuperscriptAttributeName = @"CPSuperscriptAttributeName"; +CPBaselineOffsetAttributeName = @"CPBaselineOffsetAttributeName"; +CPAttachmentAttributeName = @"CPAttachmentAttributeName"; +CPLigatureAttributeName = @"CPLigatureAttributeName"; +CPKernAttributeName = @"CPKernAttributeName"; + +/*! + @ingroup appkit + @class CPTextStorage +*/ +@implementation CPTextStorage : CPAttributedString +{ + CPMutableArray _layoutManagers; + id _delegate; + + int _changeInLength; + unsigned _editedMask; + CPRange _editedRange; + int _editCount; // {begin,end}Editing counter + + CPFont _font; + CPColor _foregroundColor; +} + +- (id)initWithString:(CPString)aString attributes:(CPDictionary)attributes +{ + self = [super initWithString:aString attributes:attributes]; + + if (self) + { + _layoutManagers = [[CPMutableArray alloc] init]; + _editedRange = CPMakeRange(CPNotFound, 0); + _changeInLength = 0; + _editedMask = 0; + } + + return self; +} + +- (id)initWithString:(CPString)aString +{ + return [self initWithString:aString attributes:nil]; +} + +- (id)init +{ + return [self initWithString:@"" attributes:nil]; +} + +- (id)delegate +{ + return _delegate; +} + +- (void)setDelegate:(id)aDelegate +{ + if (_delegate === aDelegate) + return; + + var notificationCenter = [CPNotificationCenter defaultCenter]; + + if (_delegate && aDelegate === nil) + { + [notificationCenter removeObserver:_delegate name:CPTextStorageWillProcessEditingNotification object:self]; + [notificationCenter removeObserver:_delegate name:CPTextStorageDidProcessEditingNotification object:self]; + } + + _delegate = aDelegate; + + if (_delegate) + { + if ([_delegate respondsToSelector:@selector(textStorageWillProcessEditing:)]) + [notificationCenter addObserver:_delegate selector:@selector(textStorageWillProcessEditing:) name:CPTextStorageWillProcessEditingNotification object:self]; + + if ([_delegate respondsToSelector:@selector(textStorageDidProcessEditing:)]) + [notificationCenter addObserver:_delegate selector:@selector(textStorageDidProcessEditing:) name:CPTextStorageDidProcessEditingNotification object:self]; + } +} + +- (void)addLayoutManager:(CPLayoutManager)aManager +{ + if (![_layoutManagers containsObject:aManager]) + { + [aManager setTextStorage:self]; + [_layoutManagers addObject:aManager]; + } +} +- (void)removeLayoutManager:(CPLayoutManager)aManager +{ + if ([_layoutManagers containsObject:aManager]) + { + [aManager setTextStorage:nil]; + [_layoutManagers removeObject:aManager]; + } +} + +- (CPArray)layoutManagers +{ + return _layoutManagers; +} + +- (CPRange)editedRange +{ + return _editedRange; +} + +- (int)changeInLength +{ + return _changeInLength; +} + +- (unsigned)editedMask +{ + return _editedMask; +} + +- (void)invalidateAttributesInRange:(CPRange)aRange +{ + /* FIXME: stub */ +} + +- (void)processEditing +{ + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextStorageWillProcessEditingNotification + object:self]; + + [self invalidateAttributesInRange:[self editedRange]]; + + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextStorageDidProcessEditingNotification + object:self]; + + var c = [_layoutManagers count]; + + for (var i = 0; i < c; i++) + { + [[_layoutManagers objectAtIndex:i] textStorage:self + edited:_editedMask + range:_editedRange + changeInLength:_changeInLength + invalidatedRange:_editedRange]; + } + + _editedRange.location = CPNotFound; + _editedMask = 0; + _changeInLength = 0; +} + +- (void)beginEditing +{ + if (_editCount == 0) + _editedRange = CPMakeRange(CPNotFound, 0); + + _editCount++; +} + +- (void)endEditing +{ + _editCount--; + + if (_editCount == 0) + [self processEditing]; +} + +- (void)edited:(unsigned)editedMask range:(CPRange)aRange changeInLength:(int)lengthChange +{ + if (_editCount == 0) /* used outside a beginEditing/endEditing */ + { + _editedMask = editedMask; + _changeInLength = lengthChange; + aRange.length += lengthChange; + _editedRange = aRange; + [self processEditing]; + } + else + { + _editedMask |= editedMask; + _changeInLength += lengthChange; + aRange.length += lengthChange; + + if (_editedRange.location == CPNotFound) + _editedRange = aRange; + else + _editedRange = CPUnionRange(_editedRange,aRange); + } +} + +- (void)removeAttribute:(id)anAttribute range:(CPRange)aRange +{ + [self beginEditing]; + [super removeAttribute:anAttribute range:aRange]; + [self edited:CPTextStorageEditedAttributes range:aRange changeInLength:0]; + [self endEditing]; +} + +- (void)addAttributes:(CPDictionary)aDictionary range:(CPRange)aRange +{ + [self beginEditing]; + [super addAttributes:aDictionary range:aRange]; + [self edited:CPTextStorageEditedAttributes range:aRange changeInLength:0]; + [self endEditing]; +} + +- (void)deleteCharactersInRange:(CPRange)aRange +{ + [self beginEditing]; + [super deleteCharactersInRange:aRange]; + [self edited:CPTextStorageEditedCharacters range:aRange changeInLength:-aRange.length]; + [self endEditing]; +} + +- (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString +{ + [self beginEditing]; + [super replaceCharactersInRange: aRange withString: aString]; + [self edited: CPTextStorageEditedCharacters range:aRange changeInLength:([aString length] - aRange.length)]; + [self endEditing]; +} + +- (void)replaceCharactersInRange:(CPRange)aRange withAttributedString:(CPAttributedString)aString +{ + [self beginEditing]; + [super replaceCharactersInRange: aRange withAttributedString:aString]; + [self edited:(CPTextStorageEditedAttributes | CPTextStorageEditedCharacters) range:aRange changeInLength:([aString length] - aRange.length)]; + [self endEditing]; +} + +- (void)setFont:(CPFont)aFont +{ + _font = aFont; +} + +- (CPFont)font +{ + return _font; +} + +- (void)setForegroundColor:(CPColor)color +{ + _foregroundColor = color; +} + +- (CPColor)foregroundColor +{ + return _foregroundColor; +} + +- (CPAttributedString)attributedSubstringFromRange:(CPRange)aRange +{ + if (!aRange.length) + return [CPAttributedString new]; + return [super attributedSubstringFromRange:aRange]; +} +@end diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j new file mode 100755 index 000000000..226526f77 --- /dev/null +++ b/AppKit/CPTextView/CPTextView.j @@ -0,0 +1,1709 @@ +/* + * CPTextView.j + * AppKit + * + * Created by Daniel Boehringer on 27/12/2013. + * All modifications copyright Daniel Boehringer 2013. + * 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 "CPParagraphStyle.j" +@import "CPTextStorage.j" +@import "CPTextContainer.j" +@import "CPLayoutManager.j" +@import "CPFontManager.j" + +_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); +}; + + +// FIXME: move to theme ? +@implementation CPColor(CPTextViewExtensions) + ++ (CPColor)selectedTextBackgroundColor +{ + return [CPColor colorWithHexString:"99CCFF"]; +} ++ (CPColor)selectedTextBackgroundColorUnfocussed +{ + return [CPColor colorWithHexString:"CCCCCC"]; +} + +@end + +/* + CPTextView Notifications +*/ +CPTextViewDidChangeSelectionNotification = @"CPTextViewDidChangeSelectionNotification"; +CPTextViewDidChangeTypingAttributesNotification = @"CPTextViewDidChangeTypingAttributesNotification"; + +/* + CPSelectionGranularity +*/ +CPSelectByCharacter = 0; +CPSelectByWord = 1; +CPSelectByParagraph = 2; + + +var kDelegateRespondsTo_textShouldBeginEditing = 0x0001, + kDelegateRespondsTo_textView_doCommandBySelector = 0x0002, + kDelegateRespondsTo_textView_willChangeSelectionFromCharacterRange_toCharacterRange = 0x0004, + kDelegateRespondsTo_textView_shouldChangeTextInRange_replacementString = 0x0008, + kDelegateRespondsTo_textView_shouldChangeTypingAttributes_toAttributes = 0x0010; + +/*! + @ingroup appkit + @class CPTextView +*/ +@implementation CPTextView : CPText +{ + CPTextStorage _textStorage; + CPTextContainer _textContainer; + CPLayoutManager _layoutManager; + id _delegate; + + unsigned _delegateRespondsToSelectorMask; + + CPSize _textContainerInset; + CPPoint _textContainerOrigin; + + int _startTrackingLocation; + CPRange _selectionRange; + CPDictionary _selectedTextAttributes; + int _selectionGranularity; + + CPColor _insertionPointColor; + + CPDictionary _typingAttributes; + + BOOL _isFirstResponder; + + BOOL _drawCaret; + CPTimer _caretTimer; + CPTimer _scollingTimer; + CPRect _caretRect; + + CPFont _font; + CPColor _textColor; + + CPSize _minSize; + CPSize _maxSize; + + BOOL _scrollingDownward; + + /* use bit mask ? */ + BOOL _isRichText; + BOOL _usesFontPanel; + BOOL _allowsUndo; + BOOL _isHorizontallyResizable; + BOOL _isVerticallyResizable; + BOOL _isEditable; + BOOL _isSelectable; + + var _caretDOM; + int _stickyXLocation; +} + +- (id)initWithFrame:(CPRect)aFrame textContainer:(CPTextContainer)aContainer +{ + self = [super initWithFrame:aFrame]; + + if (self) + { + _DOMElement.style.cursor = "text"; + _textContainerInset = CPSizeMake(2,0); + _textContainerOrigin = CPPointMake(_bounds.origin.x, _bounds.origin.y); + [aContainer setTextView:self]; + _isEditable = YES; + _isSelectable = YES; + + _isFirstResponder = NO; + _delegate = nil; + _delegateRespondsToSelectorMask = 0; + _selectionRange = CPMakeRange(0, 0); + + _selectionGranularity = CPSelectByCharacter; + _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]]; + + _minSize = CPSizeCreateCopy(aFrame.size); + _maxSize = CPSizeMake(aFrame.size.width, 1e7); + + _isRichText = YES; + _usesFontPanel = YES; + _allowsUndo = YES; + _isVerticallyResizable = YES; + _isHorizontallyResizable = NO; + + _caretRect = CPRectMake(0,0,1,11); + } + + [self registerForDraggedTypes:[CPColorDragType]]; + + return self; +} + +- (BOOL)_isFocused +{ + return [[self window] isKeyWindow] && _isFirstResponder; +} +- (void)becomeKeyWindow +{ + [self setNeedsDisplay:YES]; +} + +/*! + @ignore +*/ +- (void)resignKeyWindow +{ + [self setNeedsDisplay:YES]; +} + +- (void)undo:(id)sender +{ + if (_allowsUndo) + [[[self window] undoManager] undo]; +} + +- (void)redo:(id)sender +{ + if (_allowsUndo) + [[[self window] undoManager] redo]; +} + +- (id)initWithFrame:(CPRect)aFrame +{ + var layoutManager = [[CPLayoutManager alloc] init], + textStorage = [[CPTextStorage alloc] init], + container = [[CPTextContainer alloc] initWithContainerSize:CPSizeMake(aFrame.size.width, 1e7)]; + + [textStorage addLayoutManager:layoutManager]; + [layoutManager addTextContainer:container]; + + return [self initWithFrame:aFrame textContainer:container]; +} + +- (void)setDelegate:(id)aDelegate +{ + _delegateRespondsToSelectorMask = 0; + + if (_delegate) + [[CPNotificationCenter defaultCenter] removeObserver:_delegate name:nil object:self]; + + _delegate = aDelegate; + + if (_delegate) + { + if ([_delegate respondsToSelector:@selector(textDidChange:)]) + [[CPNotificationCenter defaultCenter] addObserver:_delegate selector:@selector(textDidChange:) name:CPTextDidChangeNotification object:self]; + + if ([_delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) + [[CPNotificationCenter defaultCenter] addObserver:_delegate selector:@selector(textViewDidChangeSelection:) name:CPTextViewDidChangeSelectionNotification object:self]; + + if ([_delegate respondsToSelector:@selector(textViewDidChangeTypingAttributes:)]) + [[CPNotificationCenter defaultCenter] addObserver:_delegate selector:@selector(textViewDidChangeTypingAttributes:) name:CPTextViewDidChangeTypingAttributesNotification object:self]; + + 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; + } +} + +- (CPString)string +{ + return [_textStorage string]; +} + +- (void)setString:(CPString)aString +{ + [_textStorage replaceCharactersInRange: CPMakeRange(0, [_layoutManager numberOfCharacters]) withString:aString]; + [self didChangeText]; + [_layoutManager _validateLayoutAndGlyphs]; + [self sizeToFit]; + [self setNeedsDisplay:YES]; +} + +// 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]; +} + +- (CPTextStorage)textStorage +{ + return _textStorage; +} + +- (CPTextContainer)textContainer +{ + return _textContainer; +} + +- (CPLayoutManager)layoutManager +{ + return _layoutManager; +} + +- (void)setTextContainerInset:(CPSize)aSize +{ + _textContainerInset = aSize; + [self invalidateTextContainerOrigin]; +} + +- (CPSize)textContainerInset +{ + return _textContainerInset; +} + +- (CPPoint)textContainerOrigin +{ + return _textContainerOrigin; +} + +- (void)invalidateTextContainerOrigin +{ + _textContainerOrigin.x = _bounds.origin.x; + _textContainerOrigin.x += _textContainerInset.width; + + _textContainerOrigin.y = _bounds.origin.y; + _textContainerOrigin.y += _textContainerInset.height; +} + +- (BOOL)isEditable +{ + return _isEditable; +} + +- (void)setEditable:(BOOL)flag +{ + _isEditable = flag; + if (flag) + _isSelectable = flag; +} + +- (BOOL)isSelectable +{ + return _isSelectable; +} + +- (void)setSelectable:(BOOL)flag +{ + _isSelectable = flag; + if (flag) + _isEditable = flag; +} + +- (void)doCommandBySelector:(SEL)aSelector +{ + var done = NO; + + if (_delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_doCommandBySelector) + done = [_delegate textView:self doCommandBySelector:aSelector]; + + if (!done) + [super doCommandBySelector:aSelector]; +} + +- (void)didChangeText +{ + [[CPNotificationCenter defaultCenter] postNotificationName: CPTextDidChangeNotification object:self]; +} + +- (BOOL)shouldChangeTextInRange:(CPRange)aRange replacementString:(CPString)aString +{ + if (!_isEditable) + return NO; + + var shouldChange = YES; + + if (_delegateRespondsToSelectorMask & kDelegateRespondsTo_textShouldBeginEditing) + shouldChange = [_delegate textShouldBeginEditing:self]; + + if (shouldChange && (_delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_shouldChangeTextInRange_replacementString)) + shouldChange = [_delegate textView:self shouldChangeTextInRange:aRange replacementString:aString]; + + return shouldChange; +} + +- (void)_replaceCharactersInRange:aRange withAttributedString: aString +{ + [_textStorage replaceCharactersInRange:aRange withAttributedString:aString]; + [self setSelectedRange:CPMakeRange(aRange.location, [aString length])]; + [_layoutManager _validateLayoutAndGlyphs]; + [self sizeToFit]; + [self scrollRangeToVisible:_selectionRange]; + [self setNeedsDisplay:YES]; + +} +- (void)_replaceCharactersInRange: aRange withString: aString +{ + [_textStorage replaceCharactersInRange:CPMakeRangeCopy(aRange) withString:aString]; + [self setSelectedRange:CPMakeRange(aRange.location, aString.length)]; + [_layoutManager _validateLayoutAndGlyphs]; + [self sizeToFit]; + [self scrollRangeToVisible:_selectionRange]; + [self setNeedsDisplay:YES]; +} + +- (void)insertText:(id)aString +{ + var isAttributed = [aString isKindOfClass:CPAttributedString], + string = (isAttributed)?[aString string]:aString; + + if (![self shouldChangeTextInRange:CPMakeRangeCopy(_selectionRange) replacementString:string]) + return; + + + if (isAttributed) + { + [[[[self window] undoManager] prepareWithInvocationTarget: self] + _replaceCharactersInRange:CPMakeRange(_selectionRange.location, [aString length]) + withAttributedString:[_textStorage attributedSubstringFromRange:CPMakeRangeCopy(_selectionRange)]]; + [[[self window] undoManager] setActionName:@"Replace rich text"]; + + [_textStorage replaceCharactersInRange: CPMakeRangeCopy(_selectionRange) withAttributedString:aString]; + } + else + { + [[[self window] undoManager] setActionName:@"Replace plain text"]; + if (_isRichText) + { + aString = [[CPAttributedString alloc] initWithString:aString attributes:_typingAttributes]; + [[[[self window] undoManager] prepareWithInvocationTarget:self] + _replaceCharactersInRange:CPMakeRange(_selectionRange.location, [aString length]) + withAttributedString: [_textStorage attributedSubstringFromRange:CPMakeRangeCopy(_selectionRange)]]; + [_textStorage replaceCharactersInRange: CPMakeRangeCopy(_selectionRange) withAttributedString:aString]; + } + else + { + [[[[self window] undoManager] prepareWithInvocationTarget:self] _replaceCharactersInRange:CPMakeRange(_selectionRange.location, [aString length]) withString:[[self string] substringWithRange:CPMakeRangeCopy(_selectionRange)]]; + [_textStorage replaceCharactersInRange: CPMakeRangeCopy(_selectionRange) withString:aString]; + } + } + + [self setSelectedRange:CPMakeRange(_selectionRange.location + [string length], 0)]; + + [self didChangeText]; + [_layoutManager _validateLayoutAndGlyphs]; + [self sizeToFit]; + [self scrollRangeToVisible:_selectionRange]; + _stickyXLocation = _caretRect.origin.x; +} + +- (void)_blinkCaret:(CPTimer)aTimer +{ + _drawCaret = !_drawCaret; + [self setNeedsDisplayInRect:_caretRect]; +} + +- (void)drawRect:(CPRect)aRect +{ + var ctx = [[CPGraphicsContext currentContext] graphicsPort], + range = [_layoutManager glyphRangeForBoundingRect:aRect inTextContainer:_textContainer]; + + if (_selectionRange.length) + { + var rects = [_layoutManager rectArrayForCharacterRange:_selectionRange + withinSelectedCharacterRange:_selectionRange + inTextContainer:_textContainer + rectCount:nil]; + + CGContextSaveGState(ctx); + var effectiveSelectionColor = [self _isFocused]? [_selectedTextAttributes objectForKey:CPBackgroundColorAttributeName] : [CPColor selectedTextBackgroundColorUnfocussed]; + + CGContextSetFillColor(ctx, effectiveSelectionColor); + + for (var i = 0; i < rects.length; i++) + { + rects[i].origin.x += _textContainerOrigin.x; + rects[i].origin.y += _textContainerOrigin.y; + + CGContextFillRect(ctx, rects[i]); + } + + CGContextRestoreGState(ctx); + } + + if (range.length) + [_layoutManager drawGlyphsForGlyphRange: range atPoint:_textContainerOrigin]; + + if ([self shouldDrawInsertionPoint]) + { + [self updateInsertionPointStateAndRestartTimer:NO]; + [self drawInsertionPointInRect:_caretRect color:_insertionPointColor turnedOn:_drawCaret]; + } + else // FIXME: breaks DOM abstraction, but i did get it working otherwise + if (_caretDOM) + _caretDOM.style.visibility = "hidden"; +} + +- (void)setSelectedRange:(CPRange)range +{ + [self setSelectedRange:range affinity:0 stillSelecting:NO]; + [self setTypingAttributes:[_textStorage attributesAtIndex:MAX(0, range.location -1) effectiveRange:nil]]; +} + +- (void)setSelectedRange:(CPRange)range affinity:(CPSelectionAffinity /* unused */ )affinity stillSelecting:(BOOL)selecting +{ + var maxRange = CPMakeRange(0, [_layoutManager numberOfCharacters]); + range = CPIntersectionRange(maxRange, range); + + if (!selecting && (_delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_willChangeSelectionFromCharacterRange_toCharacterRange)) + _selectionRange = [_delegate textView:self willChangeSelectionFromCharacterRange:_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 (_isFirstResponder) + [self updateInsertionPointStateAndRestartTimer:((_selectionRange.length === 0) && ![_caretTimer isValid])]; + + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeSelectionNotification object:self]; + + [self setTypingAttributes:[_textStorage attributesAtIndex:MAX(0, range.location -1) effectiveRange:nil]]; + } +} + +- (CPArray)selectedRanges +{ + return [_selectionRange]; +} + +- (void)keyDown:(CPEvent)event +{ + [self interpretKeyEvents:[event]]; +} + +- (void)mouseDown:(CPEvent)event +{ + var fraction = [], + point = [self convertPoint:[event locationInWindow] fromView:nil]; + + /* stop _caretTimer */ + [_caretTimer invalidate]; + _caretTimer = nil; + [self _hideCaret]; + + // 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]; + + var granularities = [-1, CPSelectByCharacter, CPSelectByWord, CPSelectByParagraph]; + [self setSelectionGranularity:granularities[[event clickCount]]]; + + var setRange = CPMakeRange(_startTrackingLocation, 0); + + if ([event modifierFlags] & CPShiftKeyMask) + { + setRange = _MakeRangeFromAbs(_startTrackingLocation < _MidRange(_selectionRange)? + CPMaxRange(_selectionRange) : _selectionRange.location, + _startTrackingLocation); + + } + [self setSelectedRange:setRange affinity:0 stillSelecting:YES]; +} + +- (void)_clearRange:(var)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 (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 */ + [self setSelectionGranularity:CPSelectByCharacter]; + [self setSelectedRange:[self selectedRange] affinity:0 stillSelecting:NO]; + var point = [_layoutManager locationForGlyphAtIndex: [self selectedRange].location]; + _stickyXLocation= point.x; + _startTrackingLocation = _selectionRange.location; +} + +- (void)moveDown:(id)sender +{ + if (_isSelectable) + { + 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 (point.y >= rectEnd.origin.y) + return; + + if (_stickyXLocation) + point.x = _stickyXLocation; + + // FIXME: Define constants for this magic number + point.y += 2 + rectSource.size.height; + point.x += 2; + + var dindex= [_layoutManager glyphIndexForPoint: point inTextContainer:_textContainer fractionOfDistanceThroughGlyph:fraction], + oldStickyLoc = _stickyXLocation; + [self _establishSelection:CPMakeRange(dindex,0) byExtending:NO]; + _stickyXLocation = oldStickyLoc; + [self scrollRangeToVisible: CPMakeRange(dindex, 0)] + } +} +- (void)moveDownAndModifySelection:(id)sender +{ + if (_isSelectable) + { + 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 (_isSelectable) + { + var fraction = [], + sindex = [self selectedRange].location, + rectSource = [_layoutManager boundingRectForGlyphRange:CPMakeRange(sindex, 1) inTextContainer:_textContainer], + point = rectSource.origin; + + if (point.y <= 0) + return; + + if (_stickyXLocation) + point.x = _stickyXLocation; + + point.y -= 2; // FIXME these should not be constants + point.x += 2; + + var dindex= [_layoutManager glyphIndexForPoint:point inTextContainer:_textContainer fractionOfDistanceThroughGlyph:fraction], + oldStickyLoc = _stickyXLocation; + [self _establishSelection:CPMakeRange(dindex,0) byExtending:NO]; + _stickyXLocation = oldStickyLoc; + [self scrollRangeToVisible: CPMakeRange(dindex, 0)] + } +} +- (void)moveUpAndModifySelection:(id)sender +{ + if (_isSelectable) + { + 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 (_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 (_isSelectable) + { + [self _extendSelectionIntoDirection: +1 granularity:CPSelectByCharacter]; + } +} +- (void)moveLeft:(id)sender +{ + if (_isSelectable) + { + [self _establishSelection:CPMakeRange(_selectionRange.location - 1, 0) byExtending:NO]; + } +} + +- (void)moveToEndOfParagraph:(id)sender +{ + if (_isSelectable) + { + var parRange = [self _characterRangeForUnitAtIndex:_selectionRange.location + inString:[self stringValue] + asDefinedByCharArray:['\n'] skip:YES]; + + [self _establishSelection:CPMakeRange(CPMaxRange(parRange), 0) byExtending:NO]; + } +} +- (void) moveToEndOfParagraphAndModifySelection:(id)sender +{ + if (_isSelectable) + { + [self _extendSelectionIntoDirection: +1 granularity:CPSelectByParagraph]; + } +} +- (void) moveParagraphForwardAndModifySelection:(id)sender +{ + if (_isSelectable) + { + [self _extendSelectionIntoDirection: +1 granularity:CPSelectByParagraph]; + } +} +- (void) moveParagraphForward:(id)sender +{ + if (_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 (_isSelectable) + { + [self _establishSelection:CPMakeRange(0, 0) byExtending:NO]; + } +} +- (void) moveToBeginningOfDocumentAndModifySelection:(id)sender +{ + if (_isSelectable) + { + [self _establishSelection:CPMakeRange(0, 0) byExtending:YES]; + } +} +- (void) moveToEndOfDocument:(id)sender +{ + if (_isSelectable) + { + [self _establishSelection:CPMakeRange([_layoutManager numberOfCharacters], 0) byExtending:NO]; + } +} +- (void) moveToEndOfDocumentAndModifySelection:(id)sender +{ + if (_isSelectable) + { + [self _establishSelection:CPMakeRange([_layoutManager numberOfCharacters], 0) byExtending:YES]; + } +} + +- (void) moveWordRight:(id)sender +{ + if (_isSelectable) + { + [self _moveSelectionIntoDirection: +1 granularity:CPSelectByWord] + } +} + +// FIXME +- (void)moveToBeginningOfParagraph:(id)sender +{ + if (_isSelectable) + { + var parRange = [self _characterRangeForUnitAtIndex:_selectionRange.location + inString:[self stringValue] + asDefinedByCharArray: ['\n'] skip:YES]; + + [self _establishSelection:CPMakeRange(parRange.location, 0) byExtending:NO]; + } +} +- (void) moveToBeginningOfParagraphAndModifySelection:(id)sender +{ + if (_isSelectable) + { + [self _extendSelectionIntoDirection: -1 granularity:CPSelectByParagraph]; + } +} +- (void) moveParagraphBackward:(id)sender +{ + if (_isSelectable) + { + [self _moveSelectionIntoDirection: -1 granularity:CPSelectByParagraph] + } +} +- (void) moveParagraphBackwardAndModifySelection:(id)sender +{ + if (_isSelectable) + { + [self _extendSelectionIntoDirection: -1 granularity:CPSelectByParagraph]; + } +} +- (void) moveWordRightAndModifySelection:(id)sender +{ + if (_isSelectable) + { + [self _extendSelectionIntoDirection: +1 granularity:CPSelectByWord]; + + } +} + +- (void) deleteToEndOfParagraph:(id)sender +{ + if (_isSelectable && _isEditable) + { + [self moveToEndOfParagraphAndModifySelection:self]; + [self delete:self]; + } +} + +- (void) deleteToBeginningOfParagraph:(id)sender +{ + if (_isSelectable && _isEditable) + { + [self moveToBeginningOfParagraphAndModifySelection:self]; + [self delete:self]; + } +} +- (void) deleteToBeginningOfLine:(id)sender +{ + if (_isSelectable && _isEditable) + { + [self moveToLeftEndOfLineAndModifySelection:self]; + [self delete:self]; + } +} +- (void) deleteToEndOfLine:(id)sender +{ + if (_isSelectable && _isEditable) + { + [self moveToRightEndOfLineAndModifySelection:self]; + [self delete:self]; + } +} +- (void) deleteWordBackward:(id)sender +{ + if (_isSelectable && _isEditable) + { + [self moveWordLeftAndModifySelection:self]; + [self delete:self]; + } +} +- (void) deleteWordForward:(id)sender +{ + if (_isSelectable && _isEditable) + { + [self moveWordRightAndModifySelection:self]; + [self delete:self]; + } +} +- (void) moveToLeftEndOfLine:(id)sender +{ + if (_isSelectable) + { + var fragment = [_layoutManager _lineFragmentForLocation:_selectionRange.location]; + if (fragment) + [self _establishSelection:CPMakeRange(fragment._range.location, 0) byExtending:NO]; + } +} +- (void) moveToLeftEndOfLineAndModifySelection:(id)sender +{ + if (_isSelectable) + { + var fragment = [_layoutManager _lineFragmentForLocation:_selectionRange.location]; + if (fragment) + [self _establishSelection:CPMakeRange(fragment._range.location, 0) byExtending:YES]; + } +} +- (void) moveToRightEndOfLine:(id)sender +{ + if (_isSelectable) + { + var fragment = [_layoutManager _lineFragmentForLocation:_selectionRange.location]; + if (fragment) + [self _establishSelection:CPMakeRange(CPMaxRange(fragment._range), 0) byExtending:NO]; + } +} +- (void) moveToRightEndOfLineAndModifySelection:(id)sender +{ + if (_isSelectable) + { + var fragment = [_layoutManager _lineFragmentForLocation:_selectionRange.location]; + if (fragment) + [self _establishSelection:CPMakeRange(CPMaxRange(fragment._range), 0) byExtending:YES]; + } +} + +- (void) moveWordLeftAndModifySelection:(id)sender +{ + if (_isSelectable) + { + [self _extendSelectionIntoDirection: -1 granularity:CPSelectByWord]; + } +} +- (void) moveWordLeft:(id)sender +{ + if (_isSelectable) + { + [self _moveSelectionIntoDirection: -1 granularity:CPSelectByWord] + } +} + +- (void)moveRight:(id)sender +{ + if (_isSelectable) + { + [self _establishSelection:CPMakeRange(CPMaxRange(_selectionRange) + 1, 0) byExtending:NO]; + } +} + +- (void)selectAll:(id)sender +{ + if (_isSelectable) + { + if (_caretTimer) + { + [_caretTimer invalidate]; + _caretTimer = nil; + } + + [self setSelectedRange:CPMakeRange(0, [_layoutManager numberOfCharacters])]; + } +} + +- (void)_deleteForRange:(CPRange) changedRange +{ + if (![self shouldChangeTextInRange:changedRange replacementString:@""]) + return; + + [[[[self window] undoManager] prepareWithInvocationTarget:self] _replaceCharactersInRange:CPMakeRange(_selectionRange.location, 0) withAttributedString:[_textStorage attributedSubstringFromRange:CPMakeRangeCopy(changedRange)]]; + [_textStorage deleteCharactersInRange: CPMakeRangeCopy(changedRange)]; + + [self setSelectedRange:CPMakeRange(changedRange.location, 0)]; + [self didChangeText]; + [_layoutManager _validateLayoutAndGlyphs]; + [self sizeToFit]; + _stickyXLocation = _caretRect.origin.x; +} + +- (void)deleteBackward:(id)sender +{ + var changedRange; + + if (CPEmptyRange(_selectionRange) && _selectionRange.location > 0) + changedRange = CPMakeRange(_selectionRange.location - 1, 1); + else + changedRange = _selectionRange; + + [self _deleteForRange: changedRange]; +} + +- (void)deleteForward:(id)sender +{ + var changedRange = nil; + + if (CPEmptyRange(_selectionRange) && _selectionRange.location < [_layoutManager numberOfCharacters]) + changedRange = CPMakeRange(_selectionRange.location, 1); + else + changedRange = _selectionRange; + + [self _deleteForRange: changedRange]; +} + +- (void)cut:(id)sender +{ + [self copy:sender]; + [self deleteBackward:sender] +} + +- (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]; +} + +- (BOOL)acceptsFirstResponder +{ + if (_isSelectable) + return YES; + + return NO; +} + +- (BOOL)becomeFirstResponder +{ + _isFirstResponder = YES; + [self updateInsertionPointStateAndRestartTimer:YES]; + [[CPFontManager sharedFontManager] setSelectedFont:[self font] isMultiple:NO]; + [self setNeedsDisplay:YES]; + return YES; +} + +- (BOOL)resignFirstResponder +{ + [_caretTimer invalidate]; + _caretTimer = nil; + _isFirstResponder = NO; + [self setNeedsDisplay:YES]; + return YES; +} + +- (void)setTypingAttributes:(CPDictionary)attributes +{ + if (!attributes) + attributes = [CPDictionary dictionary]; + + if (_delegateRespondsToSelectorMask & kDelegateRespondsTo_textView_shouldChangeTypingAttributes_toAttributes) + _typingAttributes = [_delegate textView:self shouldChangeTypingAttributes:_typingAttributes toAttributes:attributes]; + else + { + _typingAttributes = [attributes copy]; + /* check that new attributes contains essentials one's */ + if (![_typingAttributes containsKey:CPFontAttributeName]) + [_typingAttributes setObject:[self font] forKey:CPFontAttributeName]; + + if (![_typingAttributes containsKey:CPForegroundColorAttributeName]) + [_typingAttributes setObject:[self textColor] forKey:CPForegroundColorAttributeName]; + } + + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification + object:self]; +} + +- (CPDictionary)typingAttributes +{ + return _typingAttributes; +} + +- (void)setSelectedTextAttributes:(CPDictionary)attributes +{ + _selectedTextAttributes = attributes; +} + +- (CPDictionary)selectedTextAttributes +{ + return _selectedTextAttributes; +} + +- (void)delete:(id)sender +{ + [self deleteBackward: sender]; +} + +- stringValue +{ + return _textStorage._string; +} + +- objectValue +{ + return [self stringValue]; +} + +- (void)setFont:(CPFont)font +{ + _font = font; + var length = [_layoutManager numberOfCharacters]; + [_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 (!_isRichText) + { + _font = font; + [_textStorage setFont:_font]; + } + + [_textStorage addAttribute:CPFontAttributeName value:font range:CPMakeRangeCopy(range)]; + [_layoutManager _validateLayoutAndGlyphs]; + [self scrollRangeToVisible:CPMakeRange(CPMaxRange(range), 0)]; +} + +- (CPFont)font +{ + return _font; +} + +- (void)changeColor:(id)sender +{ + [self setTextColor:[sender color] range:_selectionRange]; +} + +- (void)changeFont:(id)sender +{ + var currRange = CPMakeRange(_selectionRange.location, 0), + oldFont, + attributes, + scrollRange = CPMakeRange(CPMaxRange(_selectionRange), 0); + + if (_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 + { + oldFont = [self font]; + var length = [_textStorage length]; + [self setFont:[sender convertFont:oldFont] range:CPMakeRange(0,length)]; + scrollRange = CPMakeRange(length, 0); + } + [_layoutManager _validateLayoutAndGlyphs]; + [self sizeToFit]; + [self setNeedsDisplay:YES]; + [self scrollRangeToVisible:scrollRange]; +} + +- (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; +} + +- (void)setUsesFontPanel:(BOOL)flag +{ + _usesFontPanel = flags; +} + +- (BOOL)usesFontPanel +{ + return _usesFontPanel; +} + +- (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 (!_isRichText) // FIXME + 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)]; +} + +- (CPColor)textColor +{ + return _textColor; +} + +- (BOOL)isRichText +{ + return _isRichText; +} + +- (BOOL)isRulerVisible +{ + return NO; +} + +- (BOOL)allowsUndo +{ + return _allowsUndo; +} + +- (CPRange)selectedRange +{ + return _selectionRange; +} + +- (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString +{ + + [_textStorage replaceCharactersInRange: aRange withString:aString]; +} + +- (CPString)string +{ + return [_textStorage string]; +} + +- (BOOL)isHorizontallyResizable +{ + return _isHorizontallyResizable; +} + +- (void)setHorizontallyResizable:(BOOL)flag +{ + _isHorizontallyResizable = flag; +} + +- (BOOL)isVerticallyResizable +{ + return _isVerticallyResizable; +} + +- (void)setVerticallyResizable:(BOOL)flag +{ + _isVerticallyResizable = flag; +} + +- (CPSize)maxSize +{ + return _maxSize; +} + +- (CPSize)minSize +{ + return _minSize; +} + +- (void)setMaxSize:(CPSize)aSize +{ + _maxSize = aSize; +} + +- (void)setMinSize:(CPSize)aSize +{ + _minSize = aSize; +} + +- (void)setConstrainedFrameSize:(CPSize)desiredSize +{ + [self setFrameSize:desiredSize]; +} + +- (void)sizeToFit +{ + [self setFrameSize:[self frameSize]] + +} + +- (void)setFrameSize:(CPSize) aSize +{ + var minSize = [self minSize], + maxSize = [self maxSize], + desiredSize = aSize, + rect = [_layoutManager boundingRectForGlyphRange: CPMakeRange(0, MAX(0, [_layoutManager numberOfCharacters] - 1)) inTextContainer:_textContainer]; + + if ([_layoutManager extraLineFragmentTextContainer] === _textContainer) + rect = CPRectUnion(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; + } + + [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] _wordBoundaryCharacterArray]; + break; + case CPSelectByParagraph: + characterSet = ['\n']; + break; + } + // FIXME if (!characterSet) croak! + return characterSet.join("").indexOf([self string].charAt(index)) !== CPNotFound; +} + +- (CPRange)_characterRangeForUnitAtIndex:(unsigned)index inString:(CPString)string asDefinedByCharArray: characterSet skip:(BOOL)flag +{ + var wordRange = CPMakeRange(0, 0), + lastIndex = CPNotFound, + searchIndex, + setString = characterSet.join(""); + + // do we start on a boundary character? + if (flag && string.charAt(index) && setString.indexOf(string.charAt(index)) !== CPNotFound) + { + // -> extend to the left + wordRange = CPMakeRange(index, 1); + while(setString.indexOf(string.charAt(--index)) !== CPNotFound && index > -1) + { + wordRange = CPMakeRange(index, 1); + + } + // -> extend to the right + for(index = wordRange.location; setString.indexOf(string.charAt(++index)) !== CPNotFound && index < string.length; ) + { + wordRange = _MakeRangeFromAbs(wordRange.location, MIN(string.length - 1, index + 1)); + + } + return wordRange; + } + + for (searchIndex = 0; searchIndex < characterSet.length; searchIndex++) + { + var peek = string.lastIndexOf(characterSet[searchIndex], index); + + if (peek !== CPNotFound) + { + if (lastIndex === CPNotFound) + lastIndex = peek; + else + lastIndex = MAX(lastIndex, peek); + } + } + + if (lastIndex !== CPNotFound) + wordRange.location = lastIndex + 1; + + lastIndex = CPNotFound; + + for (searchIndex = 0 ; searchIndex < characterSet.length; searchIndex++) + { + var peek= string.indexOf(characterSet[searchIndex], index); + + if (peek !== CPNotFound) + { + if (lastIndex === CPNotFound) + lastIndex = peek; + else + lastIndex = MIN(lastIndex, peek); + } + + } + + if (lastIndex != CPNotFound) + wordRange.length = lastIndex - wordRange.location; + else + wordRange.length = string.length - wordRange.location; + + return wordRange; +} + +/* FIXME + just a testing characterSet + all of this depend of the current language. + Need some CPLocale support and maybe even a FSM... + */ ++ (CPArray)_wordBoundaryCharacterArray +{ + return ['\n', ' ', '\t', ',', ';', '.', '!', '?', '\'', '"', '-', ':']; +} + + +- (CPRange)selectionRangeForProposedRange:(CPRange)proposedRange granularity:(CPSelectionGranularity)granularity +{ + var textStorageLength = [_layoutManager numberOfCharacters]; + + if (textStorageLength == 0) + return CPMakeRange(0, 0); + + if (proposedRange.location >= textStorageLength) + return CPMakeRange(textStorageLength, 0); + + if (CPMaxRange(proposedRange) > textStorageLength) + proposedRange.length = textStorageLength - proposedRange.location; + + var string = [_textStorage string]; + + switch (granularity) + { + case CPSelectByWord: + var wordRange = [self _characterRangeForUnitAtIndex:proposedRange.location inString:string asDefinedByCharArray: [[self class] _wordBoundaryCharacterArray] skip:YES]; + + if (proposedRange.length) + wordRange = CPUnionRange(wordRange, [self _characterRangeForUnitAtIndex:CPMaxRange(proposedRange) inString:string asDefinedByCharArray: [[self class] _wordBoundaryCharacterArray] skip:NO]); + + return wordRange; + + case CPSelectByParagraph: + var parRange = [self _characterRangeForUnitAtIndex: proposedRange.location inString: string asDefinedByCharArray: ['\n'] skip:NO]; + + if (proposedRange.length) + parRange = CPUnionRange(parRange, [self _characterRangeForUnitAtIndex: CPMaxRange(proposedRange) inString: string asDefinedByCharArray: ['\n'] skip:NO]); + + return parRange; + + default: + return proposedRange; + } +} + +- (void)setSelectionGranularity:(CPSelectionGranularity)granularity +{ + _selectionGranularity = granularity; +} + +- (CPSelectionGranularity)selectionGranularity +{ + return _selectionGranularity; +} + +- (CPColor)insertionPointColor +{ + return _insertionPointColor; +} + +- (void)setInsertionPointColor:(CPColor)aColor +{ + _insertionPointColor = aColor; +} + +- (BOOL)shouldDrawInsertionPoint +{ + return (_selectionRange.length === 0 && [self _isFocused]) +} + +- (void)drawInsertionPointInRect:(CPRect)aRect color:(CPColor)aColor turnedOn:(BOOL)flag +{ + 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"; + self._DOMElement.appendChild(_caretDOM); + } + + _caretDOM.style.left = (aRect.origin.x) + "px"; + _caretDOM.style.top = (aRect.origin.y) + "px"; + _caretDOM.style.height = (aRect.size.height) + "px"; + _caretDOM.style.visibility = flag ? "visible" : "hidden"; +} + +- (void)_hideCaret +{ + if (_caretDOM) + _caretDOM.style.visibility = "hidden"; +} + +- (void)updateInsertionPointStateAndRestartTimer:(BOOL)flag +{ + if (_selectionRange.length) + [self _hideCaret]; + + if (_selectionRange.location >= [_layoutManager numberOfCharacters]) // cursor is "behind" the last chacacter + { + _caretRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(MAX(0,_selectionRange.location - 1), 1) inTextContainer:_textContainer]; + _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]; + + _caretRect.origin.x += _textContainerOrigin.x; + _caretRect.origin.y += _textContainerOrigin.y; + _caretRect.size.width = 1; + + if (flag) + { + _drawCaret = flag; + _caretTimer = [CPTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(_blinkCaret:) userInfo:nil repeats:YES]; + } +} + +- (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 diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j new file mode 100755 index 000000000..a4efc9991 --- /dev/null +++ b/AppKit/CPTextView/CPTypesetter.j @@ -0,0 +1,393 @@ + +/* + * CPTypesetter.j + * AppKit + * + * Created by Daniel Boehringer on 27/12/2013. + * All modifications copyright Daniel Boehringer 2013. + * Based on original work by + * Emmanuel Maillard on 27/02/2010. + * Copyright Emmanuel Maillard 2010. + * + * FIXME: paragraphStyle indent information is currently not properly respected + * + * 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 +@import "CPTextStorage.j" +@import "CPParagraphStyle.j" + +/* + CPTypesetterControlCharacterAction +*/ +CPTypesetterZeroAdvancementAction = (1 << 0); +CPTypesetterWhitespaceAction = (1 << 1); +CPSTypesetterHorizontalTabAction = (1 << 2); +CPTypesetterLineBreakAction = (1 << 3); +CPTypesetterParagraphBreakAction = (1 << 4); +CPTypesetterContainerBreakAction = (1 << 5); + + +var _measuringContext; +var _measuringContextFont; +var _isCanvasSizingInvalid = 0; +var _didTestCanvasSizingValid; +function _widthOfStringForFont(aString, aFont) +{ + if (!_measuringContext) + _measuringContext = CGBitmapGraphicsContextCreate(); + if (!_didTestCanvasSizingValid && CPFeatureIsCompatible(CPHTMLCanvasFeature)) + { + var teststring = "0123456879abcdefghiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,.-()"; + _didTestCanvasSizingValid = YES; + _measuringContext.font = [aFont cssString]; + _isCanvasSizingInvalid = [teststring sizeWithFont:aFont].width != _measuringContext.measureText(teststring).width; + } + if (!CPFeatureIsCompatible(CPHTMLCanvasFeature) || _isCanvasSizingInvalid) // measuring with canvas is _much_ faster on chrome + return [aString sizeWithFont:aFont]; + if (_measuringContextFont !== aFont) + { + _measuringContextFont = aFont + _measuringContext.font = [aFont cssString]; + } + return _measuringContext.measureText(aString); +} + +var CPSystemTypesetterFactory = Nil; + +@implementation CPTypesetter : CPObject +{ +} + ++ (id)sharedSystemTypesetter +{ + return [CPSystemTypesetterFactory sharedInstance]; +} + ++ (void)_setSystemTypesetterFactory:(Class)aClass +{ + CPSystemTypesetterFactory = aClass; +} + ++ (void)initialize +{ + [CPTypesetter _setSystemTypesetterFactory:[CPSimpleTypesetter class]]; +} + +- (CPTypesetterControlCharacterAction)actionForControlCharacterAtIndex:(unsigned)charIndex +{ + return CPTypesetterZeroAdvancementAction; +} + +- (CPLayoutManager)layoutManager +{ + return nil; +} + +- (CPTextContainer)currentTextContainer +{ + return nil; +} + +- (CPArray)textContainers +{ + return nil; +} + +- (void)layoutGlyphsInLayoutManager:(CPLayoutManager)layoutManager + startingAtGlyphIndex:(unsigned)startGlyphIndex + maxNumberOfLineFragments:(unsigned)maxNumLines + nextGlyphIndex:(UIntegerPointer)nextGlyph +{ + CPLog.error(@"-[CPTypesetter subclass responsibility"); +} + +@end + +var _sharedSimpleTypesetter = nil; + +@implementation CPSimpleTypesetter:CPTypesetter +{ + CPLayoutManager _layoutManager; + CPTextContainer _currentTextContainer; + CPTextStorage _textStorage; + + CPRange _attributesRange; + CPDictionary _currentAttributes; + CPFont _currentFont; + CPParagraphStyle _currentParagraph; + + float _lineHeight; + float _lineBase; + float _lineWidth; + + unsigned _indexOfCurrentContainer; +} + ++ (id)sharedInstance +{ + if (_sharedSimpleTypesetter === nil) + _sharedSimpleTypesetter = [[CPSimpleTypesetter alloc] init]; + + return _sharedSimpleTypesetter; +} + +- (CPLayoutManager)layoutManager +{ + return _layoutManager; +} + +- (CPTextContainer)currentTextContainer +{ + return _currentTextContainer; +} + +- (CPArray)textContainers +{ + return [_layoutManager textContainers]; +} + +- (CPTextTab)textTabForWidth:(double)aWidth writingDirection:(CPWritingDirection)direction +{ + var tabStops = [_currentParagraph tabStops]; + + if (!tabStops) + tabStops = [CPParagraphStyle _defaultTabStops]; + + var i, + l = tabStops.length; + + if (aWidth > tabStops[l-1]._location) + return nil; + + for (i = l-1; i >= 0; i--) + { + if (aWidth > tabStops[i]._location) + { + if (i + 1 < l) + return tabStops[i + 1]; + } + } + return nil; +} + +- (BOOL)_flushRange:(CPRange)lineRange + lineOrigin:(CPPoint)lineOrigin + currentContainerSize:(CPSize)containerSize + advancements:(CPArray)advancements + lineCount:(unsigned)lineCount +{ + [_layoutManager setTextContainer:_currentTextContainer forGlyphRange:lineRange]; // creates a new lineFragment + var rect = CPRectMake(lineOrigin.x, lineOrigin.y, _lineWidth, _lineHeight); + [_layoutManager setLineFragmentRect: rect forGlyphRange:lineRange usedRect:rect]; + var myX = 0; + + switch ([_currentParagraph alignment]) + { + case CPLeftTextAlignment: + myX = 0; + break; + + case CPCenterTextAlignment: + myX = (containerSize.width - _lineWidth) / 2; + break; + + case CPRightTextAlignment: + myX = containerSize.width - _lineWidth; + break; + } + + [_layoutManager setLocation:CPMakePoint(myX, _lineBase) forStartOfGlyphRange:lineRange]; + [_layoutManager _setAdvancements:advancements forGlyphRange:lineRange]; + + if (!lineCount) + return NO; + + return ([_layoutManager _rescuingInvalidFragmentsWasPossibleForGlyphRange:lineRange]); +} + +- (void)layoutGlyphsInLayoutManager:(CPLayoutManager)layoutManager + startingAtGlyphIndex:(unsigned)glyphIndex + maxNumberOfLineFragments:(unsigned)maxNumLines + nextGlyphIndex:(UIntegerReference)nextGlyph +{ + _layoutManager = layoutManager; + _textStorage = [_layoutManager textStorage]; + _indexOfCurrentContainer = MAX(0, [[_layoutManager textContainers] + indexOfObject:[_layoutManager textContainerForGlyphAtIndex:glyphIndex effectiveRange:nil withoutAdditionalLayout:YES] + inRange:CPMakeRange(0, [[_layoutManager textContainers] count])]); + _currentTextContainer = [[_layoutManager textContainers] objectAtIndex:_indexOfCurrentContainer]; + _attributesRange = CPMakeRange(0, 0); + _lineHeight = 0; + _lineBase = 0; + _lineWidth = 0; + + var containerSize = [_currentTextContainer containerSize], + lineRange = CPMakeRange(glyphIndex, 0), + wrapRange = CPMakeRange(0, 0), + wrapWidth = 0, + isNewline = NO, + isTabStop = NO, + isWordWrapped = NO, + numberOfGlyphs= [_textStorage length], + leading; + + var numLines = 0, + theString = [_textStorage string], + lineOrigin, + ascent, descent; + + var advancements = [], + prevRangeWidth = 0, + measuringRange = CPMakeRange(glyphIndex, 0), + currentAnchor = 0, + _previousFont = nil; + + if (glyphIndex > 0) + lineOrigin = CPPointCreateCopy([_layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex effectiveRange:nil].origin); + else if ([_layoutManager extraLineFragmentTextContainer]) + lineOrigin = CPPointMake(0, [_layoutManager extraLineFragmentUsedRect].origin.y); + else + lineOrigin = CPPointMake(0, 0); + + [_layoutManager _removeInvalidLineFragments]; + + if (![_textStorage length]) + return; + + for (; numLines != maxNumLines && glyphIndex < numberOfGlyphs; glyphIndex++) + { + if (!CPLocationInRange(glyphIndex, _attributesRange)) + { + _currentAttributes = [_textStorage attributesAtIndex:glyphIndex effectiveRange:_attributesRange]; + _currentFont = [_currentAttributes objectForKey:CPFontAttributeName]; + _currentParagraph = [_currentAttributes objectForKey:CPParagraphStyleAttributeName] || [CPParagraphStyle defaultParagraphStyle]; + + if (!_currentFont) + _currentFont = [_textStorage font]; + + ascent = ["x" sizeWithFont:_currentFont].height; //FIXME + descent = 0; //FIXME + leading = (ascent - descent) * 0.2; // FAKE leading + } + + if (_previousFont !== _currentFont) + { + measuringRange = CPMakeRange(glyphIndex, 0); + currentAnchor = prevRangeWidth; + _previousFont = _currentFont; + } + + lineRange.length++; + measuringRange.length++; + + var currentChar = theString[glyphIndex], // use pure javascript methods for performance reasons + rangeWidth = _widthOfStringForFont(theString.substr(measuringRange.location, measuringRange.length), _currentFont).width + currentAnchor; + + switch (currentChar) // faster than sending actionForControlCharacterAtIndex: called for each char. + { + case '\n': + isNewline = YES; + break; + case '\t': + { + isTabStop = YES; + var nextTab = [self textTabForWidth:rangeWidth + lineOrigin.x writingDirection:0]; + if (nextTab) + { + rangeWidth = nextTab._location - lineOrigin.x; + } + else + rangeWidth += 28; //FIXME + } // fallthrough intentional + case ' ': + wrapRange = CPMakeRangeCopy(lineRange); + wrapWidth = rangeWidth; + break; + } + + advancements.push(rangeWidth - prevRangeWidth); + prevRangeWidth = _lineWidth = rangeWidth; + + if (lineOrigin.x + rangeWidth > containerSize.width) + { + if (wrapWidth) + { + lineRange = wrapRange; + _lineWidth = wrapWidth; + } + + isNewline = YES; + isWordWrapped = YES; + glyphIndex = CPMaxRange(lineRange) - 1; + } + + _lineHeight = MAX(_lineHeight, ascent - descent + leading); + _lineBase = MAX(_lineBase, ascent); + + if (isNewline || isTabStop) + { + if ([self _flushRange:lineRange lineOrigin:lineOrigin currentContainerSize:containerSize advancements:advancements lineCount:numLines]) + return; + + if (isTabStop) + { + lineOrigin.x += rangeWidth; + isTabStop = NO; + } + if (isNewline) + { + if ([_currentParagraph minimumLineHeight]) + _lineHeight = MAX(_lineHeight, [_currentParagraph minimumLineHeight]); + if ([_currentParagraph maximumLineHeight]) + _lineHeight = MIN(_lineHeight, [_currentParagraph maximumLineHeight]); + lineOrigin.y += _lineHeight; + if ([_currentParagraph lineSpacing]) + lineOrigin.y += [_currentParagraph lineSpacing]; + if (lineOrigin.y > [_currentTextContainer containerSize].height) + { + _currentTextContainer = [[_layoutManager textContainers] objectAtIndex: ++_indexOfCurrentContainer]; + } + lineOrigin.x = 0; + numLines++; + isNewline = NO; + } + _lineWidth = 0; + advancements = []; + prevRangeWidth = 0; + currentAnchor = 0; + _lineHeight = 0; + _lineBase = 0; + _previousFont = nil; + lineRange = CPMakeRange(glyphIndex + 1, 0); + wrapRange = CPMakeRange(0, 0); + wrapWidth = 0; + isWordWrapped = NO; + } + } + + // this is to "flush" the remaining characters + if (lineRange.length) + [self _flushRange:lineRange lineOrigin:lineOrigin currentContainerSize:containerSize advancements:advancements lineCount:numLines]; + + if ([theString.charAt(theString.length - 1) ==="\n"]) + { + var rect = CPRectMake(0, lineOrigin.y, containerSize.width, [_layoutManager._lineFragments lastObject]._usedRect.size.height); // fixme: row-height is crudely hacked + [_layoutManager setExtraLineFragmentRect:rect usedRect:rect textContainer:_currentTextContainer]; + } +} + +@end diff --git a/AppKit/CPTextView/RTFParser.j b/AppKit/CPTextView/RTFParser.j new file mode 100755 index 000000000..777800234 --- /dev/null +++ b/AppKit/CPTextView/RTFParser.j @@ -0,0 +1,700 @@ +/* RTFParser.j + + Parse a RTF string into a CPAttributedString + + Copyright (C) 2014 Daniel Boehringer + +FIXME: this really sucks and should be redone using a 'real' parser +e.g. using zaach/jison on github + * all paragraph spacing information is currently not parsed + + + * 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 + +// Hold the attributes of the current run +@implementation _RTFAttribute: CPObject +{ + CPRange _range; + CPParagraphStyle paragraph; + CPColor fgColour; + CPColor bgColour; + CPColor ulColour; + CPString fontName; + unsigned fontSize; + BOOL bold; + BOOL italic; + BOOL underline; + BOOL strikethrough; + BOOL script; + BOOL _tabChanged; +} + +- (id) init +{ + [self resetFont]; + [self resetParagraphStyle]; + _range = CPMakeRange(0, 0); + + return self; +} + +- (id) copy +{ + var mynew = [_RTFAttribute new]; + + mynew.paragraph = [paragraph copy]; + mynew.fontName = fontName; + mynew.fgColour = fgColour; + mynew.bgColour = bgColour; + mynew.ulColour = ulColour; + + return mynew; +} + +- (CPFont)currentFont +{ + var font = [CPFont _fontWithName:fontName size:fontSize bold:bold italic:italic]; + + if (font == nil) + { + /* Before giving up and using a default font, we try if this is + * not the case of a font with a composite name, such as + * 'Helvetica-Light'. In that case, even if we don't have + * exactly an 'Helvetica-Light' font family, we might have an + * 'Helvetica' one. */ + var range = [fontName rangeOfString:@"-"]; + + if (range.location != CPNotFound) + { + var fontFamily = [fontName substringToIndex: range.location]; + + font = [[CPFontManager sharedFontManager] fontWithFamily: fontFamily + traits: traits + weight: weight + size: fontSize]; + } + + if (font == nil) + { + console.log(@"RTFParser", + @"Could not find font %@ size %f traits %d weight %d", + fontName, fontSize, traits, weight); + + /* Last resort, default font. :-( */ + font = [CPFont systemFontOfSize: fontSize]; + } + } + return font; +} + +- (CPNumber)script +{ + return [CPNumber numberWithInt: script]; +} + +- (CPNumber)underline +{ + if (underline != 0) + return [CPNumber numberWithInteger: underline]; + else + return nil; +} + +- (CPNumber)strikethrough +{ + if (strikethrough != 0) + return [CPNumber numberWithInteger: strikethrough]; + else + return nil; +} + +- (void)resetParagraphStyle +{ + paragraph = [[CPParagraphStyle defaultParagraphStyle] copy]; +} + +- (void)resetFont +{ + var font = [CPFont systemFontOfSize:12]; + + fontName = [font familyName]; + fontSize = 12.0; + italic = NO; + bold = NO; + + underline = 0; + strikethrough = 0; + script = 0; +} + +- (void)addTab:(float)location type:(CPTextTabType)type +{ + var tab = [[CPTextTab alloc] initWithType: CPLeftTabStopType + location: location]; + + if (!_tabChanged) + { + [paragraph setTabStops:[tab]]; + _tabChanged = YES; + } + else + { + [paragraph addTabStop: tab]; + } +} + +-(CPDictionary) dictionary +{ + var ret = @{}; + [ret setObject:[self currentFont] forKey:CPFontAttributeName]; + [ret setObject:paragraph forKey:CPParagraphStyleAttributeName]; + + if (fgColour) + [ret setObject:fgColour forKey:CPForegroundColorAttributeName]; + + return ret; +} +@end + + +// based on https://github.com/lazygyu/RTF-parser + +var kRTFParserType_char = 0, + kRTFParserType_dest = 1, + kRTFParserType_prop = 2, + kRTFParserType_spec = 3; + +// Keyword descriptions +var kRgsymRtf = { + // keyword dflt fPassDflt kwd idx + "b" : [ "b", 1, false, kRTFParserType_prop, "propBold"], + "ul" : [ "ul", 1, false, kRTFParserType_prop, "propUnderline"], + "i" : [ "i", 1, false, kRTFParserType_prop, "propItalic"], + "li" : [ "li", 0, false, kRTFParserType_prop, "propPgnFormat"], + "pgnucltr" : [ "pgnucltr", "pgULtr", true, kRTFParserType_prop, "propPgnFormat"], + "pgnlcltr" : [ "pgnlcltr", "pgLLtr", true, kRTFParserType_prop, "propPgnFormat"], + "qc" : [ "qc", "justC", true, kRTFParserType_prop, "propJust"], + "ql" : [ "ql", "justL", true, kRTFParserType_prop, "propJust"], + "qr" : [ "qr", "justR", true, kRTFParserType_prop, "propJust"], + "qj" : [ "qj", "justF", true, kRTFParserType_prop, "propJust"], + "paperw" : [ "paperw", 12240, false, kRTFParserType_prop, "propXaPage"], + "paperh" : [ "paperh", 15480, false, kRTFParserType_prop, "propYaPage"], + "margl" : [ "margl", 1800, false, kRTFParserType_prop, "propXaLeft"], + "margr" : [ "margr", 1800, false, kRTFParserType_prop, "propXaRight"], + "margt" : [ "margt", 1440, false, kRTFParserType_prop, "propYaTop"], + "margb" : [ "margb", 1440, false, kRTFParserType_prop, "propYaBottom"], + "pgnstart" : [ "pgnstart", 1, true, kRTFParserType_prop, "propPgnStart"], + "facingp" : [ "facingp", 1, true, kRTFParserType_prop, "propFacingp"], + "landscape" : [ "landscape",1, true, kRTFParserType_prop, "propLandscape"], + "par" : [ "par", 0, false, kRTFParserType_char, "\n"], + "pard" : [ "pard", 0, false, kRTFParserType_prop, "propDefaultPara"], + "\0x0a" : [ "\0x0a", 0, false, kRTFParserType_char, "\n"], + "\0x0d" : [ "\0x0d", 0, false, kRTFParserType_char, ""], + "tab" : [ "tab", 0, false, kRTFParserType_char, "\t"], + "ldblquote" : [ "ldblquote",0, false, kRTFParserType_char, '"'], + "rdblquote" : [ "rdblquote",0, false, kRTFParserType_char, '"'], + "bin" : [ "bin", 0, false, kRTFParserType_spec, "ipfnBin"], + "*" : [ "*", 0, false, kRTFParserType_spec, "ipfnDestSkip"], + "'" : [ "'", 0, false, kRTFParserType_spec, "ipfnHex"], + "author" : [ "author", 0, false, kRTFParserType_dest, "destSkip"], + "buptim" : [ "buptim", 0, false, kRTFParserType_dest, "destSkip"], + "colortbl" : [ "colortbl", 0, false, kRTFParserType_dest, "destSkip"], + "comment" : [ "comment", 0, false, kRTFParserType_dest, "destSkip"], + "creatim" : [ "creatim", 0, false, kRTFParserType_dest, "destSkip"], + "doccomm" : [ "doccomm", 0, false, kRTFParserType_dest, "destSkip"], + "fonttbl" : [ "fonttbl", 0, false, kRTFParserType_dest, "destSkip"], + "footer" : [ "footer", 0, false, kRTFParserType_dest, "destSkip"], + "footerf" : [ "footerf", 0, false, kRTFParserType_dest, "destSkip"], + "footerl" : [ "footerl", 0, false, kRTFParserType_dest, "destSkip"], + "footerr" : [ "footerr", 0, false, kRTFParserType_dest, "destSkip"], + "footnote" : [ "footnote", 0, false, kRTFParserType_dest, "destSkip"], + "ftncn" : [ "ftncn", 0, false, kRTFParserType_dest, "destSkip"], + "ftnsep" : [ "ftnsep", 0, false, kRTFParserType_dest, "destSkip"], + "ftnsepc" : [ "ftnsepc", 0, false, kRTFParserType_dest, "destSkip"], + "fprq" : [ "fprq", 0, false, kRTFParserType_dest, "destSkip"], + "fcharset" : [ "fcharset", 0, false, kRTFParserType_dest, "destSkip"], + "rquote" : [ "rquote", 0, false, kRTFParserType_char, "'"], +// "s" : [ "s", 0, false, kRTFParserType_dest, "destSkip"], + "header" : [ "header", 0, false, kRTFParserType_dest, "destSkip"], + "headerf" : [ "headerf", 0, false, kRTFParserType_dest, "destSkip"], + "headerl" : [ "headerl", 0, false, kRTFParserType_dest, "destSkip"], + "headerr" : [ "headerr", 0, false, kRTFParserType_dest, "destSkip"], + "info" : [ "info", 0, false, kRTFParserType_dest, "destSkip"], + "keywords" : [ "keywords", 0, false, kRTFParserType_dest, "destSkip"], + "operator" : [ "operator", 0, false, kRTFParserType_dest, "destSkip"], + "pict" : [ "pict", 0, false, kRTFParserType_dest, "destSkip"], + "printim" : [ "printim", 0, false, kRTFParserType_dest, "destSkip"], + "private1" : [ "private1", 0, false, kRTFParserType_dest, "destSkip"], + "revtim" : [ "revtim", 0, false, kRTFParserType_dest, "destSkip"], + "rxe" : [ "rxe", 0, false, kRTFParserType_dest, "destSkip"], + "stylesheet" : [ "stylesheet",0, false, kRTFParserType_dest, "destSkip"], + "subject" : [ "subject", 0, false, kRTFParserType_dest, "destSkip"], + "tc" : [ "tc", 0, false, kRTFParserType_dest, "destSkip"], + "title" : [ "title", 0, false, kRTFParserType_dest, "destSkip"], + "txe" : [ "txe", 0, false, kRTFParserType_dest, "destSkip"], + "xe" : [ "xe", 0, false, kRTFParserType_dest, "destSkip"], + "[" : [ "[", 0, false, kRTFParserType_char, '['], + " " : [ " ", 0, false, kRTFParserType_char, ' '], + "]" : [ "]", 0, false, kRTFParserType_char, ']'], + "\\" : [ "\\", 0, false, kRTFParserType_char, '\\'] +} + +@implementation _RTFParser : CPObject +{ + CPString _codePage; + CPSize _paper; + CPString _rtf; + unsigned _curState; + CPArray _states; + unsigned _currentParseIndex; + BOOL _hexreturn; + _RTFAttribute _currentRun; + CPAttributedString _result; + CPArray _colorArray; + CPArray _fontArray; + CPString _freename; + BOOL _parsingFontTable; +} + +- (id)init +{ + if (self = [super init]) + { + _paper = CPMakeSize(0, 0); + _rtf = ""; + _curState = 0; // 0 = normal, 1 = skip + _states = []; + _currentParseIndex = 0; + _hexreturn = NO; + _currentRun = nil; + _result = [CPAttributedString new]; + _colorArray = []; + _fontArray = ['Arial']; // FIXME: should be name of system font + _freename = ""; + _parsingFontTable = NO; + } + return self; +} + +- (CPString)_checkChar:sym parameter:ch +{ + switch(_curState) + { + case 0: + if (sym && sym[4]) + return sym[4]; + + case 1: + console.log("skipped : " + sym[4]); + return ''; + default: + if (sym && sym[4]) + return sym[4]; + } +} +- (BOOL)pushState +{ + _states.push["group"]; + return YES; +} + +- (BOOL)popState +{ + _states.pop(); + if(_curState > 0) _curState--; + return YES; +} + +- (CPString)_parseSpec:sym parameter:v +{ + var ch = ''; + switch(sym[4]) + { + case "ipfnDestSkip": + _curState++; + return ''; + case "ipfnHex": + ch = _rtf.charAt(++_currentParseIndex); + var hex = ''; + while(/[a-fA-F0-9\']/.test(ch)) + { + if(ch == "'") + { + _currentParseIndex++; + continue; + } + hex += (ch + ''); + ch = _rtf.charAt(++_currentParseIndex); + } + //ch = parseInt(ch, 16); + console.log("hex : " + hex); + _hexreturn = YES; + _currentParseIndex--; + if (_curState !== 0) return ''; + else return hex; + break; + case "codePage": + ch = _rtf.charAt(++_currentParseIndex); + var code = ''; + while(/[0-9]/.test(ch)) + { + code += (ch + ''); + ch = _rtf.charAt(++_currentParseIndex); + } + _codePage=code; + _currentParseIndex--; + break; + } + return ''; +} + +- (void) _flushCurrentRun +{ + var newOffset = 0; + if (_currentRun) + { + if ([_result length] == _currentRun._range.location) + return; + _currentRun._range.length = [_result length] - _currentRun._range.location; + newOffset = CPMaxRange(_currentRun._range); + var dict = [_currentRun dictionary]; + [_result setAttributes:dict range:_currentRun._range]; // flush previous run + } + _currentRun = [_RTFAttribute new]; + _currentRun._range = CPMakeRange(newOffset, 0); // open a new one +} +- (CPString)_applyPropChange:sym parameter:param +{ + console.log("prop : " + sym[0] + " / param : " + param+ ' '); + + switch (sym[0]) + { + case "pard": + [self _flushCurrentRun]; + break; + case "b": // bold + if (param === 0) + { + if (_currentRun && _currentRun.bold) + [self _flushCurrentRun]; + _currentRun.bold = NO + } else + { + if (_currentRun && !_currentRun.bold) + [self _flushCurrentRun] + _currentRun.bold = YES; + } + break; + case "i": // italic + if (param === 0) + { + if (_currentRun && _currentRun.italic) + [self _flushCurrentRun]; + _currentRun.italic = NO + } else + { + if (_currentRun && !_currentRun.italic) + [self _flushCurrentRun] + _currentRun.italic = YES; + } + break; + case "qc": // paragraph center + [_currentRun.paragraph setAlignment:CPCenterTextAlignment]; + break; + case "paperw": + _paper.width = param; + break; + case "paperh": + _paper.height = param; + break; + } + + return ''; +} + + +- (CPString)_changeDest:sym +{ + switch (sym[0]) + { + case "colortbl": + _colorArray.push([CPColor blackColor]); + break; + case "fonttbl": + _parsingFontTable = YES; + break; + } + if (sym[4] == "destSkip") + { + console.log("Dest skip start : [" + sym[0] + "]"); + _curState++; + + } + return ''; +} + +- (CPString)_translateKeyword:keyword parameter:param fParameter:(BOOL)fParam +{ + if (kRgsymRtf[keyword] !== undefined ){ + var sym = kRgsymRtf[keyword]; + switch (sym[3]) + { + case kRTFParserType_prop: + if (sym[2] || !fParam) + { + param = sym[1]; + } + return [self _applyPropChange:sym parameter:param]; + case kRTFParserType_char: + return [self _checkChar:sym parameter:param]; + case kRTFParserType_dest: + return [self _changeDest:sym]; + case kRTFParserType_spec: + return [self _parseSpec:sym parameter:param]; + default: + return ''; + break; + } + } else + { + switch (keyword) + { + case "red": + var oldColor = [_colorArray lastObject], + green = [oldColor greenComponent], + blue = [oldColor blueComponent]; + _colorArray.pop(); + _colorArray.push([CPColor colorWithRed: parseInt(param) / 255 green:green blue:blue alpha:1.0]); + break; + case "green": + var oldColor = [_colorArray lastObject], + red = [oldColor redComponent], + blue = [oldColor blueComponent]; + _colorArray.pop(); + _colorArray.push([CPColor colorWithRed: red green: parseInt(param) / 255 blue:blue alpha:1.0]); + break; + case "blue": + var oldColor = [_colorArray lastObject], + green = [oldColor greenComponent], + red = [oldColor redComponent]; + _colorArray.pop(); + _colorArray.push([CPColor colorWithRed: red green:green blue:parseInt(param) / 255 alpha:1.0]); + break; + case "cf": // change foreground color + var fontIndex = parseInt(param) - 1; + if (_currentRun && fontIndex >= 0) + _currentRun.fgColour = _colorArray[fontIndex]; + break; + case "f": // change font + var fontIndex = parseInt(param); + if (_currentRun && fontIndex >= 0 && fontIndex < _fontArray.length) + _currentRun.fontName = _fontArray[fontIndex]; + + break; + case "fs": // change font size + _currentRun.fontSize = parseInt(param) / 2; + break; + case "tx": // tabstop + var location = parseInt(param) / 20; + if (_currentRun) + { + [_currentRun addTab:location type:CPLeftTabStopType]; + } + break; + default: + console.log("skip : " + keyword + " param: " + param); + + } + if (_states.length > 0) _curState = 1; + return ''; + } +} + +- (CPString)_parseKeyword:rtf length:len +{ + var ch = ''; + var fParam = false, fNeg = false; + var keyword = ''; + var param = ''; + _rtf = rtf; + + if (++_currentParseIndex >= len) + return len; + ch = rtf.charAt(_currentParseIndex); + + if (!/[a-zA-Z]/.test(ch)) + { + return [self _translateKeyword:ch parameter:nil fParameter:fParam]; + } + + while (/[a-zA-Z]/.test(ch)) + { + keyword += ch; + ch = rtf.charAt(++_currentParseIndex); + } + + if( ch == '-' ) + { + fNeg = true; + ch = rtf.charAt(++_currentParseIndex); + } + fParam = true; + + while (/[0-9]/.test(ch)) + { + param += (ch + ''); + ch = rtf.charAt(++_currentParseIndex); + } + _currentParseIndex--; + param = parseInt(param); + + if (fNeg) + param *= -1; + + return [self _translateKeyword:keyword parameter:param fParameter:fParam]; +} +- (void) _appendPlainString:(CPString) aString +{ + [_result replaceCharactersInRange:CPMakeRange([_result length], 0) withString:aString]; + +} +- (CPAttributedString) parseRTF:(CPString)rtf +{ + if(rtf.length == 0) + { + // alert("invalid rtf"); + return ''; + } + _currentParseIndex = -1; + var len = rtf.length; + var tmp = ''; + var ch = ''; + var hex = ''; + var lastchar = 0; + + while (_currentParseIndex < len) + { + tmp = rtf.charAt(++_currentParseIndex); + + if (tmp !== "\\" && hex.length > 0) + { + [self _appendPlainString: String.fromCharCode(parseInt((hex), 16))]; + hex = ''; + } + switch(tmp) + { + case " ": + if (lastchar == 1) + { + lastchar = 0; + } else + { + _freename += tmp; + [self _appendPlainString:tmp]; + } + break; + case "{": + if ([self pushState]) + { + console.log("push"); + } + break; + case "}": + if ([self popState]) + { + + console.log("pop"); + } + if (_freename) + { + console.log(_freename); + if (_parsingFontTable) + { + _fontArray.push(_freename); + _parsingFontTable = NO; + } + _freename = ""; + } + [self _flushCurrentRun] + break; + case "\\": + _freename = ''; + ch = [self _parseKeyword:rtf length:len]; + if (!_hexreturn && ch.length == 0) + { + lastchar = 1; + } else + { + lastchar = 0; + } + if (_hexreturn) + { + if(ch.length > 0) + { + if(parseInt(ch, 16) & 0x80) + { + hex += ch.toUpperCase(); + } else + { + [self _appendPlainString: String.fromCharCode(parseInt((hex + ch), 16))]; + hex = ''; + } + + if (hex.length == 4) + { + var temp = parseInt(hex, 16); + if (hexTable && hexTable[hex.toUpperCase()] !== undefined) + { + temp = parseInt(hexTable[hex.toUpperCase()], 16); + } + [self _appendPlainString: String.fromCharCode(temp)] + hex = ''; + } + } else + { + console.log("hex skipped"); + } + _hexreturn = NO; + } else + if (ch !== undefined && _curState === 0) + { + [self _appendPlainString:ch]; + + } + break; + case 0x0d: + case 0x0a: + case '\n': + case '\r': + break; + default: + lastchar = 0; + if (_curState == 0) + { + [self _appendPlainString:tmp]; + } else if (tmp !== ';') + { + _freename += tmp; + } + break; + } + } + return _result; +} + +@end \ No newline at end of file diff --git a/AppKit/CPTextView/RTFProducer.j b/AppKit/CPTextView/RTFProducer.j new file mode 100755 index 000000000..b33042d95 --- /dev/null +++ b/AppKit/CPTextView/RTFProducer.j @@ -0,0 +1,600 @@ +/* + RTFProducer.j + + Serialize CPAttributedString to a RTF String + + Copyright (C) 2014 Daniel Boehringer + This file is based on the RTFProducer from GNUStep + (which i co-authored with Fred Kiefer in 1999) + + * 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 +@import "CPFont.j" +@import "CPParagraphStyle.j" +@import "CPColor.j" + + +var PAPERSIZE = @"PaperSize"; +var LEFTMARGIN = @"LeftMargin"; +var RIGHTMARGIN = @"RightMargin"; +var TOPMARGIN = @"TopMargin"; +var BUTTOMMARGIN = @"ButtomMargin"; + +CPISOLatin1StringEncoding = "CPISOLatin1StringEncoding"; + +function _points2twips(a) { return (a)*20.0; } + + +@implementation RTFProducer:CPObject +{ + CPAttributedString text; + CPMutableDictionary fontDict; + CPMutableDictionary colorDict; + CPDictionary docDict; + CPMutableArray attachments; + CPFont currentFont; + + CPColor fgColor; + CPColor bgColor; + CPColor ulColor; +} + ++ (CPString)produceRTF: (CPAttributedString) aText documentAttributes: (CPDictionary)dict +{ + var mynew = [self new], + data; + + return [mynew RTFDStringFromAttributedString: aText + documentAttributes: dict]; +} + +- (id)init +{ + /* + * maintain a dictionary for the used colours + * (for rtf-header generation) + */ + colorDict = [CPMutableDictionary new]; + /* + * maintain a dictionary for the used fonts + * (for rtf-header generation) + */ + fontDict = [CPMutableDictionary new]; + + currentFont = nil; + fgColor = [CPColor blackColor]; + bgColor= [CPColor whiteColor]; + + return self; +} + +// private stuff follows +- (CPString) fontTable +{ + // write Font Table + if ([fontDict count]) + { + var fontlistString = ""; + var fontEnum; + var currFont; + var keyArray; + + keyArray = [fontDict allKeys]; + keyArray = [keyArray sortedArrayUsingSelector: @selector(compare:)]; + + fontEnum = [keyArray objectEnumerator]; + while ((currFont = [fontEnum nextObject]) !== nil) + { + var fontFamily; + var detail; + + if ([currFont isEqualToString: @"Symbol"]) + fontFamily = @"tech"; + else if ([currFont isEqualToString: @"Helvetica"]) + fontFamily = @"swiss"; + else if ([currFont isEqualToString: @"Arial"]) + fontFamily = @"swiss"; + else if ([currFont isEqualToString: @"Courier"]) + fontFamily = @"modern"; + else if ([currFont isEqualToString: @"Times"]) + fontFamily = @"roman"; + else fontFamily = @"nil"; + + detail = [CPString stringWithFormat: @"%@\\f%@ %@;", + [fontDict objectForKey: currFont], fontFamily, currFont]; + fontlistString += detail; + } + return [CPString stringWithFormat: @"{\\fonttbl%@}\n", fontlistString]; + } + else + return @""; +} + +- (CPString) colorTable +{ + // write Colour table + if ([colorDict count]) + { + var result, + count = [colorDict count], + list = [CPMutableArray arrayWithCapacity: count], + keyEnum = [colorDict keyEnumerator], + next, + i; + + while ((next = [keyEnum nextObject]) != nil) + { + var cn = [colorDict objectForKey: next]; + [list insertObject: next atIndex: [cn intValue]-1]; + } + + result = [CPString stringWithString: @"{\\colortbl;"]; + for (i = 0; i < count; i++) + { + var color = [[list objectAtIndex: i] + colorUsingColorSpaceName: CPCalibratedRGBColorSpace]; + result += [CPString stringWithFormat: + @"\\red%d\\green%d\\blue%d;", + ([color redComponent]*255), + ([color greenComponent]*255), + ([color blueComponent]*255)]; + } + + result += @"}\n"; + return result; + } + else + return @""; +} + +- (CPString) documentAttributes +{ + if (docDict != nil) + { + var result, + detail, + val, + num, + + result = [CPString string]; + + val = [docDict objectForKey: PAPERSIZE]; + if (val != nil) + { + var size = [val sizeValue]; + detail = [CPString stringWithFormat: @"\\paperw%d \\paperh%d", + _points2twips(size.width), + _points2twips(size.height)]; + result += detail; + } + + num = [docDict objectForKey: LEFTMARGIN]; + if (num != nil) + { + var f = [num floatValue]; + detail = [CPString stringWithFormat: @"\\margl%d", + _points2twips(f)]; + result+= detail; + } + num = [docDict objectForKey: RIGHTMARGIN]; + if (num != nil) + { + var f = [num floatValue]; + detail = [CPString stringWithFormat: @"\\margr%d", + _points2twips(f)]; + result += detail; + } + num = [docDict objectForKey: TOPMARGIN]; + if (num != nil) + { + var f = [num floatValue]; + detail = [CPString stringWithFormat: @"\\margt%d", + _points2twips(f)]; + result += detail; + } + num = [docDict objectForKey: BUTTOMMARGIN]; + if (num != nil) + { + var f = [num floatValue]; + detail = [CPString stringWithFormat: @"\\margb%d", + _points2twips(f)]; + result += detail; + } + + return result; + } + else + return @""; +} + +- (CPString) headerString +{ + var result; + + result = [CPString stringWithString: @"{\\rtf1\\ansi"]; + + result += [self fontTable]; + result += [self colorTable]; + result += [self documentAttributes]; + + return result; +} + +- (CPString) trailerString +{ + return @"}"; +} + +- (CPString)fontToken: (CPString) fontName +{ + var fCount = [fontDict objectForKey: fontName]; + + if (fCount == nil) + { + var count = [fontDict count]; + + fCount = [CPString stringWithFormat: @"\\f%d", count]; + [fontDict setObject: fCount forKey: fontName]; + } + + return fCount; +} + +- (int)numberForColor: (CPColor)color +{ + var cn, + num = [colorDict objectForKey: color]; + + if (num == nil) + { + cn = [colorDict count] + 1; + + [colorDict setObject: [CPNumber numberWithInt: cn] + forKey: color]; + } + var cn = [num intValue]; + + return cn + 1; +} + +- (CPString) paragraphStyle: (CPParagraphStyle) paraStyle +{ + var headerString = [CPString stringWithString:@"\\pard\\plain"], + twips; + + if (paraStyle == nil) + return headerString; + + switch ([paraStyle alignment]) + { + case CPRightTextAlignment: + headerString += @"\\qr"; + break; + case CPCenterTextAlignment: + headerString += @"\\qc"; + break; + case CPLeftTextAlignment: + headerString += @"\\ql"; + break; + case CPJustifiedTextAlignment: + headerString += @"\\qj"; + break; + default: + headerString += @"\\ql"; + break; + } + + // write first line indent and left indent + var twips = _points2twips([paraStyle firstLineHeadIndent]); + if (twips != 0.0) + { + headerString += [CPString stringWithFormat:@"\\fi%d", twips]; + } + twips = _points2twips([paraStyle headIndent]); + if (twips != 0.0) + { + headerString += [CPString stringWithFormat:@"\\li%d", twips]; + } + twips = _points2twips([paraStyle tailIndent]); + if (twips != 0.0) + { + headerString += [CPString stringWithFormat:@"\\ri%d", twips]; + } + twips = _points2twips([paraStyle paragraphSpacing]); + if (twips != 0.0) + { + headerString += [CPString stringWithFormat:@"\\sa%d", twips]; + } + twips = _points2twips([paraStyle minimumLineHeight]); + if (twips != 0.0) + { + headerString += [CPString stringWithFormat:@"\\sl%d", twips]; + } + twips = _points2twips([paraStyle maximumLineHeight]); + if (twips != 0.0) + { + headerString += [CPString stringWithFormat: @"\\sl-%d", twips]; + } +// tabs + if (1) + { + var enumerator, + tab; + + enumerator = [[paraStyle tabStops] objectEnumerator]; + while ((tab = [enumerator nextObject])) + { + switch ([tab tabStopType]) + { + case CPLeftTabStopType: + // no tabkind emission needed + break; +/* case NSRightTabStopType: + headerString += @"\\tqr"; + break; + case NSCenterTabStopType: + headerString += @"\\tqc"; + break; + case NSDecimalTabStopType: + headerString += @"\\tqdec"; + break; + default: + NSLog(@"Unknown tab stop type."); +*/ + } + + headerString += [CPString stringWithFormat:@"\\tx%d",_points2twips([tab location])]; + } + } + return headerString; +} + +- (CPString) runStringForString: (CPString) substring + attributes: (CPDictionary) attributes + paragraphStart: (BOOL) first +{ + var result = "", + headerString = "", + trailerString = "", + attribEnum, + currAttrib; + + if (first) + { + var paraStyle = [attributes objectForKey:CPParagraphStyleAttributeName]; + headerString += [self paragraphStyle: paraStyle]; + } + + /* + * analyze attributes of current run + * + * FIXME: All the character attributes should be output relative to the font + * attributes of the paragraph. So if the paragraph has underline on it should + * still be possible to switch it off for some characters, which currently is + * not possible. + */ + attribEnum = [attributes keyEnumerator]; + while ((currAttrib = [attribEnum nextObject]) != nil) + { + if ([currAttrib isEqualToString: CPFontAttributeName]) + { + /* + * handle fonts + */ + var font, + fontName, + traits; + + font = [attributes objectForKey: CPFontAttributeName]; + fontName = [font familyName]; + traits = [[CPFontManager sharedFontManager] traitsOfFont: font]; + + /* + * font name + */ + if (currentFont == nil || + ![fontName isEqualToString: [currentFont familyName]]) + { + headerString += [self fontToken: fontName]; + } + /* + * font size + */ + if (currentFont == nil || + [font size] != [currentFont size]) + { + var points =[font size]*2, + pString; + + pString = [CPString stringWithFormat: @"\\fs%d", points]; + headerString += pString; + } + /* + * font attributes + */ + if (traits & CPItalicFontMask) + { + headerString += @"\\i"; + trailerString += @"\\i0"; + } + if (traits & CPBoldFontMask) + { + headerString += @"\\b"; + trailerString += @"\\b0"; + } + + if (first) + currentFont = font; + } + else if ([currAttrib isEqualToString: CPForegroundColorAttributeName]) + { + var color = [attributes objectForKey: CPForegroundColorAttributeName]; + if (![color isEqual: fgColor]) + { + headerString += [CPString stringWithFormat:@"\\cf%d", [self numberForColor:color]]; + trailerString += @"\\cf0"; + } + } + else if ([currAttrib isEqualToString: CPBackgroundColorAttributeName]) + { + var color = [attributes objectForKey: CPBackgroundColorAttributeName]; + if (![color isEqual: bgColor]) + { + headerString += [CPString stringWithFormat:@"\\cb%d", [self numberForColor: color]]; + trailerString += @"\\cb0"; + } + } + else if ([currAttrib isEqualToString: CPUnderlineStyleAttributeName]) + { + headerString += @"\\ul"; + trailerString += @"\\ulnone"; + } + else if ([currAttrib isEqualToString: CPSuperscriptAttributeName]) + { + var value = [attributes objectForKey: CPSuperscriptAttributeName], + svalue = [value intValue] * 6; + + if (svalue > 0) + { + headerString += [CPString stringWithFormat:@"\\up%d", svalue]; + trailerString += @"\\up0"; + } + else if (svalue < 0) + { + headerString +=[CPString stringWithFormat:@"\\dn-%d", svalue]; + trailerString += @"\\dn0"; + } + } + else if ([currAttrib isEqualToString: CPBaselineOffsetAttributeName]) + { + var value = [attributes objectForKey: CPBaselineOffsetAttributeName], + svalue = [value floatValue] * 2; + + if (svalue > 0) + { + headerString +=[CPString stringWithFormat:@"\\up%d", svalue]; + trailerString += @"\\up0"; + } + else if (svalue < 0) + { + headerString += [CPString stringWithFormat:@"\\dn-%d", svalue]; + trailerString += @"\\dn0"; + } + } + else if ([currAttrib isEqualToString: CPAttachmentAttributeName]) + { + } + else if ([currAttrib isEqualToString: CPLigatureAttributeName]) + { + } + else if ([currAttrib isEqualToString: CPKernAttributeName]) + { + } + } + + substring = substring.replace(/\\/g, '\\\\'); + substring = substring.replace(/\n/g, '\\par\n'); + substring = substring.replace(/\t/g, '\\tab'); + substring = substring.replace(/{/g, '\\{'); + substring = substring.replace(/}/g, '\\}'); + // FIXME: All characters not in the standard encoding must be + // replaced by \'xx + + if (!first) + { + var braces; + + if ([headerString length]) + braces = [CPString stringWithFormat: @"{%@ %@}", headerString, substring]; + else + braces = substring; + + result += braces; + } + else + { + var nobraces; + + if ([headerString length]) + nobraces = [CPString stringWithFormat: @"%@ %@", headerString, substring]; + else + nobraces = substring; + + + result += nobraces; + } + + return result + trailerString; +} + +- (CPString)bodyString +{ + var string = [text string], + result = "", + loc = 0, + length = [string length]; + + var currRange = CPMakeRange(loc, 0), + completeRange = CPMakeRange(0, length), + first = YES; + +// FIXME split along newline characters and run as outer loop + while (CPMaxRange(currRange) < CPMaxRange(completeRange)) // save all "runs" + { + var attributes, + substring, + runString; + + attributes = [text attributesAtIndex: CPMaxRange(currRange) + longestEffectiveRange:currRange + inRange:completeRange]; + substring = [string substringWithRange:currRange]; + + runString = [self runStringForString:substring + attributes:attributes + paragraphStart:YES]; + result += runString; + first = NO; + } + return result; +} + + +- (CPString) RTFDStringFromAttributedString: (CPAttributedString)aText + documentAttributes: (CPDictionary)dict +{ + var output = [CPString string], + headerString, + trailerString, + bodyString; + + text = aText; + docDict = dict; + + /* + * do not change order! (esp. body has to be generated first; builds context) + */ + bodyString = [self bodyString]; + trailerString = [self trailerString]; + headerString = [self headerString]; + + output += headerString; + output += bodyString; + output += trailerString; + return output; +} +@end \ No newline at end of file