Using NSParagraphStyle
Volume Number: 19 (2003)
Issue Number: 8
Column Tag: Cocoa
Using NSParagraphStyle
How to programmatically set tabs and more in an NSTextView
by Clark Jackson
The need to become acquainted with NSParagraphStyle occurred when I attempted to automate some reporting. At times "enterprise" computing stands in contrast to typical "consumer" computing because the latter is user-centric while the former is report-centric. For example, consumer computing facilitates lots of user control whereas enterprise computing just wants to-generate-reports-unattended-at-midnight-thank-you. Since much of the text system in Cocoa "just works" for the user, the details of how to let the computer take control in making a report are often left a mystery. Let's shed a little light on some of that mystery.
What we need is to know how to use just one piece of Cocoa's extensive and capable text system that will allow a program to format text inside an NSTextView. The key is using the NSParagraphStyle class.
Some preliminaries
Explaining Interface Builder and Project Builder is beyond the scope of this article. Suffice it to say, the project uses the Model-View-Controller paradigm employing MyModel, MyWindowController, and NSTextView classes. Here's what the view looks like:
Figure 1. The interface window containing an NSTextView.
The only thing noteworthy about this window is that it contains an NSTextView because that is where our formatted text will appear.
To see the results of our formatting it will be useful to use NSTextView's ruler. To see the ruler it is necessary to add the Format menu to our menu bar. Therefore, before leaving IB, we drag the Format menu onto our project's main menu bar and give Show Ruler a key equivalent:
Figure 2. For convenience, in Interface Builder we drag a Format menu onto the menu bar and give Show Ruler a key equivalent.
The view
For our purposes, the important thing to know about NSTextView is that the text displayed in an NSTextView is its textStorage and that textStorage holds attributed strings. The textStorage is an instance of NSTextStorage which inherits from NSAttributedString itself. Attributed strings are Cocoa Foundation objects that contain both ordinary NSString's and formatting objects that apply to those strings, for example, NSFont and NSParagraphStyle. You can bypass dealing with NSTextView's textStorage directly, that is, you can [myNSTextView setString:@"aString"], but @"aString" will adopt either the default paragraph style or a previous style already residing in the textStorage.
Therefore, in order for text to appear formatted in an NSTextView we have to: 1) create an NSAttributedString instance initialized with the string we want to format, and 2) give that NSAttributedString instance the objects that it will use to format the string-in this case an NSParagraphStyle object. This attributed string goes into our NSTextView's textStorage where it appears formatted to the user.
The Controller
The interface buttons are connected to the controller's actions in a typical fashion. Basically, each button initiates a controller action to display a different formatted paragraph in the NSTextView. The controller does this task by sending a string (NSString) to the model object. The model object will return attributed strings (NSAttributedString's attributed with NSParagraphStyle's) to the controller. Finally, the controller will pass the attributed string to the text storage of the NSTextView where it will appear formatted to the user.
Here is one of the three controller actions:
applyStyle1
This method sets a pattern for the other two belonging to the controller. An unformatted string is
put in the NSTextView for comparison purposes only. The program pauses so the user can see it. Next,
the string is sent to the model object which returns it as an attributed string. Finally, the
attributed string goes into the NSTextView revealing the new format. Refer to the source to see the
other two action methods, applyStyle2 and applyStyle3.
- (IBAction)applyStyle1:(id)sender
{
MyModel *theModel;
NSAttributedString *anAttString;
NSString *aString;
// a string containing multiple paragraphs
aString = @"On a merry-go-round in the night, ";
aString = [aString stringByAppendingString:
@"Coriolis was shaken with fright.\n"];
aString = [aString stringByAppendingString:
@"Despite how he walked, "];
aString = [aString stringByAppendingString:
@"'Twas like he was stalked,\n"];
aString = [aString stringByAppendingString:
@"By some fiend always pushing him right."];
// limerick by David Morin, Eric Zaslow, E'beth Haley, John Golden, and Nathan Salwen,
// http://www.aps.org/apsnews/11855.html
// a convenience method to restore the default paragraph style to the NSTextView
[self resetTextView];
// install string w/o changing paragraph style for comparison
[myNSTextView setString:aString];
[myNSTextView display]; // force redraw
sleep(1); // pause
// now convert aString to an NSAttributedString by applying an NSParagraphStyle
// using the model object
theModel = [[MyModel alloc]init];
anAttString = [theModel attributeString1:aString];
[theModel release];
// now that we have the attributed string, put it into the NSTextView.
// In order to get multiple attributed strings into a textStorage use
// [myNSTextView insertText:attributedString] instead
[[myNSTextView textStorage]
setAttributedString:anAttString];
[myNSTextView setNeedsDisplay:YES];
return;
}
The Model
MyModel has a method for each of the controller's actions. It is here that we make use of NSParagraphStyle. Each method takes an ordinary NSString and returns it as an NSAttributedString so it can end up formatted in the NSTextView. When running the program, click inside the NSTextView and make the ruler visible. Some of the paragraph style attributes that we set show up on the NSTextView's ruler:
Figure 3. Many of NSParagraphStyle attributes appear in the NSTextView ruler.
Following are the methods used to create NSParagraphStyle's and apply them to strings thus creating NSAttributedString's:
attributeString1
This method creates an NSMutableAttributedString, using an NSString and an NSMutableParagraphStyle.
-(NSMutableAttributedString *) attributeString1:
(NSString *) aString
{
NSMutableParagraphStyle *aMutableParagraphStyle;
NSMutableAttributedString *attString;
/*
The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
to copy, we use the default one.
The default values supplied by the default NSParagraphStyle are:
Alignment NSNaturalTextAlignment
Tab stops 12 left-aligned tabs, spaced by 28.0 points
Line break mode NSLineBreakByWordWrapping
All others 0.0
*/
aMutableParagraphStyle =
[[NSParagraphStyle defaultParagraphStyle]mutableCopy];
// Now adjust our NSMutableParagraphStyle formatting to be whatever we want.
// The numeric values below are in points (72 points per inch)
[aMutableParagraphStyle
setAlignment:NSLeftTextAlignment];
[aMutableParagraphStyle setLineSpacing:5.5];
[aMutableParagraphStyle setParagraphSpacing:25.5];
[aMutableParagraphStyle setHeadIndent:25.0];
[aMutableParagraphStyle setTailIndent:-45.0];
// setTailIndent: if negative, offset from right margin (right margin mark does
// NOT appear); if positive, offset from left margin (margin mark DOES appear)
[aMutableParagraphStyle setFirstLineHeadIndent:65.0];
[aMutableParagraphStyle
setLineBreakMode:NSLineBreakByWordWrapping];
/*
possible allignments
NSLeftTextAlignment
NSRightTextAlignment
NSCenterTextAlignment
NSJustifiedTextAlignment
NSNaturalTextAlignment
possible line wraps
NSLineBreakByWordWrapping
NSLineBreakByCharWrapping
NSLineBreakByClipping
*/
// Instantiate the NSMutableAttributedString with the argument string
attString = [[NSMutableAttributedString alloc]
initWithString:aString];
// Apply your paragraph style attribute over the entire string
[attString addAttribute:NSParagraphStyleAttributeName
value:aMutableParagraphStyle
range:NSMakeRange(0,[aString length])];
[aMutableParagraphStyle release]; // since it was copy'd
[attString autorelease]; // since it was alloc'd
return attString;
}
If your NSTextView already has attributed strings in its textStorage, you can get the NSParagraphStyle by:
aMutableParagraphStyle = [[myTextView typingAttributes]
objectForKey:@"NSParagraphStyle"];
The NSParagraphStyle returned above comes from the attributes of the text from where the cursor is found inside the NSTextView. NSParagraphStyle's can span multiple paragraphs but there cannot be more than one per paragraph.
attributeString2
This method creates an array of NSTextTab's and applies them to the NSMutableParagraphStyle before
it is used to create the NSMutableAttributedString.
-(NSMutableAttributedString *)
attributeString2:(NSString *) aString
{
float firstColumnInch = 1.75,
otherColumnInch = 0.6, pntPerInch = 72.0;
int i;
NSTextTab *aTab;
NSMutableArray *myArrayOfTabs;
NSMutableParagraphStyle *aMutableParagraphStyle;
NSMutableAttributedString *attString;
myArrayOfTabs = [NSMutableArray arrayWithCapacity:14];
aTab = [[NSTextTab alloc]
initWithType:NSLeftTabStopType
location:firstColumnInch*pntPerInch];
[myArrayOfTabs addObject:aTab];
[aTab release]; // aTab was alloc'd and the array owns it now so release it
for(i=1;i<14;i++)
{
aTab = [[NSTextTab alloc]
initWithType:NSRightTabStopType
location:(firstColumnInch*pntPerInch)
+ ((float)i * otherColumnInch * pntPerInch)];
[myArrayOfTabs addObject:aTab];
[aTab release]; // aTab was alloc'd and the array owns it now so release it
}
/*
possible tab stop types
NSLeftTabStopType
NSRightTabStopType
NSCenterTabStopType
NSDecimalTabStopType
*/
aMutableParagraphStyle =
[[NSParagraphStyle defaultParagraphStyle]mutableCopy];
[aMutableParagraphStyle setTabStops:myArrayOfTabs];
attString = [[NSMutableAttributedString alloc]
initWithString:aString];
[attString addAttribute:NSParagraphStyleAttributeName
value:aMutableParagraphStyle
range:NSMakeRange(0,[aString length])];
[aMutableParagraphStyle release];
[attString autorelease];
return attString;
}
Tabbed text appears as below without using NSParagraphStyle:
Figure 4. Default behavior of an NSTextView containing tabbed text. The tab spacing is arbitrary and the lines wrap even though there is space enough to hold the whole line.
Tabbed text appears as below after creating an NSMutableParagraphStyle and assigning it to an NSMutableAttributedString using the attributeString2 method:
Figure 5. By using an NSMutableParagraphStyle the tab stops have been precisely set and the lines do not wrap unexpectedly.
attributeString3
This method supplies a way to parse a string containing multiple paragraphs and apply different
paragraph styles to each.
-(NSMutableArray *) attributeString3:(NSString *)aString
{
NSRange myRange,rangeOfLine,rangeOfLineContent;
unsigned startIndex,lineEndIndex,contentsEndIndex;
NSMutableParagraphStyle
*aMutableParagraphStyle1,*aMutableParagraphStyle2;
NSMutableAttributedString *attString;
int lineCtr;
NSMutableArray * arrayOfNSMutableAttributedString;
NSString *pulledOutParagraph;
arrayOfNSMutableAttributedString
= [NSMutableArray arrayWithCapacity:5];
/*
lines can be terminated in the following ways for use with
getLineStart:end:contentsEnd:forRange:
* U+000D (\r or CR), Mac
* U+2028 (Unicode line separator)
* U+000A (\n or LF), Unix
* U+2029 (Unicode paragraph separator)
* \r\n, in that order (also known as CRLF), Windows
*/
/*
Using getLineStart:end:contentsEnd:forRange:
@"Big\r\nMac --example string
in this case
1st contentsEndIndex is set to 3,
the first character past the line content,
line content being defined as the characters not including
line termination character(s)
1st lineEndIndex is set to 5
the first character past the end of line character(s)
*/
// make 2 different paragraph styles
aMutableParagraphStyle1 =
[[NSParagraphStyle defaultParagraphStyle]mutableCopy];
aMutableParagraphStyle2 =
[[NSParagraphStyle defaultParagraphStyle]mutableCopy];
[aMutableParagraphStyle1
setAlignment:NSLeftTextAlignment];
[aMutableParagraphStyle2
setAlignment:NSRightTextAlignment];
// initialize
myRange = NSMakeRange(0,0); // (location,length)
lineEndIndex = 0;
lineCtr = 0;
/*
The while() block below walks down the string pulling out lines (paragraphs)
including their end of line (paragraph) characters, converts them to attributed strings,
and adds them to an array.
The range in the getLineStart:end:contentsEnd:forRange: method has to be picky
about where it's location is but doesn't have to be picky about length (i.e. 0 will do)
*/
while(lineEndIndex < [aString length])
{
[aString getLineStart: &startIndex
end: &lineEndIndex
contentsEnd: &contentsEndIndex
forRange: myRange];
// rangeOfLineContent excludes line termination character(s)
rangeOfLineContent =
NSMakeRange(startIndex,contentsEndIndex-startIndex);
// rangeOfLine includes line termination character(s)
rangeOfLine =
NSMakeRange(startIndex,lineEndIndex-startIndex);
// apply paragraph style to pulledOutParagraph
pulledOutParagraph =
[aString substringWithRange:rangeOfLine];
attString = [[NSMutableAttributedString alloc]
initWithString:pulledOutParagraph];
if(lineCtr % 2 == 0) // alternate paragraph styles on odd/even lines
{
// If range.length below is zero then the paragraph style is not applied.
// For strings with length > 0, range.length should be > 0 and <= length of the
// string so NSMakeRange(0,1) would work as well below
// provided you don't have an empty string.
[attString addAttribute:NSParagraphStyleAttributeName
value: aMutableParagraphStyle1
range: NSMakeRange(0,[pulledOutParagraph length])];
}
else
{
[attString addAttribute:NSParagraphStyleAttributeName
value:aMutableParagraphStyle2
range:NSMakeRange(0,[pulledOutParagraph length])];
}
[arrayOfNSMutableAttributedString addObject:attString];
[attString release]; // the array now owns it
lineCtr++;
// recalculate the range before looping
myRange = NSMakeRange(lineEndIndex,0);
}
[aMutableParagraphStyle1 release];
[aMutableParagraphStyle2 release];
return arrayOfNSMutableAttributedString;
}
Conclusion
Cocoa's text system is extensive involving many classes not even mentioned here. But if you just need to get some formatted, tabbed text into an NSTextView for that special report, the methods explained here should do the trick.
Clark Jackson test drove the first Mac and recognized that it was about to change the world. Lately he has been busy wearing many hats automating energy accounting in the rapidly changing electrical power industry at Tacoma Power. He can be contacted at cjackson@cityoftacoma.org.