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.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

SoftRAID 5.8.4 - High-quality RAID manag...
SoftRAID allows you to create and manage disk arrays to increase performance and reliability. SoftRAID allows the user to create and manage RAID 4 and 5 volumes, RAID 1+0, and RAID 1 (Mirror) and... Read more
Audio Hijack 3.7.3 - Record and enhance...
Audio Hijack (was Audio Hijack Pro) drastically changes the way you use audio on your computer, giving you the freedom to listen to audio when you want and how you want. Record and enhance any audio... Read more
CleanMyMac X 4.6.15 - Delete files that...
CleanMyMac makes space for the things you love. Sporting a range of ingenious new features, CleanMyMac lets you safely and intelligently scan and clean your entire system, delete large, unused files... Read more
Suitcase Fusion 21.2.1 - Font management...
Suitcase Fusion is the creative professional's font manager. Every professional font manager should deliver the basics: spectacular previews, powerful search tools, and efficient font organization.... Read more
Civilization VI 1.3.6 - Next iteration o...
Civilization® VI is the award-winning experience. Expand your empire across the map, advance your culture, and compete against history’s greatest leaders to build a civilization that will stand the... Read more
Dashlane 6.2042.0 - Password manager and...
Dashlane is an award-winning service that revolutionizes the online experience by replacing the drudgery of everyday transactional processes with convenient, automated simplicity - in other words,... Read more
Airfoil 5.9.2 - Send audio from any app...
Airfoil allows you to send any audio to AirPort Express units, Apple TVs, and even other Macs and PCs, all in sync! It's your audio - everywhere. With Airfoil you can take audio from any... Read more
VirtualBox 6.1.16 - x86 virtualization s...
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
Xcode 12.1 - Integrated development envi...
Xcode includes everything developers need to create great applications for Mac, iPhone, iPad, and Apple Watch. Xcode provides developers a unified workflow for user interface design, coding, testing... Read more
FileZilla 3.51.0 - Fast and reliable FTP...
FileZilla (ported from Windows) is a fast and reliable FTP client and server with lots of useful features and an intuitive interface. Version 3.51.0: Bugfixes and minor changes: Fixed import of... Read more

Latest Forum Discussions

See All

PUBG Mobile has provided yet another upd...
PUBG Mobile has been making a point of publicly mentioning all of their ongoing efforts to vanquish cheating from the popular battle royale. Today two teams within the company have provided updates on their progress. [Read more] | Read more »
Zombieland: AFK Survival is celebrating...
Zombieland: AFK Survival is currently celebrating its one-year anniversary. If you don't quite recognise the name that's because it initially launched as Zombieland: Double Tapper. Anyway, the game is celebrating turning one with two Halloween-... | Read more »
Distract Yourself With These Great Mobil...
There’s a lot going on right now, and I don’t really feel like trying to write some kind of pithy intro for it. All I’ll say is lots of people have been coming together and helping each other in small ways, and I’m choosing to focus on that as I... | Read more »
Genshin Impact Guide - Gacha Strategy: W...
If you're playing Genshin Impact without spending money, you'll always need to be looking for ways to optimize your play to maximize rewards without getting stuck in a position where you're tempted to spend. The most obvious trap here is the game'... | Read more »
Genshin Impact Adventurer's Guide
Hello and well met, fellow adventurers of Teyvat! Check out our all-in-one resource for all things Genshin Impact. We'll be sure to add more as we keep playing the game, so be sure to come back here to check for updates! [Read more] | Read more »
Genshin Impact Currency Guide - What...
Genshin Impact is great fun, but make no mistake: this is a gacha game. It is designed specifically to suck away time and money from you, and one of the ways the game does this is by offering a drip-feed of currencies you will feel compelled to... | Read more »
XCOM 2 Collection on iOS now available f...
The XCOM 2 Collection, which was recently announced to be coming to iOS in November, is now available to pre-order on the App Store. [Read more] | Read more »
Presidents Run has returned for the 2020...
IKIN's popular endless runner Presidents Run has returned to iOS and Android just in time for the 2020 election season. It will see players choosing their favourite candidate and guiding them on a literal run for presidency to gather as many votes... | Read more »
New update for Cookies Must Die adds new...
A new update for Rebel Twins’ platformer shooter Cookies Must Die is coming out this week. The update adds quite a bit to the game, including new levels and characters to play around with. [Read more] | Read more »
Genshin Impact Guide - How to Beat Pyro...
The end game of Genshin Impact largely revolves around spending resin to take on world bosses and clear domain challenges. These fights grant amazing rewards like rare artifacts and ascension materials for weapons and adventurers, but obviously... | Read more »

Price Scanner via MacPrices.net

Use our exclusive iPhone Price Trackers to fi...
Looking for a new Apple iPhone 12 or 12 Pro? Perhaps a deal on last year’s iPhone 11? Check out our iPhone Price Tracker here at MacPrices.net. We track new and clearance iPhone prices from Apple as... Read more
Weekend deal: $100 off 13″ MacBook Airs at Am...
Amazon has new 2020 13″ MacBook Airs on sale for $100 off Apple’s MSRP, starting at only $899. Their prices are the lowest available for new MacBooks from any Apple resellers. These are the same 13″... Read more
New 10.9″ 64GB Apple iPad Air on sale for $55...
Amazon has Apple’s new 2020 10.9″ 64GB WiFi iPad Air on sale today for $549.99 shipped. That’s $40 off MSRP. Pre-orders are available today at this discounted price, and Amazon states that the iPad... Read more
Get a clearance 2019 27″ 5K iMac for up to $5...
Apple has Certified Refurbished 2019 27″ 5K iMacs available starting at $1439 and up to $520 off their original MSRP. Apple’s one-year warranty is standard and shipping is free. The following... Read more
AT&T offers the Apple iPhone 11 for $10/m...
AT&T is offering Apple’s 64GB iPhone 11 for $10 per month, for customers opening a new line of service, no trade-in required. Discount is applied via monthly bill credits over a 30 month period.... Read more
Apple’s 2020 11″ iPad Pros on sale today for...
Apple reseller Expercom has new 2020 11″ Apple iPad Pros on sale for $50-$75 off MSRP, with prices starting at $749. These are the same iPad Pros sold by Apple in their retail and online stores: – 11... Read more
Did Apple Drop The Ball By Not Branding Its C...
EDITORIAL: 10.21.20 – In the branding game, your marketing strategy can either be a hit or a miss and the latter is the case for Apple when it missed out on an opportunity to brand its “SE” series of... Read more
27″ 6-core and 8-core iMacs on sale for up to...
Adorama has Apple’s 2020 27″ 6-core and 8-core iMacs on sale today for $50-$100 off MSRP, with prices starting at $1749. Shipping is free: – 27″ 3.1GHz 6-core iMac: $1749, save $50 – 27″ 3.3GHz 6-... Read more
Apple’s 16″ MacBook Pros are on sale for $300...
B&H Photo has 16″ MacBook Pros on sale today for $300-$350 off Apple’s MSRP, starting at $2099. Expedited shipping is free to many addresses in the US. Their prices are among the lowest available... Read more
Apple has 2020 13″ MacBook Airs available sta...
Apple has a full line of Certified Refurbished 2020 13″ MacBook Airs available starting at only $849 and up to $200 off the cost of new Airs. Each MacBook features a new outer case, comes with a... Read more

Jobs Board

Dental Receptionist - *Apple* Valley Clinic...
Dental Receptionist - Apple Valley Clinic + Job ID: 57314 + Department: Apple Valley Dental + City: Apple Valley, MN + Location: HP - Apple Valley Clinic Read more
*Apple* Mobility Specialist - Best Buy (Unit...
**788165BR** **Job Title:** Apple Mobility Specialist **Job Category:** Store Associates **Store Number or Department:** 001013-Virginia Commons-Store **Job Read more
Cub Foods - *Apple* Valley - Now Hiring Par...
Cub Foods - Apple Valley - Now Hiring Part Time! United States of America, Minnesota, Apple Valley Retail Post Date Oct 08, 2020 Requisition # 124800 Sign Up for Read more
*Apple* Mobility Specialist - Best Buy (Unit...
**784631BR** **Job Title:** Apple Mobility Specialist **Job Category:** Store Associates **Store Number or Department:** 000522-Baxter-Store **Job Description:** The Read more
Senior Data Engineer - *Apple* - Theorem, L...
Job Summary Apple is seeking an experienced, detail-minded data engineeringconsultant to join our worldwide business development and strategy team. If you are Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.