CocoaDev

Edit AllPages

What we’d really like to see is NSLineBreakByTruncatingMiddle to start working someday. My guess is we’ll have to wait for Panther. (Yup, it works now.) Meanwhile, this doesn’t really do what I needed to do in the various tables and outlines in SpellCatcherX (of files, words, other things - there are quite a few in the app). Cocoa apps need to be able to draw strings that don’t fit in a given width just like the Finder does - squishing the text as far as it can, then truncating in the middle if necessary.

In the first (circa 2003) version of SpellCatcherX, I used Carbon’s TruncateThemeText. That works fine as long as the NSFont you’re drawing in matches up with an available ThemeFontID. But I really wanted this to work in any font/size. The easiest way seemed to use ATSUI to draw the text. No big deal, I come from the world of Carbon (SpellCatcherX is my first Cocoa product). Use Carbon APIs when you need to - sometimes you must! No problem, so I learned a little (more than I already did) about ATSUI and got this to (mostly, with restrictions and caveats) work quite nicely. Additionally, ATSUI (well, with most fonts) deals properly with precomposed/decomposed Unicode (a potential issue with file names). http://goo.gl/OeSCu

You could probably do this with NSLayoutManager, doing the calculations and drawing the glyphs yourself, but why re-invent the wheel?

So with this category on NSCell, and with appropriate subclassing of NSTextFieldCell (or any NSCell that can draw text) you can have non-editable, single-line table column dataCells that squish/truncate text as they’re resized, non-editable/selectable, single-line (see comments in the code) NSTextFields that will do the same if they’re not wide enough for the text. Generally you’d call this from your -drawInteriorWithFrame:inView: in your NSTextFieldCell subclass.

Anyway, here goes. If you end up using it in your product, all I ask is you take a look at SpellCatcherX and buy a copy if you like it…

// // NSCell-Extensions.m // Spell Catcher X // // Created by Evan Gross on Sat May 03 2003. // Copyright (c) 2001-2003 Rainmaker Research Inc. All rights reserved. // // A couple of snippets/techniques extracted from various Apple samples and // from OmniAppKit. // // Please do not remove this notice from the source code if you decide to use // it in your product (or some form of it). // // Posted as solve-a-general-Cocoa-problem and shamelessly-plug-my-own-product ‘ware. // // Revision History: // // Wed Jun 11 2003 EMG Version 1.1 // - Can deal with synthesized oblique/italic fonts (Helvetica, Courier) // - Performance improvements: // + Re-use ATSUTextLayout object, as per “Guidelines for Using ATSUI”. // + Only modify the ATSStyle object if the font or color has changed // from the previous call. // + Changes to use the user’s RGB colorspace, and to re-use it instead // of creating/releasing a new CGColorSpaceRef with every call.

#import

#import <Carbon/Carbon.h>

// —————————————————————————

@implementation NSCell (RRIExtensions)

// —————————————————————————

enum { // Indices of ATSUStyle attributes. kATSUFontMatrixTagIndex = 0, kATSUFontTagIndex, kATSUSizeTagIndex, kATSUQDBoldfaceTagIndex,

kATSURGBAlphaColorTagIndex  = 0,

//  Indices of ATSUTextLayout line layout attributes.
kATSUCGContextTagIndex      = 0,
kATSULineWidthTagIndex,
kATSULineFlushFactorTagIndex,
kATSULineTruncationTagIndex,
kATSULineLayoutOptionsTagIndex,

//  Size of stack buffer for the string's characters.
kCharacterBufferSize        = 512, };

// ————————————————————————— // Draw the text in an NSCell to get nice squished or truncated-in-the-middle // text like the Finder does. The shipping version of Spell Catcher X uses // Carbon’s TruncateThemeText to accomplish this, but that only works if you’re // drawing using a font that has a corresponding ThemeFontID. This (totally // new) method works with almost any NSFont that ATSFontFindFromPostScriptName() // can find a match for. Why almost? Well, it seems that there are certain // fonts out there that don’t work well with the code below. Whether these fonts // are incorrectly built somehow or this code is buggy isn’t something I // can’t definitively state at this point in time. Problems vary from badly-rendered // “squished” text to outright crashes deep inside ATSUI. Two examples: // Try resizing a NSTableColumn with a dataCell class that uses this method with: // 1. Helvetica (any typeface, any size) when the text contains // words separated by spaces. The result is that the spaces are // squished “too tightly” and the surrounding glyphs overlap. // 2. Thornburi will crash when squished. // 3. Helvetica-Oblique? There IS NO Helvetica Oblique! // Cocoa synthesizes it, so we have to as well. // NOTE: We simply skew the font matrix by kATSItalicQDSkew. // The result isn’t quite the same as whatever Cocoa does. // // NOTE: This works in flipped controlview’s. Hasn’t been tested in // non-flipped views. // NOTE: This works with left-to-right writing systems. Hasn’t been // tested with right-left or bi-di. // NOTE: I have no idea if this works correctly when printing. // // Returns NO if an error occurred, so you can call super to draw if something // goes wrong (and it can).

@end

</code>

Have fun! Fixes/modifications/comments/preformance enhancements gratefully accepted.

Evan Gross Rainmaker Research Inc.


This does’t seem to work, or return an error, if the cellFrame.origin.x is around 32768 or higher. this can happen when drawing table views with many items… 2-3 thousand. i’m guessing that an somewhere a float is getting cast to an int, but i don’t know much about carbon and can’t find the error. also it doesn’t seem to be direcly related to cellFrame.origin.x going above 32768 (i see the problem at 32761) so maybe the problem is when cellFrame.origin.x + cellFrame.size.height > 32768.

if anyone can spot it please let me know.

JesseGrosjean


From your description of the problem I looked at references to cellFrame, which is an NSRect. NSRect normally uses floating point so it doesn’t have a problem with reasonably large coordinates. But we’re not dealing with just Cocoa APIs here; it seems that in some places coordinates within the NSRect are being converted to numbers of type Fixed.

The problem is that Fixed is a 16-bit integer quantity. Actually it’s 16 bits of integer and 16 bits of fraction, but anyway, the point is you can’t represent numbers greater than plus or minus 2^16. InsideMacintosh describes the Fixed type.

http://developer.apple.com/documentation/mac/OSUtilities/OSUtilities-39.html#MARKER-9-36

There are three places in the above code where X2Fix is used to convert a float into a Fixed. They’re all potential failure points. By failure I mean bogus results - if the value goes beyond the legal range for a Fixed, it’ll wind up passing incorrect values for drawing the text, and you’ll get unpredictable results. Missing or incorrectly offset text, most likely.

*This will fail if [font pointSize] is greater than 32767. Unlikely, but possible.

// Set the size. Fixed thePointSize = X2Fix([font pointSize]); theFontStyleValues[kATSUSizeTagIndex] = &thePointSize;

*This will fail if the cell’s width is greater than about 32767. Slightly more possible than the above, but still not typical.

// Set the line width. Fixed theLineWidth = X2Fix(NSWidth(cellFrame) - 2 * kCellFrameInset - theExtraSkewWidth); theLayoutValues[kATSULineWidthTagIndex] = &theLineWidth;

*This will fail if either of the cell’s X and Y coordinates are beyond (or too close to the boundary of) the range [-32767,32767].

// Draw the text, centered vertically, offset slightly to the right // (NOT the same amount that NSTextFieldCell actually uses!). status = ATSUDrawText(sATSTextLayout, kATSUFromTextBeginning, kATSUToTextEnd, X2Fix(NSMinX(cellFrame) + kArbitraryHorizontalOffset), X2Fix(NSMidY(cellFrame) - theLineHeight / 2.0 + Fix2X(theDescent) + kArbitraryVerticalOffset));

So that’s at least part of the problem, which it sounds like you’re hitting. I’d solve the first problem by pinning. Do a range check on [font pointSize], and pin it to the legal range. You won’t be able to use font sizes larger than 32767 points, but that seems like it’ll probably be okay most of the time.

// Set the size. float thePointSizeFloat = [font pointSize]; Fixed thePointSize = X2Fix(MAX(MIN(thePointSizeFloat,32767.0f),-32767.0f)); theFontStyleValues[kATSUSizeTagIndex] = &thePointSize;

The simplest way to solve the second problem would also be by pinning. In some cases this behavior might be incorrect, if you routinely expect to draw very very very long strings and don’t want to limit the drawable line width. You’d probably have to break the line drawing up into separate calls if you wanted to do that.

// Set the line width. float theLineWidthFloat = NSWidth(cellFrame) - 2 * kCellFrameInset - theExtraSkewWidth; Fixed theLineWidth = X2Fix(MAX(MIN(theLineWidthFloat,32767.0f),-32767.0f)); theLayoutValues[kATSULineWidthTagIndex] = &theLineWidth;

The third problem is harder. The x and y arguments that you use to tell ATSUI where to draw are of type ATSUTextMeasurement, which is a Fixed, so it cannot refer to numbers outside of [-32767,32767]. So there’s no way to pass in a number greater than 32767 to this routine.

One possible trick to use is changing the origin before you draw. You can’t use Carbon to do that because Carbon’s drawing engine (QuickDraw) is similarly limited to 16-bit coordinates. The saving grace might be that ATSUDrawText does say that it will draw into the current Quartz graphics context. So changing the origin of the Quartz context might make it work.

This is straight off the top of my head and totally untested, but maybe something like:

// Draw the text, centered vertically, offset slightly to the right // (NOT the same amount that NSTextFieldCell actually uses!). // Change the context origin before drawing so that we can // draw at coordinates beyond 2^16. float xpos = NSMinX(cellFrame) + kArbitraryHorizontalOffset; float ypos = NSMidY(cellFrame) - theLineHeight / 2.0 + Fix2X(theDescent) + kArbitraryVerticalOffset; CGContextTranslateCTM(theCGContextRef,xpos,ypos); status = ATSUDrawText(sATSTextLayout, kATSUFromTextBeginning, kATSUToTextEnd, (Fixed)0, (Fixed)0);

This modification might need some tweaking but I think it’s the general idea. If this works for you, please post the actual code you used! :-)


Ironically, speaking of 32K limits, this page is very close to the wiki’s 32K size limit so please direct further comments, etc, to BetterTruncatingStringsInTableViewDiscussion.