From fb696efd169db17e8169d0d0cfd1caf0728c0722 Mon Sep 17 00:00:00 2001 From: "gtm.daemon" Date: Fri, 18 Mar 2011 21:00:21 +0000 Subject: [Author: sail] Currently GTMFadeTruncatingTextFieldCell only supports truncating the tail of a string. It should also allow the head of a string to be truncated. I'd like to use this in Chromium. See http://crbug.com/69304 R=thomasvl APPROVED=thomasvl DELTA=238 (197 added, 17 deleted, 24 changed) --- AppKit/GTMFadeTruncatingTextFieldCell.h | 22 ++- AppKit/GTMFadeTruncatingTextFieldCell.m | 173 +++++++++++++++++---- AppKit/GTMFadeTruncatingTextFieldCellTest.m | 36 ++++- .../GTMFadeTruncatingTextFieldCellTest5.tiff | Bin 0 -> 2188 bytes .../GTMFadeTruncatingTextFieldCellTest6.tiff | Bin 0 -> 2068 bytes .../GTMFadeTruncatingTextFieldCellTest7.tiff | Bin 0 -> 2222 bytes GTM.xcodeproj/project.pbxproj | 12 ++ 7 files changed, 210 insertions(+), 33 deletions(-) create mode 100644 AppKit/TestData/GTMFadeTruncatingTextFieldCellTest5.tiff create mode 100644 AppKit/TestData/GTMFadeTruncatingTextFieldCellTest6.tiff create mode 100644 AppKit/TestData/GTMFadeTruncatingTextFieldCellTest7.tiff diff --git a/AppKit/GTMFadeTruncatingTextFieldCell.h b/AppKit/GTMFadeTruncatingTextFieldCell.h index 76e7b1c..267d4b8 100644 --- a/AppKit/GTMFadeTruncatingTextFieldCell.h +++ b/AppKit/GTMFadeTruncatingTextFieldCell.h @@ -21,8 +21,26 @@ #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 -// A simple text field cell that truncates the right hand edge using a gradient -@interface GTMFadeTruncatingTextFieldCell : NSTextFieldCell +typedef enum { + GTMFadeTruncatingTail, + GTMFadeTruncatingHead, + GTMFadeTruncatingHeadAndTail, +} GTMFadeTruncateMode; + +// A simple text field cell that can truncate at the beginning or the end +// using a gradient. By default it truncates the end. +@interface GTMFadeTruncatingTextFieldCell : NSTextFieldCell { + @private + NSUInteger desiredCharactersToTruncateFromHead_; + GTMFadeTruncateMode truncateMode_; +} + +@property (nonatomic) GTMFadeTruncateMode truncateMode; + +// When truncating the head this specifies the maximum number of characters +// that can be truncated. Setting this to 0 means that there is no maximum. +@property (nonatomic) NSUInteger desiredCharactersToTruncateFromHead; + @end #endif diff --git a/AppKit/GTMFadeTruncatingTextFieldCell.m b/AppKit/GTMFadeTruncatingTextFieldCell.m index 0e7344c..ec266a2 100644 --- a/AppKit/GTMFadeTruncatingTextFieldCell.m +++ b/AppKit/GTMFadeTruncatingTextFieldCell.m @@ -34,54 +34,167 @@ return self; } +- (void)drawTextGradientPart:(NSAttributedString *)attributedString + titleRect:(NSRect)titleRect + backgroundRect:(NSRect)backgroundRect + clipRect:(NSRect)clipRect + mask:(NSGradient *)mask + fadeToRight:(BOOL)fadeToRight { + // Draw the gradient part with a transparency layer. This makes the text look + // suboptimal, but since it fades out, that's ok. + [[NSGraphicsContext currentContext] saveGraphicsState]; + [NSBezierPath clipRect:clipRect]; + CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; + CGContextBeginTransparencyLayerWithRect(context, + NSRectToCGRect(clipRect), 0); + + if ([self drawsBackground]) { + [[self backgroundColor] set]; + NSRectFillUsingOperation([self titleRectForBounds:backgroundRect], + NSCompositeSourceOver); + } + [attributedString drawInRect:titleRect]; + + NSPoint startPoint; + NSPoint endPoint; + if (fadeToRight) { + startPoint = clipRect.origin; + endPoint = NSMakePoint(NSMaxX(clipRect), NSMinY(clipRect)); + } else { + startPoint = NSMakePoint(NSMaxX(clipRect), NSMinY(clipRect)); + endPoint = clipRect.origin; + } + + // Draw the gradient mask + CGContextSetBlendMode(context, kCGBlendModeDestinationIn); + [mask drawFromPoint:startPoint + toPoint:endPoint + options:fadeToRight ? NSGradientDrawsBeforeStartingLocation : + NSGradientDrawsAfterEndingLocation]; + + CGContextEndTransparencyLayer(context); + [[NSGraphicsContext currentContext] restoreGraphicsState]; +} + - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { - NSSize size = [[self attributedStringValue] size]; + NSRect titleRect = [self titleRectForBounds:cellFrame]; + // For some reason the title rect is too far to the left. + titleRect.origin.x += 2; + titleRect.size.width -= 2; + + NSAttributedString *attributedString = [self attributedStringValue]; + NSSize size = [attributedString size]; // Don't complicate drawing unless we need to clip - if (size.width <= NSWidth(cellFrame)) { + if (size.width <= NSWidth(titleRect)) { [super drawInteriorWithFrame:cellFrame inView:controlView]; return; } + // Clip the string by drawing it to the left by |offsetX|. + CGFloat offsetX = 0; + switch (truncateMode_) { + case GTMFadeTruncatingTail: + break; + case GTMFadeTruncatingHead: + offsetX = size.width - titleRect.size.width; + break; + case GTMFadeTruncatingHeadAndTail: { + if (desiredCharactersToTruncateFromHead_ > 0) { + NSAttributedString *clippedHeadString = + [attributedString attributedSubstringFromRange: + NSMakeRange(0, desiredCharactersToTruncateFromHead_)]; + NSSize clippedHeadSize = [clippedHeadString size]; + + // Clip the desired portion from the beginning of the string. + offsetX = clippedHeadSize.width; + + CGFloat delta = size.width - titleRect.size.width; + if (offsetX > delta) + offsetX = delta; + } else { + // Center the string and clip equal portions of the head and tail. + offsetX = round((size.width - titleRect.size.width) / 2.0); + } + break; + } + } + + NSRect offsetTitleRect = titleRect; + offsetTitleRect.origin.x -= offsetX; + offsetTitleRect.size.width += offsetX; + BOOL isTruncatingHead = offsetX > 0; + BOOL isTruncatingTail = (size.width - titleRect.size.width) > offsetX; + // Gradient is about twice our line height long - CGFloat gradientWidth = MIN(size.height * 2, NSWidth(cellFrame) / 4); + CGFloat gradientWidth = MIN(size.height * 2, round(NSWidth(cellFrame) / 4)); + NSRect solidPart = cellFrame; + NSRect headGradientPart = NSZeroRect; + NSRect tailGradientPart = NSZeroRect; + if (isTruncatingHead) + NSDivideRect(solidPart, &headGradientPart, &solidPart, + gradientWidth, NSMinXEdge); + if (isTruncatingTail) + NSDivideRect(solidPart, &tailGradientPart, &solidPart, + gradientWidth, NSMaxXEdge); - NSRect solidPart, gradientPart; - NSDivideRect(cellFrame, &gradientPart, &solidPart, gradientWidth, NSMaxXEdge); - // Draw non-gradient part without transparency layer, as light text on a dark // background looks bad with a gradient layer. + NSRect backgroundRect = [self drawingRectForBounds:cellFrame]; [[NSGraphicsContext currentContext] saveGraphicsState]; [NSBezierPath clipRect:solidPart]; - [super drawInteriorWithFrame:cellFrame inView:controlView]; + if ([self drawsBackground]) { + [[self backgroundColor] set]; + NSRectFillUsingOperation(backgroundRect, NSCompositeSourceOver); + } + // We draw the text ourselves because [super drawInteriorWithFrame:inView:] + // doesn't draw correctly if the cell draws its own background. + [attributedString drawInRect:offsetTitleRect]; [[NSGraphicsContext currentContext] restoreGraphicsState]; - // Draw the gradient part with a transparency layer. This makes the text look - // suboptimal, but since it fades out, that's ok. - [[NSGraphicsContext currentContext] saveGraphicsState]; - [NSBezierPath clipRect:gradientPart]; - CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; - CGContextBeginTransparencyLayerWithRect(context, - NSRectToCGRect(gradientPart), 0); - - [super drawInteriorWithFrame:cellFrame inView:controlView]; + NSColor *startColor = [self textColor];; + NSColor *endColor = [startColor colorWithAlphaComponent:0.0]; + NSGradient *mask = [[NSGradient alloc] initWithStartingColor:startColor + endingColor:endColor]; - // TODO(alcor): switch this to GTMLinearRGBShading if we ever need on 10.4 - NSColor *color = [self textColor]; - NSColor *alphaColor = [color colorWithAlphaComponent:0.0]; - NSGradient *mask = [[NSGradient alloc] initWithStartingColor:color - endingColor:alphaColor]; + if (isTruncatingHead) + [self drawTextGradientPart:attributedString + titleRect:offsetTitleRect + backgroundRect:backgroundRect + clipRect:headGradientPart + mask:mask + fadeToRight:NO]; + if (isTruncatingTail) + [self drawTextGradientPart:attributedString + titleRect:offsetTitleRect + backgroundRect:backgroundRect + clipRect:tailGradientPart + mask:mask + fadeToRight:YES]; - // Draw the gradient mask - CGContextSetBlendMode(context, kCGBlendModeDestinationIn); - [mask drawFromPoint:NSMakePoint(NSMaxX(cellFrame) - gradientWidth, - NSMinY(cellFrame)) - toPoint:NSMakePoint(NSMaxX(cellFrame), - NSMinY(cellFrame)) - options:NSGradientDrawsBeforeStartingLocation]; [mask release]; - CGContextEndTransparencyLayer(context); - [[NSGraphicsContext currentContext] restoreGraphicsState]; +} + +- (void)setTruncateMode:(GTMFadeTruncateMode)mode { + if (truncateMode_ != mode) { + truncateMode_ = mode; + [[self controlView] setNeedsDisplay:YES]; + } +} + +- (GTMFadeTruncateMode)truncateMode { + return truncateMode_; +} + +- (void)setDesiredCharactersToTruncateFromHead:(NSUInteger)length { + if (desiredCharactersToTruncateFromHead_ != length) { + desiredCharactersToTruncateFromHead_ = length; + [[self controlView] setNeedsDisplay:YES]; + } +} + +- (NSUInteger)desiredCharactersToTruncateFromHead { + return desiredCharactersToTruncateFromHead_; } @end diff --git a/AppKit/GTMFadeTruncatingTextFieldCellTest.m b/AppKit/GTMFadeTruncatingTextFieldCellTest.m index a42e22e..a1bf937 100644 --- a/AppKit/GTMFadeTruncatingTextFieldCellTest.m +++ b/AppKit/GTMFadeTruncatingTextFieldCellTest.m @@ -28,7 +28,7 @@ @implementation GTMFadeTruncatingTextFieldCellTest -- (void)testFadeCell { +- (void)testFadeCellRight { NSTextField *field = [[[NSTextField alloc] initWithFrame: NSMakeRect(0, 0, 100, 16)] autorelease]; [field setCell:[[[GTMFadeTruncatingTextFieldCell alloc] initTextCell:@""] @@ -38,6 +38,7 @@ GTMAssertObjectImageEqualToImageNamed(field, @"GTMFadeTruncatingTextFieldCellTest1", nil); + [field setStringValue:@"A short string"]; GTMAssertObjectImageEqualToImageNamed(field, @"GTMFadeTruncatingTextFieldCellTest2", @@ -60,6 +61,39 @@ nil); } +- (void)testFadeCellLeftAndRight { + NSTextField *field = [[[NSTextField alloc] initWithFrame: + NSMakeRect(0, 0, 100, 16)] autorelease]; + GTMFadeTruncatingTextFieldCell *cell = + [[[GTMFadeTruncatingTextFieldCell alloc] initTextCell:@""] autorelease]; + [cell setTruncateMode:GTMFadeTruncatingHeadAndTail]; + [cell setDesiredCharactersToTruncateFromHead:5]; + [field setCell:cell]; + + [field setStringValue:@"Fade on both left and right AAAA"]; + GTMAssertObjectImageEqualToImageNamed(field, + @"GTMFadeTruncatingTextFieldCellTest5", + nil); + + [field setStringValue:@"Fade on left only AA"]; + GTMAssertObjectImageEqualToImageNamed(field, + @"GTMFadeTruncatingTextFieldCellTest6", + nil); + + [field setStringValue:@"A short string"]; + GTMAssertObjectImageEqualToImageNamed(field, + @"GTMFadeTruncatingTextFieldCellTest2", + nil); + + // Test the case where the number of characters to truncate from head is not + // specified. This should cause the string to be drawn centered. + [cell setDesiredCharactersToTruncateFromHead:0]; + [field setStringValue:@"Fade on both left and right AAAA"]; + GTMAssertObjectImageEqualToImageNamed(field, + @"GTMFadeTruncatingTextFieldCellTest7", + nil); +} + @end #endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 diff --git a/AppKit/TestData/GTMFadeTruncatingTextFieldCellTest5.tiff b/AppKit/TestData/GTMFadeTruncatingTextFieldCellTest5.tiff new file mode 100644 index 0000000..f360479 Binary files /dev/null and b/AppKit/TestData/GTMFadeTruncatingTextFieldCellTest5.tiff differ diff --git a/AppKit/TestData/GTMFadeTruncatingTextFieldCellTest6.tiff b/AppKit/TestData/GTMFadeTruncatingTextFieldCellTest6.tiff new file mode 100644 index 0000000..d704a81 Binary files /dev/null and b/AppKit/TestData/GTMFadeTruncatingTextFieldCellTest6.tiff differ diff --git a/AppKit/TestData/GTMFadeTruncatingTextFieldCellTest7.tiff b/AppKit/TestData/GTMFadeTruncatingTextFieldCellTest7.tiff new file mode 100644 index 0000000..ac1dca6 Binary files /dev/null and b/AppKit/TestData/GTMFadeTruncatingTextFieldCellTest7.tiff differ diff --git a/GTM.xcodeproj/project.pbxproj b/GTM.xcodeproj/project.pbxproj index 88d4537..db294ea 100644 --- a/GTM.xcodeproj/project.pbxproj +++ b/GTM.xcodeproj/project.pbxproj @@ -311,6 +311,9 @@ 8BFE6EA31282371200B5C894 /* GTMTransientRootProxyTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10A4028D0F44DB2B003B511C /* GTMTransientRootProxyTest.m */; }; 8BFE6EA41282371200B5C894 /* GTMURITemplateTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F42866B71267340A0090FE0F /* GTMURITemplateTest.m */; }; 8BFE6EA51282371200B5C894 /* GTMValidatingContainersTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B3AA9F70E033E5F007E31B5 /* GTMValidatingContainersTest.m */; }; + B71B91E21332CD680039B2CB /* GTMFadeTruncatingTextFieldCellTest5.tiff in Resources */ = {isa = PBXBuildFile; fileRef = B71B91E01332CD680039B2CB /* GTMFadeTruncatingTextFieldCellTest5.tiff */; }; + B71B91E31332CD680039B2CB /* GTMFadeTruncatingTextFieldCellTest6.tiff in Resources */ = {isa = PBXBuildFile; fileRef = B71B91E11332CD680039B2CB /* GTMFadeTruncatingTextFieldCellTest6.tiff */; }; + B71B92371332DA380039B2CB /* GTMFadeTruncatingTextFieldCellTest7.tiff in Resources */ = {isa = PBXBuildFile; fileRef = B71B92361332DA380039B2CB /* GTMFadeTruncatingTextFieldCellTest7.tiff */; }; F413908F0D75F63C00F72B31 /* GTMNSFileManager+Path.h in Headers */ = {isa = PBXBuildFile; fileRef = F413908C0D75F63C00F72B31 /* GTMNSFileManager+Path.h */; settings = {ATTRIBUTES = (Public, ); }; }; F41390900D75F63C00F72B31 /* GTMNSFileManager+Path.m in Sources */ = {isa = PBXBuildFile; fileRef = F413908D0D75F63C00F72B31 /* GTMNSFileManager+Path.m */; }; F41711350ECDFBD500B9B276 /* GTMLightweightProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = F41711320ECDFBD500B9B276 /* GTMLightweightProxy.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -784,6 +787,9 @@ 8BFE13B50FB0F2B9001BE894 /* phone.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = phone.png; sourceTree = ""; }; 8BFE13FC0FB0F2D8001BE894 /* UnitTest - AddressBook.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UnitTest - AddressBook.octest"; sourceTree = BUILT_PRODUCTS_DIR; }; 8BFE158C0FB0F34C001BE894 /* AddressBook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBook.framework; path = /System/Library/Frameworks/AddressBook.framework; sourceTree = ""; }; + B71B91E01332CD680039B2CB /* GTMFadeTruncatingTextFieldCellTest5.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = GTMFadeTruncatingTextFieldCellTest5.tiff; sourceTree = ""; }; + B71B91E11332CD680039B2CB /* GTMFadeTruncatingTextFieldCellTest6.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = GTMFadeTruncatingTextFieldCellTest6.tiff; sourceTree = ""; }; + B71B92361332DA380039B2CB /* GTMFadeTruncatingTextFieldCellTest7.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = GTMFadeTruncatingTextFieldCellTest7.tiff; sourceTree = ""; }; F413908C0D75F63C00F72B31 /* GTMNSFileManager+Path.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMNSFileManager+Path.h"; sourceTree = ""; }; F413908D0D75F63C00F72B31 /* GTMNSFileManager+Path.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSFileManager+Path.m"; sourceTree = ""; }; F413908E0D75F63C00F72B31 /* GTMNSFileManager+PathTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSFileManager+PathTest.m"; sourceTree = ""; }; @@ -1194,6 +1200,9 @@ 7F97DBA3104ED861004DDDEE /* GTMFadeTruncatingTextFieldCellTest2.tiff */, F4C58CC110BAD75200651068 /* GTMFadeTruncatingTextFieldCellTest3.tiff */, F4C58CC210BAD75200651068 /* GTMFadeTruncatingTextFieldCellTest4.tiff */, + B71B91E01332CD680039B2CB /* GTMFadeTruncatingTextFieldCellTest5.tiff */, + B71B91E11332CD680039B2CB /* GTMFadeTruncatingTextFieldCellTest6.tiff */, + B71B92361332DA380039B2CB /* GTMFadeTruncatingTextFieldCellTest7.tiff */, 8B1802410E25592200280961 /* GTMLargeTypeWindowMediumTextTest.gtmUTState */, 8BA9FBAC119CB08200E264C3 /* GTMLargeTypeWindowMediumTextTest.10.6.gtmUTState */, 8B1801A80E25341B00280961 /* GTMLargeTypeWindowImageTest.gtmUTState */, @@ -2118,6 +2127,9 @@ 8BA7148211B57EC700EB4781 /* GTMUILocalizerAndLayoutTweakerTest4-1.10_4_SDK.10.6.tiff in Resources */, 8BA7148311B57EC700EB4781 /* GTMUILocalizerAndLayoutTweakerTest4-2.10_4_SDK.10.6.tiff in Resources */, 8B4BD7E71200E5EA009C7060 /* GTMUILocalizerWindow3State.10_4_SDK.gtmUTState in Resources */, + B71B91E21332CD680039B2CB /* GTMFadeTruncatingTextFieldCellTest5.tiff in Resources */, + B71B91E31332CD680039B2CB /* GTMFadeTruncatingTextFieldCellTest6.tiff in Resources */, + B71B92371332DA380039B2CB /* GTMFadeTruncatingTextFieldCellTest7.tiff in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; -- cgit v1.2.3