├── LICENSE ├── OEXTokenAttachmentCell.h ├── OEXTokenAttachmentCell.m ├── OEXTokenField.h ├── OEXTokenField.m ├── OEXTokenField.podspec ├── OEXTokenFieldCell.h ├── OEXTokenFieldCell.m ├── OEXTokenTextStorage.h ├── OEXTokenTextStorage.m └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | OEXTokenField and all code associated with it is distributed under the Revised BSD License, as listed below. 2 | 3 | Copyright (c) 2013 Octiplex. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | * Neither the name of Octiplex nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OCTIPLEX BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /OEXTokenAttachmentCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // OEXTokenAttachmentCell.h 3 | // OEXTokenField 4 | // 5 | // Created by Nicolas BACHSCHMIDT on 16/03/2013. 6 | // Copyright (c) 2013 Octiplex. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef NS_ENUM(NSUInteger, OEXTokenDrawingMode) { 12 | OEXTokenDrawingModeDefault, 13 | OEXTokenDrawingModeHighlighted, 14 | OEXTokenDrawingModeSelected, 15 | }; 16 | 17 | typedef NS_ENUM(NSUInteger, OEXTokenJoinStyle) { 18 | OEXTokenJoinStyleNone, 19 | OEXTokenJoinStyleLeft, 20 | OEXTokenJoinStyleRight, 21 | OEXTokenJoinStyleBoth, 22 | }; 23 | 24 | /** `OEXTokenAttachmentCell` is a subclass of `NSTextAttachmentCell` that provides methods for drawing text attachment tokens. 25 | 26 | `OEXTokenAttachmentCell` instances may be used to customize tokens in `` and ``. 27 | 28 | The default implementation draws tokens that look indentical to the standard ones. 29 | */ 30 | @interface OEXTokenAttachmentCell : NSTextAttachmentCell 31 | 32 | // TODO: Add documentation for the methods 33 | 34 | - (NSSize)cellSizeForTitleSize:(NSSize)titleSize; 35 | - (NSRect)titleRectForBounds:(NSRect)bounds; 36 | 37 | - (void)drawTokenWithFrame:(NSRect)rect inView:(NSView *)controlView; 38 | - (void)drawTitleWithFrame:(NSRect)rect inView:(NSView *)controlView; 39 | 40 | - (OEXTokenDrawingMode)tokenDrawingMode; 41 | - (OEXTokenJoinStyle)tokenJoinStyle; 42 | 43 | - (NSColor *)tokenFillColorForDrawingMode:(OEXTokenDrawingMode)drawingMode; 44 | - (NSColor *)tokenStrokeColorForDrawingMode:(OEXTokenDrawingMode)drawingMode; 45 | - (NSColor *)tokenTitleColorForDrawingMode:(OEXTokenDrawingMode)drawingMode; 46 | 47 | - (NSBezierPath *)tokenPathForBounds:(NSRect)bounds joinStyle:(OEXTokenJoinStyle)joinStyle; 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /OEXTokenAttachmentCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // OEXTokenAttachmentCell.m 3 | // OEXTokenField 4 | // 5 | // Created by Nicolas BACHSCHMIDT on 16/03/2013. 6 | // Copyright (c) 2013 Octiplex. All rights reserved. 7 | // 8 | 9 | #import "OEXTokenAttachmentCell.h" 10 | 11 | static CGFloat const kOEXTokenAttachmentTitleMargin = 11; 12 | static CGFloat const kOEXTokenAttachmentTokenMargin = 3; 13 | 14 | @implementation OEXTokenAttachmentCell 15 | { 16 | // Theses ivars are set at the begining of the draw 17 | OEXTokenDrawingMode _drawingMode; 18 | OEXTokenJoinStyle _joinStyle; 19 | } 20 | 21 | #pragma mark - Geometry 22 | 23 | - (NSPoint)cellBaselineOffset 24 | { 25 | return NSMakePoint(0, self.font.descender); 26 | } 27 | 28 | - (NSSize)cellSize 29 | { 30 | NSSize titleSize = [self.stringValue sizeWithAttributes:@{NSFontAttributeName:self.font}]; 31 | return [self cellSizeForTitleSize:titleSize]; 32 | } 33 | 34 | - (NSSize)cellSizeForTitleSize:(NSSize)titleSize 35 | { 36 | NSSize size = titleSize; 37 | // Add margins + height for the token rounded edges 38 | size.width += size.height + kOEXTokenAttachmentTitleMargin * 2; 39 | NSRect rect = {NSZeroPoint, size}; 40 | return NSIntegralRect(rect).size; 41 | } 42 | 43 | - (NSRect)titleRectForBounds:(NSRect)bounds 44 | { 45 | bounds.size.width = MAX(bounds.size.width, kOEXTokenAttachmentTitleMargin * 2 + bounds.size.height); 46 | return NSInsetRect(bounds, kOEXTokenAttachmentTitleMargin + bounds.size.height / 2, 0); 47 | } 48 | 49 | #pragma mark - Drawing 50 | 51 | - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView characterIndex:(NSUInteger)charIndex layoutManager:(NSLayoutManager *)layoutManager 52 | { 53 | _drawingMode = self.isHighlighted && controlView.window.isKeyWindow ? OEXTokenDrawingModeHighlighted : OEXTokenDrawingModeDefault; 54 | _joinStyle = OEXTokenJoinStyleNone; 55 | 56 | if ( [controlView respondsToSelector:@selector(selectedRanges)] ) 57 | { 58 | for ( NSValue *rangeValue in [(id) controlView selectedRanges] ) 59 | { 60 | NSRange range = rangeValue.rangeValue; 61 | if ( ! NSLocationInRange(charIndex, range) ) 62 | continue; 63 | 64 | if ( controlView.window.isKeyWindow ) 65 | _drawingMode = OEXTokenDrawingModeSelected; 66 | 67 | // TODO: RTL is not supported yet 68 | if ( range.location < charIndex ) 69 | _joinStyle |= OEXTokenJoinStyleLeft; 70 | if ( NSMaxRange(range) > charIndex + 1 ) 71 | _joinStyle |= OEXTokenJoinStyleRight; 72 | } 73 | } 74 | 75 | [self drawTokenWithFrame:cellFrame inView:controlView]; 76 | } 77 | 78 | - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView 79 | { 80 | [self drawWithFrame:cellFrame inView:controlView characterIndex:NSNotFound layoutManager:nil]; 81 | } 82 | 83 | - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView 84 | { 85 | [self drawWithFrame:cellFrame inView:controlView characterIndex:NSNotFound layoutManager:nil]; 86 | } 87 | 88 | - (void)drawTokenWithFrame:(NSRect)rect inView:(NSView *)controlView 89 | { 90 | [[NSGraphicsContext currentContext] saveGraphicsState]; 91 | 92 | NSColor *fillColor = [self tokenFillColorForDrawingMode:self.tokenDrawingMode]; 93 | NSColor *strokeColor = [self tokenStrokeColorForDrawingMode:self.tokenDrawingMode]; 94 | 95 | NSBezierPath *path = [self tokenPathForBounds:rect joinStyle:self.tokenJoinStyle]; 96 | [path addClip]; 97 | 98 | if ( fillColor ) { 99 | [fillColor setFill]; 100 | [path fill]; 101 | } 102 | 103 | if ( strokeColor ) { 104 | [strokeColor setStroke]; 105 | [path stroke]; 106 | } 107 | 108 | [self drawTitleWithFrame:[self titleRectForBounds:rect] inView:controlView]; 109 | [[NSGraphicsContext currentContext] restoreGraphicsState]; 110 | } 111 | 112 | - (void)drawTitleWithFrame:(NSRect)rect inView:(NSView *)controlView; 113 | { 114 | NSColor *textColor = [self tokenTitleColorForDrawingMode:self.tokenDrawingMode]; 115 | NSMutableParagraphStyle *style = [NSMutableParagraphStyle new]; 116 | style.lineBreakMode = NSLineBreakByTruncatingTail; 117 | [self.stringValue drawInRect:rect withAttributes:@{NSFontAttributeName:self.font, NSForegroundColorAttributeName:textColor, NSParagraphStyleAttributeName:style}]; 118 | } 119 | 120 | #pragma mark - State 121 | 122 | - (OEXTokenDrawingMode)tokenDrawingMode 123 | { 124 | return _drawingMode; 125 | } 126 | 127 | - (OEXTokenJoinStyle)tokenJoinStyle 128 | { 129 | return _joinStyle; 130 | } 131 | 132 | - (NSColor *)tokenFillColorForDrawingMode:(OEXTokenDrawingMode)drawingMode 133 | { 134 | // Those colors are Stolen From Apple 135 | switch ( drawingMode ) 136 | { 137 | case OEXTokenDrawingModeDefault: 138 | return [NSColor colorWithDeviceRed:0.8706 green:0.9059 blue:0.9725 alpha:1]; 139 | case OEXTokenDrawingModeHighlighted: 140 | return [NSColor colorWithDeviceRed:0.7330 green:0.8078 blue:0.9451 alpha:1]; 141 | case OEXTokenDrawingModeSelected: 142 | return [NSColor colorWithDeviceRed:0.3490 green:0.5451 blue:0.9255 alpha:1]; 143 | } 144 | } 145 | 146 | - (NSColor *)tokenStrokeColorForDrawingMode:(OEXTokenDrawingMode)drawingMode 147 | { 148 | // Those colors are Stolen From Apple 149 | switch ( drawingMode ) 150 | { 151 | case OEXTokenDrawingModeDefault: 152 | return [NSColor colorWithDeviceRed:0.6431 green:0.7412 blue:0.9255 alpha:1]; 153 | case OEXTokenDrawingModeHighlighted: 154 | return [NSColor colorWithDeviceRed:0.4275 green:0.5843 blue:0.8784 alpha:1]; 155 | case OEXTokenDrawingModeSelected: 156 | return [NSColor colorWithDeviceRed:0.3490 green:0.5451 blue:0.9255 alpha:1]; 157 | } 158 | } 159 | 160 | - (NSColor *)tokenTitleColorForDrawingMode:(OEXTokenDrawingMode)drawingMode 161 | { 162 | switch ( drawingMode ) 163 | { 164 | case OEXTokenDrawingModeDefault: 165 | case OEXTokenDrawingModeHighlighted: 166 | return [NSColor controlTextColor]; 167 | case OEXTokenDrawingModeSelected: 168 | return [NSColor alternateSelectedControlTextColor]; 169 | } 170 | } 171 | 172 | #pragma mark - Geometry 173 | 174 | - (NSBezierPath *)tokenPathForBounds:(NSRect)bounds joinStyle:(OEXTokenJoinStyle)jointStyle 175 | { 176 | bounds.size.width = MAX(bounds.size.width, kOEXTokenAttachmentTokenMargin * 2 + bounds.size.height); 177 | 178 | CGFloat radius = bounds.size.height / 2; 179 | CGRect innerRect = NSInsetRect(bounds, kOEXTokenAttachmentTokenMargin + radius, 0); 180 | CGFloat x0 = NSMinX(bounds); 181 | CGFloat x1 = NSMinX(innerRect); 182 | CGFloat x2 = NSMaxX(innerRect); 183 | CGFloat x3 = NSMaxX(bounds); 184 | CGFloat minY = NSMinY(bounds); 185 | CGFloat maxY = NSMaxY(bounds); 186 | CGFloat midY = NSMidY(bounds); 187 | 188 | NSBezierPath *path = [NSBezierPath new]; 189 | [path moveToPoint:NSMakePoint(x1, minY)]; 190 | 191 | // Left edge 192 | if ( jointStyle & OEXTokenJoinStyleLeft ) { 193 | [path lineToPoint:NSMakePoint(x0, minY)]; 194 | [path lineToPoint:NSMakePoint(x0, maxY)]; 195 | [path lineToPoint:NSMakePoint(x1, maxY)]; 196 | } 197 | else { 198 | // Degrees?! O_o 199 | [path appendBezierPathWithArcWithCenter:NSMakePoint(x1, midY) radius:radius startAngle:-90 endAngle:90 clockwise:YES]; 200 | } 201 | 202 | // Top edge 203 | [path lineToPoint:NSMakePoint(x2, maxY)]; 204 | 205 | // Right edge 206 | if ( jointStyle & OEXTokenJoinStyleRight ) { 207 | [path lineToPoint:NSMakePoint(x3, maxY)]; 208 | [path lineToPoint:NSMakePoint(x3, minY)]; 209 | [path lineToPoint:NSMakePoint(x2, minY)]; 210 | } 211 | else { 212 | [path appendBezierPathWithArcWithCenter:NSMakePoint(x2, midY) radius:radius startAngle:90 endAngle:-90 clockwise:YES]; 213 | } 214 | 215 | // Bottom edge 216 | [path lineToPoint:NSMakePoint(x1, minY)]; 217 | [path closePath]; 218 | 219 | // As we'll clip to the path, let's double the desired line width (1) 220 | [path setLineWidth:2]; 221 | 222 | return path; 223 | } 224 | 225 | @end 226 | -------------------------------------------------------------------------------- /OEXTokenField.h: -------------------------------------------------------------------------------- 1 | // 2 | // OEXTokenField.h 3 | // OEXTokenField 4 | // 5 | // Created by Nicolas BACHSCHMIDT on 16/03/2013. 6 | // Copyright (c) 2013 Octiplex. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol OEXTokenFieldDelegate; 12 | 13 | /** `OEXTokenField` is a subclass of `NSTokenField` that allows token customization. 14 | 15 | `OEXTokenField` uses an `` to implement much of the control's functionality. 16 | */ 17 | @interface OEXTokenField : NSTokenField 18 | 19 | /** @name Accessing the Delegate */ 20 | 21 | /** The token field's delegate. 22 | @discussion The delegate must adopt the `` protocol. 23 | */ 24 | @property(nonatomic, assign) id delegate; 25 | 26 | @end 27 | 28 | #pragma mark - 29 | 30 | /** The `OEXTokenFieldDelegate` protocol defines the optional methods implemented by delegates of `` objects. 31 | */ 32 | @protocol OEXTokenFieldDelegate 33 | 34 | @optional 35 | 36 | /** @name Displaying Tokenized Attachment Cells */ 37 | 38 | /** Allows the delegate to provide an attachment cell to be displayed for the given represented object. 39 | @param tokenField The token field that sent the message. 40 | @param representedObject A represented object of the token field. 41 | @return The attachment cell to be displayed for `representedObject`. If you return `nil` or do not implement this method, then a standard token is displayed. 42 | */ 43 | - (NSTextAttachmentCell *)tokenField:(OEXTokenField *)tokenField attachmentCellForRepresentedObject:(id)representedObject; 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /OEXTokenField.m: -------------------------------------------------------------------------------- 1 | // 2 | // OEXTokenField.m 3 | // OEXTokenField 4 | // 5 | // Created by Nicolas BACHSCHMIDT on 16/03/2013. 6 | // Copyright (c) 2013 Octiplex. All rights reserved. 7 | // 8 | 9 | #import "OEXTokenField.h" 10 | #import "OEXTokenFieldCell.h" 11 | 12 | @interface OEXTokenField () 13 | @end 14 | 15 | @implementation OEXTokenField 16 | 17 | @dynamic delegate; 18 | 19 | - (id)initWithCoder:(NSCoder *)aDecoder 20 | { 21 | if ( ! (self = [super initWithCoder:aDecoder]) ) 22 | return nil; 23 | 24 | // Changing the cell's class in the XIB is easy to forget 25 | // Let's make sure we have a OEXTokenFieldCell 26 | 27 | if ( ! [self.cell isKindOfClass:[OEXTokenFieldCell class]] ) 28 | { 29 | NSMutableData *data = [NSMutableData new]; 30 | NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; 31 | [self.cell encodeWithCoder:archiver]; 32 | [archiver finishEncoding]; 33 | self.cell = [[OEXTokenFieldCell alloc] initWithCoder:[[NSKeyedUnarchiver alloc] initForReadingWithData:data]]; 34 | } 35 | return self; 36 | } 37 | 38 | + (Class)cellClass 39 | { 40 | return [OEXTokenFieldCell class]; 41 | } 42 | 43 | - (NSTextAttachmentCell *)tokenFieldCell:(OEXTokenFieldCell *)tokenFieldCell attachmentCellForRepresentedObject:(id)representedObject 44 | { 45 | if ( [self.delegate respondsToSelector:@selector(tokenField:attachmentCellForRepresentedObject:)] ) 46 | return [self.delegate tokenField:self attachmentCellForRepresentedObject:representedObject]; 47 | 48 | return nil; 49 | } 50 | 51 | #pragma mark - NSTextViewDelegate 52 | 53 | - (void)textView:(NSTextView *)aTextView clickedOnCell:(id )cell inRect:(NSRect)cellFrame atIndex:(NSUInteger)charIndex 54 | { 55 | [aTextView setSelectedRange:NSMakeRange(charIndex, 1)]; 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /OEXTokenField.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "OEXTokenField" 3 | s.version = "0.0.1" 4 | s.summary = "A subclass of NSTokenField that allows token customization." 5 | s.homepage = "http://www.octiplex.com/" 6 | s.license = "Modified BSD License" 7 | 8 | s.author = { "Octiplex" => "contact@octiplex" } 9 | s.source = { :git => "https://github.com/octiplex/OEXTokenField.git" } 10 | 11 | s.platform = :osx, "10.7" 12 | s.source_files = "*.{m,h}" 13 | s.public_header_files = [ "OEXTokenAttachmentCell.h", "OEXTokenField.h", "OEXTokenFieldCell.h" ] 14 | s.requires_arc = true 15 | end 16 | -------------------------------------------------------------------------------- /OEXTokenFieldCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // OEXTokenFieldCell.h 3 | // OEXTokenField 4 | // 5 | // Created by Nicolas BACHSCHMIDT on 16/03/2013. 6 | // Copyright (c) 2013 Octiplex. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol OEXTokenFieldCellDelegate; 12 | 13 | /** `OEXTokenFieldCell` is a subclass of `NSTokenFieldCell` that allows token customization. 14 | */ 15 | @interface OEXTokenFieldCell : NSTokenFieldCell 16 | 17 | /** @name Accessing the Delegate */ 18 | 19 | /** The token field cell's delegate. 20 | @discussion The delegate must adopt the `` protocol. 21 | */ 22 | @property(nonatomic, assign) id delegate; 23 | 24 | /** @name Displaying Tokenized Attachment Cells */ 25 | 26 | /** Returns the attachment cell to be displayed for the given represented object. 27 | @param representedObject A represented object of the receiver. 28 | @return The attachment cell to be displayed for `representedObject`. 29 | @discussion The default implementation invokes `<[OEXTokenFieldCellDelegate tokenFieldCell:attachmentCellForRepresentedObject:]>` on the receiver's delegate if the method is implemented. Otherwise it returns `nil`. 30 | */ 31 | - (NSTextAttachmentCell *)attachmentCellForRepresentedObject:(id)representedObject; 32 | 33 | @end 34 | 35 | #pragma mark - 36 | 37 | /** The `OEXTokenFieldDelegate` protocol defines the optional methods implemented by delegates of `` objects. 38 | */ 39 | @protocol OEXTokenFieldCellDelegate 40 | 41 | @optional 42 | 43 | /** @name Displaying Tokenized Attachment Cells */ 44 | 45 | /** Allows the delegate to provide an attachment cell to be displayed for the given represented object. 46 | @param tokenFieldCell The token field cell that sent the message. 47 | @param representedObject A represented object of the token field cell. 48 | @return The attachment cell to be displayed for `representedObject`. If you return `nil` or do not implement this method, a standard token is displayed. 49 | */ 50 | - (NSTextAttachmentCell *)tokenFieldCell:(OEXTokenFieldCell *)tokenFieldCell attachmentCellForRepresentedObject:(id)representedObject; 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /OEXTokenFieldCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // OEXTokenFieldCell.m 3 | // OEXTokenField 4 | // 5 | // Created by Nicolas BACHSCHMIDT on 16/03/2013. 6 | // Copyright (c) 2013 Octiplex. All rights reserved. 7 | // 8 | 9 | #import "OEXTokenFieldCell.h" 10 | #import "OEXTokenTextStorage.h" 11 | 12 | #import 13 | 14 | static int kOEXTokenFieldCellRepresentedObjectKey; 15 | 16 | @interface OEXTokenFieldCell () 17 | @end 18 | 19 | @implementation OEXTokenFieldCell 20 | { 21 | NSTokenFieldCell *_tokenCell; 22 | NSArray *_objects; 23 | } 24 | 25 | @dynamic delegate; 26 | 27 | #pragma mark - Attributed String Value 28 | 29 | - (NSAttributedString *)attributedStringValue 30 | { 31 | // The token cells are drawn using text attachments 32 | // Replace attachment cells before displaying the string 33 | 34 | NSAttributedString *attrString = super.attributedStringValue; 35 | NSRange range = NSMakeRange(0, attrString.length); 36 | [attrString enumerateAttribute:NSAttachmentAttributeName inRange:range options:0 usingBlock:^(NSTextAttachment *attachment, NSRange range, BOOL *stop) { 37 | if ( attachment ) { 38 | [self updateTokenAttachment:attachment forAttributedString:[attrString attributedSubstringFromRange:range]]; 39 | } 40 | }]; 41 | return attrString; 42 | } 43 | 44 | - (void)setAttributedStringValue:(NSAttributedString *)attrString 45 | { 46 | // The default implementation of setAttributedString: cannot handle attachments with replaced cells 47 | // Transform the attributed string to array of represented objects 48 | 49 | NSMutableArray *objects = [NSMutableArray new]; 50 | NSRange range = NSMakeRange(0, attrString.length); 51 | [attrString enumerateAttribute:NSAttachmentAttributeName inRange:range options:0 usingBlock:^(id attachment, NSRange range, BOOL *stop) { 52 | id representedObject = [self representedObjectWithAttachment:attachment attributedString:[attrString attributedSubstringFromRange:range]]; 53 | [objects addObject:representedObject]; 54 | }]; 55 | 56 | [self setObjectValue:objects]; 57 | } 58 | 59 | #pragma mark - Field Editor 60 | 61 | - (NSText *)setUpFieldEditorAttributes:(NSText *)textObj 62 | { 63 | // Replace the text storage of the text view so we can replace attachment cells on the fly 64 | 65 | NSTextView *textView = (NSTextView *) [super setUpFieldEditorAttributes:textObj]; 66 | 67 | if ( [textView isKindOfClass:[NSTextView class]] ) 68 | { 69 | NSLayoutManager *layoutManager = textView.textContainer.layoutManager; 70 | OEXTokenTextStorage *textStorage = (OEXTokenTextStorage *) layoutManager.textStorage; 71 | 72 | if ( ! [textStorage isKindOfClass:[OEXTokenTextStorage class]] ) { 73 | textStorage = [[OEXTokenTextStorage alloc] initWithAttributedString:textStorage]; 74 | [layoutManager replaceTextStorage:textStorage]; 75 | } 76 | 77 | textStorage.delegate = self; 78 | } 79 | 80 | return textView; 81 | } 82 | 83 | - (void)endEditing:(NSText *)textObj 84 | { 85 | NSTextView *textView = (NSTextView *) textObj; 86 | if ( [textView isKindOfClass:[NSTextView class]] ) 87 | { 88 | OEXTokenTextStorage *textStorage = (OEXTokenTextStorage *) textView.textContainer.layoutManager.textStorage; 89 | if ( [textStorage isKindOfClass:[OEXTokenTextStorage class]] ) { 90 | textStorage.delegate = nil; 91 | } 92 | } 93 | [super endEditing:textObj]; 94 | } 95 | 96 | #pragma mark - Token Replacement 97 | 98 | - (void)updateTokenAttachment:(NSTextAttachment *)attachment forAttributedString:(NSAttributedString *)attrString 99 | { 100 | // If the represented object in set, we've already updated the attachment 101 | if ( objc_getAssociatedObject(attachment, &kOEXTokenFieldCellRepresentedObjectKey) ) 102 | return; 103 | 104 | id representedObject = [self representedObjectWithAttachment:attachment attributedString:attrString]; 105 | objc_setAssociatedObject(attachment, &kOEXTokenFieldCellRepresentedObjectKey, representedObject, OBJC_ASSOCIATION_RETAIN); 106 | 107 | // Replace the attachment's cell 108 | id cell = attachment.attachmentCell; 109 | cell = [self attachmentCellForRepresentedObject:representedObject] ?: cell; 110 | [cell setAttachment:attachment]; 111 | [attachment setAttachmentCell:cell]; 112 | } 113 | 114 | - (NSTextAttachmentCell *)attachmentCellForRepresentedObject:(id)representedObject; 115 | { 116 | NSTextAttachmentCell *cell = nil; 117 | if ( [self.delegate respondsToSelector:@selector(tokenFieldCell:attachmentCellForRepresentedObject:)] ) { 118 | cell = [self.delegate tokenFieldCell:self attachmentCellForRepresentedObject:representedObject]; 119 | } 120 | 121 | cell.font = self.font; 122 | return cell; 123 | } 124 | 125 | - (id)representedObjectWithAttachment:(NSTextAttachment *)attachment attributedString:(NSAttributedString *)attrString 126 | { 127 | // If the attachment was updated, we just need to access the associated object 128 | if ( attachment && objc_getAssociatedObject(attachment, &kOEXTokenFieldCellRepresentedObjectKey) ) 129 | return objc_getAssociatedObject(attachment, &kOEXTokenFieldCellRepresentedObjectKey); 130 | 131 | // This attributed string was generated by NSTokenField 132 | // As we don't want to rely on private APIs (NSTokenAttachment/NSTokenAttachmentCell), let's just use a NSTokenFieldCell to do the job 133 | 134 | if ( ! _tokenCell ) 135 | _tokenCell = [NSTokenFieldCell new]; 136 | 137 | _tokenCell.attributedStringValue = attrString; 138 | NSArray *objectValue = _tokenCell.objectValue; 139 | return objectValue.count ? objectValue[0] : attrString.string; 140 | } 141 | 142 | #pragma mark - OEXTokenTextStorageDelegate 143 | 144 | - (void)tokenTextStorage:(OEXTokenTextStorage *)textStorage updateTokenAttachment:(NSTextAttachment *)attachment forRange:(NSRange)range 145 | { 146 | [self updateTokenAttachment:attachment forAttributedString:[textStorage attributedSubstringFromRange:range]]; 147 | } 148 | 149 | @end 150 | -------------------------------------------------------------------------------- /OEXTokenTextStorage.h: -------------------------------------------------------------------------------- 1 | // 2 | // OEXTokenTextStorage.h 3 | // OEXTokenField 4 | // 5 | // Created by Nicolas BACHSCHMIDT on 16/03/2013. 6 | // Copyright (c) 2013 Octiplex. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol OEXTokenTextStorageDelegate; 12 | 13 | /*- OEXTokenTextStorage is used internally by OEXTokenFieldCell to perform attachment cell replacement as the tokens are inserted in the editor text view. 14 | */ 15 | @interface OEXTokenTextStorage : NSTextStorage 16 | 17 | @property(nonatomic, weak) id delegate; 18 | 19 | - (id)initWithAttributedString:(NSAttributedString *)attrStr; 20 | 21 | @end 22 | 23 | #pragma mark - 24 | 25 | @protocol OEXTokenTextStorageDelegate 26 | 27 | @optional 28 | - (void)tokenTextStorage:(OEXTokenTextStorage *)textStorage updateTokenAttachment:(NSTextAttachment *)attachment forRange:(NSRange)range; 29 | 30 | @end -------------------------------------------------------------------------------- /OEXTokenTextStorage.m: -------------------------------------------------------------------------------- 1 | // 2 | // OEXTokenTextStorage.m 3 | // OEXTokenField 4 | // 5 | // Created by Nicolas BACHSCHMIDT on 16/03/2013. 6 | // Copyright (c) 2013 Octiplex. All rights reserved. 7 | // 8 | 9 | #import "OEXTokenTextStorage.h" 10 | 11 | @implementation OEXTokenTextStorage 12 | { 13 | NSMutableAttributedString *_string; 14 | } 15 | 16 | #pragma mark - init 17 | 18 | - (id)initWithAttributedString:(NSAttributedString *)attrStr 19 | { 20 | if ( ! (self = [super init]) ) 21 | return nil; 22 | 23 | _string = [[NSMutableAttributedString alloc] initWithAttributedString:attrStr]; 24 | return self; 25 | } 26 | 27 | - (id)init 28 | { 29 | return [self initWithAttributedString:nil]; 30 | } 31 | 32 | #pragma mark - Primitive Methods 33 | 34 | - (NSString *)string 35 | { 36 | return [_string string]; 37 | } 38 | 39 | - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range 40 | { 41 | return [_string attributesAtIndex:location effectiveRange:range]; 42 | } 43 | 44 | - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str 45 | { 46 | [_string replaceCharactersInRange:range withString:str]; 47 | [self edited:NSTextStorageEditedCharacters range:range changeInLength:str.length - range.length]; 48 | } 49 | 50 | - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range 51 | { 52 | [_string setAttributes:attrs range:range]; 53 | NSTextAttachment *attachment = attrs[NSAttachmentAttributeName]; 54 | if ( attachment && [_delegate respondsToSelector:@selector(tokenTextStorage:updateTokenAttachment:forRange:)] ) 55 | [_delegate tokenTextStorage:self updateTokenAttachment:attachment forRange:range]; 56 | [self edited:NSTextStorageEditedAttributes range:range changeInLength:0]; 57 | } 58 | 59 | #pragma mark - Convenience Methods 60 | 61 | - (void)removeAttribute:(NSString *)name range:(NSRange)range 62 | { 63 | [_string removeAttribute:name range:range]; 64 | [self edited:NSTextStorageEditedAttributes range:range changeInLength:0]; 65 | } 66 | 67 | - (void)replaceCharactersInRange:(NSRange)range withAttributedString:(NSAttributedString *)attrString 68 | { 69 | [_string replaceCharactersInRange:range withAttributedString:attrString]; 70 | NSRange strRange = NSMakeRange(range.location, attrString.length); 71 | 72 | [_string enumerateAttribute:NSAttachmentAttributeName inRange:strRange options:0 usingBlock:^(NSTextAttachment *attachment, NSRange range, BOOL *stop) { 73 | if ( attachment && [_delegate respondsToSelector:@selector(tokenTextStorage:updateTokenAttachment:forRange:)] ) { 74 | [_delegate tokenTextStorage:self updateTokenAttachment:attachment forRange:range]; 75 | } 76 | }]; 77 | [self edited:NSTextStorageEditedAttributes | NSTextStorageEditedCharacters range:range changeInLength:strRange.length - range.length]; 78 | } 79 | 80 | @end 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | `OEXTokenField` is a subclass of `NSTokenField` that allows token customization. 4 | 5 | ## How to use 6 | 7 | `NSTokenField` tokens rely on text attachments, which are drawn by instances of `NSTextAttachmentCell`. `OEXTokenField` works by subsituting the default text attachment cells as they are inserted with cells provided by its delegate. 8 | 9 | In order to provide custom token cells, you just need to implement the following delegation method: 10 | 11 | - (NSTextAttachmentCell *)tokenField:(OEXTokenField *)tokenField attachmentCellForRepresentedObject:(id)representedObject; 12 | 13 | Any instance of `NSTextAttachmentCell` is fine. However, if you want to display tokens that look identical or similar to AppKit tokens, you may use and subclass the `OEXTokenAttachmentCell` class. 14 | 15 | ## Known issues 16 | 17 | For the moment, `OEXTokenField` doesn't provide menus for represented objects that display custom attachment cells. 18 | 19 | ## Source Code 20 | 21 | The OEXTokenField code is available from GitHub: 22 | 23 | https://github.com/octiplex/OEXTokenField.git 24 | 25 | ## License 26 | 27 | OEXTokenField is made available under the Revised BSD License. See the `LICENSE` file for more info. --------------------------------------------------------------------------------