TweetFollow Us on Twitter

Spaced Out

Volume Number: 18 (2002)
Issue Number: 12
Column Tag: Mac OS X

Spaced Out

Adding Paragraph Spacing to the Cocoa Text System

by Andrew C. Stone

One of my favorite Cocoa demos is to make a full featured word processor application in 5 minutes - complete with rulers, tabs, embedded graphics, line spacing, kerning, ligatures, baseline, colors, multi-font, automatic spell checking and more - (see http://www.stone.com/The_Cocoa_Files/Arise_Aqua_.html). Cocoa's powerful text system is a collection of classes that can meet almost any text need, and I highly recommend reading the documentation for these classes:,NSText and NSTextView (the display classes), NSTextStorage and NSAttributedString (how rich strings store all the attributes of unicode text), NSLayoutManager (manages an NSTextStorage and set of NSTextContainers which display in an NSTextView) and playing with the sample Text layout application available in the Developer distribution in /Developer/Examples/AppKit/TextSizingExample.

These full featured classes are getting more powerful with each major system release. In Mac OS X 10.2, a feature which was previously defined in the API was implemented in NSParagraphStyle:

ParagraphSpacing

- (float)paragraphSpacing

Returns the space added at the end of the paragraph to separate it from the following paragraph. This value is always nonnegative.

See Also: - lineSpacing - setParagraphSpacing: (NSMutableParagraphStyle)

Paragraph spacing is the additional distance between paragraphs (that is, whenever a <RETURN> appears in the text). This amount is in points (72 per inch) and by default is 0. It's added to any additional line spacing that applies to that paragraph style.

But, although the implementation is there, the interface is not. The ruler, which comes for free with NXTextView, and which contains controls for alignment, line spacing, and tabs, currently lacks any control for setting the paragraph spacing. This article will show you how to add a custom control to the standard text system ruler.


Figure 1: The standard text ruler in Mac OS X 10.2 doesn't have a paragraph spacing control.

Just using simple subclasses of NSTextView and NSLayoutManager, our page layout and web authoring application Create(R) can flow text through any size containers, place rich text along any path, apply neon and custom pattern effects to any text, and place text outside or inside of any path. Once Jaguar shipped, we could easily add paragraph spacing to our text - if we could figure out a way to add a new control to the ruler that automatically attaches itself to any NSTextView in an NSScrollView.

Like everything with Cocoa - if it's hard it's wrong. So finding an easy solution with modular application is the always the goal of any Cocoa programming challenge.

Our design imperatives include:

  • make it simple

  • make it small so it doesn't get in the way

  • make it a modular nib file

The solution has 3 parts - a user interface built in InterfaceBuilder, the create/update code in an NSLayoutManager subclass, and code in an NSTextView subclass which actually sets the spacing and maintains the undo stack.

So, I chose to make the narrowest UI possible - using the P symbol for paragraph and an NSStepper:


Figure 2: Create(R) adds a paragraph spacer next to the line spacing tools - P

The first problem is how do we get at the ruler to install the new device? It's owned by NSLayoutManager, which provides a method that returns this ruler view - so that seems like the most appropriate place to instantiate and update our own user interface addition which can set the paragraph spacing:

- (NSView *)rulerAccessoryViewForTextView:(NSTextView *)aTextView 
paragraphStyle:(NSParagraphStyle *)paraStyle ruler:(NSRulerView *)aRulerView enabled:(BOOL)flag

Returns the accessory NSView for aRulerView. This accessory contains tab wells, text alignment buttons, and so on. paraStyle is used to set the state of the controls in the accessory NSView; it must not be nil. If flag is YES the accessory view is enabled and accepts mouse and keyboard events; if NO it's disabled.

This method is invoked automatically by the NSTextView object using the layout manager. You should rarely need to invoke it, but you can override it to customize ruler support.

Let's Just Face it

We'll use InterfaceBuilder to create the interface and even the stub files for our new class, ParagraphSpacer:

    1. Launch Interface Builder

    2. File -> New..., Cocoa, "Empty", Click "New"

    3. Save this as "ParagraphSpacer.nib" in your project directory - also add it to your project when asked.

    4. Double-click the "File's Owner" icon in the folio window - NSObject will be selected in the Classes tab

    5. Control-Click NSObject and select "Create Subclass" - name it "ParagraphSpacer"

    6. Add two outlets: stepper and containerView by Control-Clicking ParagraphSpacer and choose "Add Outlet to ParagraphSpacer".

    7. Control-click ParagraphSpacer and choose "Create Files for Paragraph Spacer" - add these to your project

    8. Choose "Instances" tab, select "File's Owner", Info-> Custom Class, select "ParagraphSpacer"

    9. From the Tab icon on the Palette, drag a "Custom View" into the folio window

    10. Set the view's size with , Info -> Size, 26 wide by 28 tall

    11. Drag in "System Font Text", select all, delete, type P, Info -> Size 10 wide by 17 tall, locate on left

    12. Drag in an NSStepper from Slider Icon tab on Palette, adjust location as needed

    13. Connect the File's owner to the two instance variables - the stepper, and the view which holds the stepper and the static P text.

    14. Save ParagraphSpacer.nib

ParagraphSpacer

A very simple class which just returns its two instance variables.We need these to install the view into the Ruler view hierarchy and set the value of the stepper during updates:

/* ParagraphSpacer */
#import <Cocoa/Cocoa.h>
@interface ParagraphSpacer : NSObject
{
    IBOutlet id containerView;
    IBOutlet NSStepper *stepper;
}
- (NSStepper *)stepper;
- (NSView *)containerView;
@end
#import "ParagraphSpacer.h"
@implementation ParagraphSpacer
- (id) init {
    self = [super init];
    if (![NSBundle loadNibNamed:@"ParagraphSpacer.nib" owner:self])
   NSLog(@"couldn't load ParagraphSpacer\n");
        
        
    return self;
}
- (NSStepper *)stepper; {
    return stepper;
}
- (NSView *)containerView; {
    return containerView;
}
@end

SDLayoutManager

We just need to add one method to our NSLayoutManager subclass. Note that we call [super rulerAccessoryViewForTextView: ... ] to get the standard ruler provided for us, then we check to see if we have already initialized the paragraph spacer, and if not, proceed to create it, find it's proper position in the ruler and install it. We'll travel down the view hierarchy, looking at the subviews of each view. When we find a view with several subviews, then we know we're in the right place. When we find the view that starts far to the left, ie, not the tab well, but the NSBox which surrounds the alignment and line spacing controls, we'll place our control right next to it. We just have to hope that the ruler doesn't change drastically - if it does, it will probably have more controls in it, and our layout may be wrong.

Each time this method gets called, we'll set the target of the stepper to be the current text view with an action of changeParagraphSpacing:, and update the value of the stepper so that it sends the target the right value when incrementing or decrementing.

@interface SDLayoutManager : NSLayoutManager
{
  @private
    NSStepper *_paragraphStepper;
}
@implementation SDLayoutManager
- (NSView *)rulerAccessoryViewForTextView:(NSTextView *)view paragraphStyle:(NSParagraphStyle *)style 
ruler:(NSRulerView *)ruler enabled:(BOOL)isEnabled {

    NSView *accessory = [super rulerAccessoryViewForTextView:view paragraphStyle:style ruler:ruler 
    enabled:isEnabled];
    
    if (!_paragraphStepper) {
        ParagraphSpacer *spacer = [[ParagraphSpacer allocWithZone:[self zone]] init];
        NSView *viewToAdd = [spacer containerView];
        NSArray *subviews = [accessory subviews];
        NSView *viewToAddTo = accessory;
        unsigned int i, count = [subviews count];
        NSRect viewRect = [viewToAdd bounds];
        
        if (count == 1) {
            viewToAddTo = [subviews objectAtIndex:0];
            subviews = [[subviews objectAtIndex:0] subviews];
            count = [subviews count];
        }
        
        _paragraphStepper = [spacer stepper];
        
        for (i = 0; i < count; i++) {
            NSView *v = [subviews objectAtIndex:i];
            NSRect r = [v frame];
            if (r.origin.x < 10.0) {   // it's the box containing the left controls)
                viewRect.origin.x = r.origin.x + r.size.width;
                viewRect.origin.y = 0.0;
                [viewToAdd setFrame:viewRect];
                [viewToAddTo addSubview:viewToAdd];
            }
        }
    }
    [_paragraphStepper setDoubleValue:style? [style paragraphSpacing] : 0.0];
    [_paragraphStepper setTarget:view];
    [_paragraphStepper setAction:@selector(changeParagraphSpacing:)];
    
    return accessory;
}
@end

SDTextView

If you don't want to subclass NSTextView, you could instead add the changeParagraphSpacing: method to a category of NSTextView. This is not the case with the NSLayoutManager subclass, because we need to call super's implementation of rulerAccessoryViewForTextView:paragraphStyle:ruler:enabled:.

The main reason we place this code in NSTextView or a subclass is so we get the free automatic undo associated with Text. To do that, we alert the text system that there will be changes in a certain range with shouldChangeTextInRange: replacementString: with a replacement string of "nil" , which means other attributes are changing, but not any characters. Then, we walk over text paragraph style by paragraph style, setting the paragraph spacing to the value determined by the stepper (up or down a point from the first style in the selection). Note that if there are no paragraph attributes, one is added.

Finally, we alert the text that we are done changing it with didChangeText, and we add a custom action name so the menu will say "Undo Paragraph Spacing" and "Redo Paragraph Spacing".

@interface SDTextView: NSTextView
{}
@end
@implementation SDTextView
- (void)changeParagraphSpacing:(id)sender {
    double value = [sender doubleValue];
    NSRange range = [self rangeForUserParagraphAttributeChange];
    if (range.length > 0) {
        NSRange remainingRange = range;
        NSTextStorage *storage = [self textStorage];
        [self shouldChangeTextInRange:range replacementString:nil];
        while (remainingRange.length > 0) {
                NSRange effectiveRange;
                NSParagraphStyle *para = [storage attribute:NSParagraphStyleAttributeName 
                atIndex:remainingRange.location longestEffectiveRange:&effectiveRange 
                inRange:remainingRange];
        
                if (para == nil) {
                    para = [[[NSMutableParagraphStyle alloc] init] autorelease];
                    [para setParagraphStyle:[NSParagraphStyle defaultParagraphStyle]];
                } else para = [[para mutableCopyWithZone:[self zone]]autorelease];
                
                [para setParagraphSpacing:value];
                [storage addAttribute:NSParagraphStyleAttributeName value:para range:remainingRange];
    
                if (NSMaxRange(effectiveRange) < NSMaxRange(remainingRange)) {
                    remainingRange.length = NSMaxRange(remainingRange) - NSMaxRange(effectiveRange);
                    remainingRange.location = NSMaxRange(effectiveRange);
                } else {
                    break;
                }
        }
            [self didChangeText];
            [[self undoManager] setActionName:NSLocalizedStringFromTable(@"Paragraph Spacing",
            @"Muktinath",@"change of space between paragraphs")];
    
    }
}
@end

All Together Now

Your final task is just to be sure you create your text system with the special SDLayoutManager. If you have a shared text editor, it might look something like this:

static NSTextView *newEditor(TextArea *self) {
    SDTextView *tv;
    NSTextContainer *tc;
    // This method returns an NSTextView whose SDLayoutManager has a refcount of 1.  It is 
    the caller's responsibility to release the SDLayoutManager.  This function is only for the use of
    the following method.
    
    SDLayoutManager *lm = [[SDLayoutManager allocWithZone:NULL] init];
    tv = [[SDTextView allocWithZone:NULL] initWithFrame:NSMakeRect(0.0, 0.0, 100.0, 100.0) 
    textContainer:nil];
    
    tc = [[NSTextContainer allocWithZone:NULL] initWithContainerSize:NSMakeSize(1.0e6, 1.0e6)];
    [lm addTextContainer:tc];
    [tc release];
     
    [tc setTextView:tv];
    [tv release];
    return tv;
}


Figure 3: The new text ruler with paragraph spacing stepper installed.

Conclusion

The Cocoa text system just keeps getting better. And sometimes there are features that are still hidden from the user interface, such as paragraph spacing in Jaguar 10.2. With a little Cocoa magic, it's easy to install your own custom controls and add more functionality to the standard text object.


Andrew Stone, CEO of Stone Design, www.stone.com, has been the principal architect of several solar houses and over a dozen Cocoa applications shipping for Mac OS X.

 
AAPL
$501.11
Apple Inc.
+2.43
MSFT
$34.64
Microsoft Corpora
+0.15
GOOG
$898.03
Google Inc.
+16.02

MacTech Search:
Community Search:

Software Updates via MacUpdate

CrossOver 12.5.1 - Run Windows apps on y...
CrossOver can get your Windows productivity applications and PC games up and running on your Mac quickly and easily. CrossOver runs the Windows software that you need on Mac at home, in the office,... Read more
Paperless 2.3.1 - Digital documents mana...
Paperless is a digital documents manager. Remember when everyone talked about how we would soon be a paperless society? Now it seems like we use paper more than ever. Let's face it - we need and we... Read more
Apple HP Printer Drivers 2.16.1 - For OS...
Apple HP Printer Drivers includes the latest HP printing and scanning software for Mac OS X 10.6, 10.7 and 10.8. For information about supported printer models, see this page.Version 2.16.1: This... Read more
Yep 3.5.1 - Organize and manage all your...
Yep is a document organization and management tool. Like iTunes for music or iPhoto for photos, Yep lets you search and view your documents in a comfortable interface, while offering the ability to... Read more
Apple Canon Laser Printer Drivers 2.11 -...
Apple Canon Laser Printer Drivers is the latest Canon Laser printing and scanning software for Mac OS X 10.6, 10.7 and 10.8. For information about supported printer models, see this page.Version 2.11... Read more
Apple Java for Mac OS X 10.6 Update 17 -...
Apple Java for Mac OS X 10.6 delivers improved security, reliability, and compatibility by updating Java SE 6.Version Update 17: Java for Mac OS X 10.6 Update 17 delivers improved security,... Read more
Arq 3.3 - Online backup (requires Amazon...
Arq is online backup for the Mac using Amazon S3 and Amazon Glacier. It backs-up and faithfully restores all the special metadata of Mac files that other products don't, including resource forks,... Read more
Apple Java 2013-005 - For OS X 10.7 and...
Apple Java for OS X 2013-005 delivers improved security, reliability, and compatibility by updating Java SE 6 to 1.6.0_65. On systems that have not already installed Java for OS X 2012-006, this... Read more
DEVONthink Pro 2.7 - Knowledge base, inf...
Save 10% with our exclusive coupon code: MACUPDATE10 DEVONthink Pro is your essential assistant for today's world, where almost everything is digital. From shopping receipts to important research... Read more
VirtualBox 4.3.0 - x86 virtualization so...
VirtualBox is a family of powerful x86 virtualization products for enterprise as well as home use. Not only is VirtualBox an extremely feature rich, high performance product for enterprise customers... Read more

Briquid Gets Updated with New Undo Butto...
Briquid Gets Updated with New Undo Button, Achievements, and Leaderboards, on Sale for $0.99 Posted by Andrew Stevens on October 16th, 2013 [ | Read more »
Halloween – iLovecraft Brings Frightenin...
Halloween – iLovecraft Brings Frightening Stories From Author H.P. | Read more »
The Blockheads Creator David Frampton Gi...
The Blockheads Creator David Frampton Gives a Postmortem on the Creation Process of the Game Posted by Andrew Stevens on October 16th, 2013 [ permalink ] Hey, a | Read more »
Sorcery! Enhances the Gameplay in Latest...
Sorcery! | Read more »
It Came From Australia: Tiny Death Star
NimbleBit and Disney have teamed up to make Star Wars: Tiny Death Star, a Star Wars take on Tiny Tower. Right now, the game is in testing in Australia (you will never find a more wretched hive of scum and villainy) but we were able to sneak past... | Read more »
FIST OF AWESOME Review
FIST OF AWESOME Review By Rob Rich on October 16th, 2013 Our Rating: :: TALK TO THE FISTUniversal App - Designed for iPhone and iPad A totalitarian society of bears is only the tip of the iceberg in this throwback brawler.   | Read more »
PROVERBidioms Paints English Sayings in...
PROVERBidioms Paints English Sayings in a Picture for Users to Find Posted by Andrew Stevens on October 16th, 2013 [ permalink ] | Read more »
OmniFocus 2 for iPhone Review
OmniFocus 2 for iPhone Review By Carter Dotson on October 16th, 2013 Our Rating: :: OMNIPOTENTiPhone App - Designed for the iPhone, compatible with the iPad OmniFocus 2 for iPhone is a task management app for people who absolutely... | Read more »
Ingress – Google’s Augmented-Reality Gam...
Ingress – Google’s Augmented-Reality Game to Make its Way to iOS Next Year Posted by Andrew Stevens on October 16th, 2013 [ permalink ] | Read more »
CSR Classics is Full of Ridiculously Pre...
CSR Classics is Full of Ridiculously Pretty Classic Automobiles Posted by Rob Rich on October 16th, 2013 [ permalink ] | Read more »

Price Scanner via MacPrices.net

Apple Store Canada offers refurbished 11-inch...
 The Apple Store Canada has Apple Certified Refurbished 2013 11″ MacBook Airs available starting at CDN$ 849. Save up to $180 off the cost of new models. An Apple one-year warranty is included with... Read more
Updated MacBook Price Trackers
We’ve updated our MacBook Price Trackers with the latest information on prices, bundles, and availability on MacBook Airs, MacBook Pros, and the MacBook Pros with Retina Displays from Apple’s... Read more
13-inch Retina MacBook Pros on sale for up to...
B&H Photo has the 13″ 2.5GHz Retina MacBook Pro on sale for $1399 including free shipping. Their price is $100 off MSRP. They have the 13″ 2.6GHz Retina MacBook Pro on sale for $1580 which is $... Read more
AppleCare Protection Plans on sale for up to...
B&H Photo has 3-Year AppleCare Warranties on sale for up to $105 off MSRP including free shipping plus NY sales tax only: - Mac Laptops 15″ and Above: $244 $105 off MSRP - Mac Laptops 13″ and... Read more
Apple’s 64-bit A7 Processor: One Step Closer...
PC Pro’s Darien Graham-Smith reported that Canonical founder and Ubuntu Linux creator Mark Shuttleworth believes Apple intends to follow Ubuntu’s lead and merge its desktop and mobile operating... Read more
MacBook Pro First, Followed By iPad At The En...
French site Info MacG’s Florian Innocente says he has received availability dates and order of arrival for the next MacBook Pro and the iPad from the same contact who had warned hom of the arrival of... Read more
Chart: iPad Value Decline From NextWorth
With every announcement of a new Apple device, serial upgraders begin selling off their previous models – driving down the resale value. So, with the Oct. 22 Apple announcement date approaching,... Read more
SOASTA Survey: What App Do You Check First in...
SOASTA Inc., the leader in cloud and mobile testing announced the results of its recent survey showing which mobile apps are popular with smartphone owners in major American markets. SOASTA’s survey... Read more
Apple, Samsung Reportedly Both Developing 12-...
Digitimes’ Aaron Lee and Joseph Tsai report that Apple and Samsung Electronics are said to both be planning to release 12-inch tablets, and that Apple is currently cooperating with Quanta Computer on... Read more
Apple’s 2011 MacBook Pro Lineup Suffering Fro...
Appleinsider’s Shane Cole says that owners of early-2011 15-inch and 17-inch MacBook Pros are reporting issues with those models’ discrete AMD graphics processors, which in some cases results in the... Read more

Jobs Board

*Apple* Retail - Manager - Apple (United Sta...
Job SummaryKeeping an Apple Store thriving requires a diverse set of leadership skills, and as a Manager, youre a master of them all. In the stores fast-paced, dynamic Read more
*Apple* Support / *Apple* Technician / Mac...
Apple Support / Apple Technician / Mac Support / Mac Set up / Mac TechnicianMac Set up and Apple Support technicianThe person we are looking for will have worked Read more
Senior Mac / *Apple* Systems Engineer - 318...
318 Inc, a top provider of Apple solutions is seeking a new Senior Apple Systems Engineer to be based out of our Santa Monica, California location. We are a Read more
*Apple* Retail - Manager - Apple Inc. (Unite...
Job Summary Keeping an Apple Store thriving requires a diverse set of leadership skills, and as a Manager, you’re a master of them all. In the store’s fast-paced, Read more
*Apple* Solutions Consultant - Apple (United...
**Job Summary** Apple Solutions Consultant (ASC) - Retail Representatives Apple Solutions Consultants are trained by Apple on selling Apple -branded products Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.