Frameworks • OS X • 49:29
The Cocoa Text System provides easy-to-use building blocks for developing powerful, sophisticated applications that can create, edit, modify and add features to documents. Learn tips and tricks for enhancing the Cocoa text classes to your specifications: customizing glyph generation, synchronizing multiple document contents, modifying line fragment calculation, animating text inline, and more.
Speakers: Aki Inoue, Dan Schimpf
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
[Aki Inoue]
This is session 114, Advanced Cocoa Text Tips and Tricks. This year, this session is all about the Cocoa text system. We're going to be diving deeper into the core of the layer subsystem. By the end of this session, you should be able to get better understanding of the layer process and get comfortable extending and customizing based functionalities, the Cocoa text system provides. Some prerequisite information, this session expects some degrees of experience working with the Cocoa text system. And for your code reading pleasure, we have the simple project we're going to be using in this session downloadable from the attendee site.
The technique du jour, we're going to be warming up looking at multiple text view linking, learn how to customize ruler view, how to fold lines without touching a content of text, and we're going to be embedding UIObjects inline, and we're going to be covering text animation. These functionalities are features commonly found in advanced text editors, but you should be able to use these functionalities in any kind of applications. OK, let's get started. The Cocoa text system is an integral part of the framework. It's easy to access. The power can be accessed simply by drag dropping the text UIObjects in Interface Builder and its design to scale.
The same layout engine is used by various projects from a single line text field to subsequent application with advanced text handling features like these. And advanced typography features such as ligatures, kerning, contextual shaping are provided right out of the box. Just like any other part of the Cocoa framework, the text system is based on a model-view-controller design pattern so that it gives you many customization points.
These are the primary text system classes. The top, a view class, NSTextView provides user interaction and area to display. At the bottom, we have model objects NSTextStorage and TextContainer representing data. In the middle, NSLayoutManager acts as controller object. The primary text classes alone provides very rich customization points. You can have flexible MVC configurations.
We can do multipage, multicolor, multipane so on and so forth. Each primary class provides a wealth of delegation interface. So you can customize validation behaviors or provide custom pasteboard type, override selection points. Well, even provide your own context menu. In addition to this, these classes are ready to be subclassed.
You can provide non-rectangular shape with text container or you can define your text attributes that can affect layout and rendering. In addition to these primary classes, we're going to be covering additional helpful classes this year. NSGlyphGenerator and NSTypeSetter are the controller classes. Also, text attachment, it's a model class. At this point, I'm handing over to my colleague Dan Schimpf who's going to be discussing cool functionalities you can use with the primary classes.
[ Applause ]
[Dan Schimpf]
Good morning. My name is Dan Schimpf, and I'm going to cover some of the-- the core classes that Aki mentioned. So NSTextStorage is your model. The model of the text, Cocoa text system, and it is actually a subclass of MutableAttributedString. So it is the thing that actually stores all of your characters and your data. Does restores the attributes like font and bold and things like that. And it can be reused across many views, like any good model should. And as layout manager, actually lays out the--
[ Cut Audio ]
It detects view as the text container where you clicked. So NSTextView, NSTextView is the subclass of NSView. That's the thing that you drag out of Interface Builder. That's what you actually have in your view hierarchy. It has a layout manager that it uses to draw a text in drawRect. It edits the text in the TextStorage. When you type something, that's the thing that gets changed and it resizes the TextContainer as the view resizes to keep those two in sync.
So here's the basic setup of how the classes work when you just drag something out of the Interface Builder palette into your window, this is what you get. You have a single text storage with a single layout manager drawn into a single text container with the text view. So, now we're going to cover how can I use multiple text views to show the same content in one single text storage.
So, what we're going to do, we're going to have multiple text views and they're all going to have layout managers but we're all going to tell them to use the same text storage, and that's going to be using this call replaceTextStorage on layoutManager. And so this can be done using the views you've configured in Interface Builder. So, it minimizes the set up just down to that one call.
So this is what it's going to turn into. A single text storage with two layout managers, two text containers and two text views. So, I'm going to show you how that's going to work. So, here I've got a-- this is our demo app. It's a basic NSDocument based application. I can have several of them, and I can even open documents, such as the release notes. And you can see it has rich text, different font sizes, different fonts. And what I can do here is just create a split.
Now this is something that you've seen in a lot of text editors including Xcode and it's showing the same text, but if I go up here I can edit both views at once and it shows up in both places now, and they each have separate text selections and scroll positions but they both have the same content. So, how does this work? Let's go into Xcode, and the first thing I'm going to show you is the NIB. So, I'm going to open up the document NIB in Interface Builder.
And here's our window, it looks pretty much like it did when we're running and you can even see that there are two text views here in our split view. And I can configure those right here, and then what I do in the document, this is our document class here, I create a single text storage that they all share.
And that's the text storage that I can alter in when I'm reading in the data from the file and that's what I used to write out the data. So, here's when I load the NIB I get-- I have my two-I have outlets to my two text views, and what I do is I just call replaceTextStorage on their layoutManager with the text storage that I have already created. And then at the bottom, I remove the bottom scroll view from my split view. So later on, I can just add it back in and that's all I need to do. And that's what I really need to do to get multiple text views showing the same content.
Alright, the next thing I'm going to cover, and you may have seen in the demo there is how to add line numbers to your text views. That's another thing you see in a lot of advanced text editors, and it's not too hard, and I'll show you how. So text views, when you drag them out of an Interface Builder palette, they're actually encased in an NSScrollView and what NSScrollView does is show one part of your view and then allow you to scroll things. It also has a ruler. If you've seen-- if you open up Text Edit, you've seen the ruler that's built in NSScrollView. But scroll view can also have custom rulers using an instance of this class NSRulerView.
So, what we're going to do here is we're going to make a custom ruler view to show line numbers. And it's going to be entirely driven by changes to the text storage. You don't need to have any sort of hooks in your application to do this. It's already there in NSTextStorage DidProcessEditingNotification. All you need to do is register for this notification on the right text storage and you'll be off to the races.
So, it's going to use the information from the layoutManager to draw in the right spot using this call, rectArrayForCharacterRange, with some extra arguments. And I'm just going to show you how that's done. OK, so you may have seen some extra code here that I skipped over before, and what this is going to do, it's going to get the scroll view. It's going to turn off the horizontal ruler and turn on the vertical ruler.
And now-- then it's going to create a subclass, a ruler view, tell it what text view we have and then just set it in. And that's really all it need-- all the application code needs to do. Now, we can go into the ruler view here, and we just set up some things here. We have to configure both font and text color and background color. Here in set client view, here is where we register for this notification, and here is where we get it.
All we need to do is tell ourselves that we need to refresh our line information and then we just tell the window that we need the display. And what happens is in Snow Leopard there is a new call called viewWillDraw. And what this is, this is an opportunity for your view to do extra setup and setup time before you're about to draw, but you can do extra things during this time like change your frame or set extra areas needing display that you couldn't do during display. So this is a good chance for us to update our line information if that hasn't been done yet.
So here we are, and there's another new call in Snow Leopard for enumerating all the words or lines in a string. So this is what we're going to do, we're going to numerate the substrings in a range passing-- just we want the lines. So it's going to give us the ranges of all the lines in this string for us, and we're just going to save this off for later. And this is a set in the thickness so this is us resizing the view. That's all we can only do it in viewWillDraw.
So, when we want to draw, this is draw hash marks and labels interact. This is NSRulerViews override point for drawing. This is what you want to use. So, we're going to draw the background color and then here we're going to-- now we're going to get into drawing the actual line numbers. And the interesting part is right here.
So this is where we get direct array for our character range 'cause-- and previously, in update line information, we got all of the indexes for the lines, so now we're going to get the position of those lines on screen. And this is important 'cause it will handle things like line wrapping 'cause you'll see right here that we skipped this line. It's not just a repeating pattern because we asked the layout manager where is this line.
It tells us and then we know where to draw it. And that's really all you need to do. It's-- you can check out the sample code. I encourage you to look it up and now I'm going to hand it back to Aki, who is going to show you some more tips.
[ Applause ]
[Aki Inoue]
Thank you, Dan. OK, before going to details, let me show you a demo first here. This is the application Dan showed earlier. I am opening a new document. You can select a range of text and execute a menu item called fold lines, and it folds the lines into-- attach the UIObject. Clicking on them, we should-- it revealed a hidden text.
So, this functionality hides the part of the text from the users but actually it's not touching the content of the text itself. By going into the other view, the original content is still intact. In fact, if you go into the folded text, select it and copy it, open a new document here, and paste. You can extract the original content because the actual content is not touched by this functionality.
I will be spending the rest of the session explaining how you can do these kind of techniques. The first technique you're going to be learning is how to customize control character handling in a text system. Since the layout process executes paragraph by paragraph, you need to concatenate all the paragraphs inside the selected range in order to fold into a single line unit.
The typesetter is the object that's responsible for that action. NSTypesetter performs actual line layout operation. It determines the soft line break position, calculates the line position inside the text container. And finally, it places each characters inside the line. NSTypesetter is an abstract class declaring interface, communicating the rest of the layout system, the Cocoa text system via NSLayoutManager.
It's abstract class so that actually you can subclass and provide your own custom layout engine that works with Cocoa text system in harmony if desired. For most of the developers, we have the default system-- we have the system-default complete subclass, NSATSTypesetter that encapsulates the magic of the advanced typographic features.
Finally, the method layoutParagraphAtPoint is the core of the NSTypesetter. It performs the actual layout process. Let's take a look at the typesetting process in detail. Whenever text storage notifies layout manager about modifications, it requests typesetter to produce layout by calling layoutCharactersInRange forLayoutManager maximumNumberOfLineFragments. Inside in this method implementation, the typesetter try to determine the next paragraph range by scanning the string contents for paragraph separated characters.
Once it finds the paragraph boundary, it calls setParagraphGlyphRange separatorGlyphRange to self preparing to preparing in short its internal state for the next paragraphs range processing, and it layouts by calling layoutParagraphAtPoint. Note the argument here, lineFragmentOrigin. It's a pointer to the NSPoint. It's the caller's responsibility to fill this variable with the next lineFragmentOrigin. Upon return from this method, it is updated to point to the next lineFragmentOrigin. Once finished in layouting the paragraph, it goes back to the step 2 until it exhausts the text layout.
So, you might already guessed in order to concatenate the paragraph, the key is to control the paragraph separated character behavior. There are many control characters in the Unicode standard, a character set the Cocoa is based on. And right now, there are 5 paragraph separator defined and recognized by the Cocoa text system.
For many of these nonprinted characters, they are classified as Tab, White Space, Line Break, Paragraph Break, and Container Break. Others are simply treated as nonadvancing action. That means the typesetting process ignores those characters. And the typesetter method actionForControlCharacterAtIndex determines the action to be taken for the character at specified index.
Note that this method could be invoked multiple times during the course of layout process. One way of timing is when the typesetter is trying to determine the paragraph boundary. It calls this method for each control character it's scanning and trying to determine if the control character is supposed to be treated as paragraph separator.
So, with our subclass of NSTypesetter, we define a custom attribute, lineFoldingAttributeName that's supposed to be NSNumber. Inside our overwrite of actionForControlCharacterAtIndex, we query the attribute at the specified index. And if the value exists and returns true, we'll return NSTypesetterZeroAdvancementAction, so that control character at that point is just a node from the text system. Otherwise, we're going to just call super to let the text system handle the default behavior.
We'll learn how to customize the control character behavior. Let's make the two texts disappear. By customizing glyph mapping, you can make text disappear. Let's take a look. What is a glyph then? A glyph is the index for a graphical element inside the font. So for example, if you have a letter A, capital letter A rendered by Chalkboard form that glyph ID, glyph index is 37.
With another font, Zapfino, however, it's totally different. The glyph ID is 4 in this case and it is possible the same letter can be mapped to multiple glyphs inside a single font. For example, there could be the normal version of letter A glyph and small cap version of the letter A could be inside the font if the font is advanced typographic font like Zapfino. So, the process for mapping Unicode characters to glyph ID is called glyph generation.
This is the first step in the typesetting process. And most of the advanced font architecture such as industry standard open type structures, the information inside the font so that it can be accessed through the glyph IDs. So by customizing glyph mapping at the first step, you can influence the rest of the layered process. And as the name implies, NSGlyphGenerator is the object that performs a glyph generation.
So, you might think you can just subclass this method and overwrite to customize a glyph generation process. Not quite, and this glyph generator is an abstract class for a class cluster, and the glyph generation logic is increment in concrete subclass hidden inside a cluster. But you can access the instance of the complete subclass using the NSGlyphGenerator factory method sharedGlyphGenerator.
Once you get the shared instance, you can request it to generate glyphs by calling generateGlyphsForGlyphRange, desiredNumberOfCharacters, glyphIndex, characterIndex. Note that first item is glyph storage. It's an object-incrementing NSGlyphStorage protocol. And this layoutManager itself confirms this protocol and pass self when invoking this method. In return, the result is sent back to the originator using the NSGlyphStorage method. The core of this protocol is insertGlyphs, lengths, forStartingGlyphAtIndex, characterIndex.
So exactly how you customize the glyph generation now. You can actually have a subclass NSGlyphGenerator in preventing, in conforming to the NSGlyphStorage protocol. OK, this is the standard configuration. You have layout manager and shared instance. The layout manager request glyph generation and in return the result is sent back using NSGlyphStorage protocol.
Your custom glyph generator can be in between of those objects. And your object is implementing in NSGlyphStorage protocol. The lab manager asks you to generate glyphs and you pass on the request to the shared instance. But in this case, you pass self as glyph storage argument. This short instance sends back the results and you capture it at point and modify the result, forward it to the originator layout manager.
So this is our subclass of glyph generator, line folding glyph generator. It conforms with the NSGlyphStorage protocol and it has an instance variable for holding back original requester. In the implementation of generateGlyphsForGlyphRange beside the number of characters, glyph index character index that we have the glyph generate argument. Then we query the shared instance. We stash the glyph storage inside instance variable and forward a request to the shared instance. In this case, we are sending self as a glyph storage. In our inside glyphs length for certain glyph at index matter.
We are customizing the code we receive from the shared instance and forward the modified result back to their original requestor, NSLayoutManager here. What line folding glyph generater does? It maps the first character inside the selected range into NSControlGlyph that tells the layout system to treat a character as a control glyph.
Remember, we learn how to use the action full control character index because default implementation of this method doesn't do anything about characters. Not the control characters. It just use the default zero advancement action so that for the types and purpose this glyph is ignored. The rest of the branch is mapped to NSNullGlyph.
This is the indication that character at this index is absorbed by the previous character. And so we're going to have a range of text. The attachment is only showing and the rest are hidden from the users. We learn how to customize control characters. Since we hid the text from the users, we are supposed to provide some kind of visual feedback to the users.
So what we're going to do is try to embed UI widgets inline. Embedding arbitrary objects is straightforward using text attachments. Let's take a look at how to embed object inside a text system. As a text system image embedding is like built in. All you have to do is go into interface builder and flip a switch.
Attaching of files inside your document is also easy. All you have to do is implement an NSTextViewDelegate method that returns or refrains to the file that's being attached. In fact the whole thing is designed so that you can embed arbitrary object inside the text contents using NSTextAttachment. It's not just about punching holes inside the text layer so that you can have a space to render images. Because of the image's design, we have title integration between text attachment, text view, layout manager, and they can work in harmony to have very generic text embedding architecture. So NSTextAttachment, it encapsulates both storage and icons.
And by reference, NSFileWrapper, it can represent storage for the document you are attaching. Also, it can reference NSTextAttachmentCell that represents that user interaction. You can feel this is a complete suite of API for object embedding. One example of this custom fixed attachment is, probably you are familiar, NSTokenField, that all tokens are implemented as a custom text attachment. So we're using the text system building block to provide a new functionality.
Whereas, NSTextAttachmentCell, you can customize the look and feel of your UI objects inline. For customize or rendering, you can override drawWithFrame, inView, characterIndex, layoutManager. For affecting layout, you can overwrite cellFrameForTextContainer, proposedLineFragment, glyphPosition, characterIndex. For interaction with the users, you can customize, you can overwrite wantsToTrackMouseForEvent, inRect, ofView, atCharacterIndex.
That provides a hit testing for you, and you once you decided the mouse pointer is inside your attachment object, you can have-- you can handle the mouse event using trackMouse, inRect, ofView, atCharacterIndex, untilMouseUp. You know how to customize the TextAttachmentCell now. But as you've seen, we don't want to patch the contents of the text to show the text attachment, do we? If you modify text contents, the text attachment might appear at an intimate place in your application.
So what you want to do here is synthesize the TextAttachmentAttribute on the fly. It's pretty easy. While subclassing NSTextStorage object itself, we have a subclass of NSTextStorage called LineFoldingTextStorage that has a property lineFoldingEnabled. And whenever this property is on, we want to synthesize the TextAttachmentAttribute and return to the caller. This is attributes at index FF2 range. This is one of the primary methods you want to overwrite when you're subclassing NSAttributedString. You can go to the head of file on NSAttributedString or NSTextStorage to learn how to subclass the NSTextStorage subclass class tenure.
Inside that-- inside this method, we can access our backing store, attributed string. This is the object that actually provides the storage for string and attributes. You could use another NSTextStorage using the base class or NSAttribute-- NSMutableAttributedString. Now, we're going to ask the attributes at index, specify, if we have the line folding enabled.
We're going to add custom text attachment attribute to the return attributes and send it back to the caller. Of course, you want to control when you want to synthesize these attributes. When the property is on, you want to synthesize the attribute on a fly. When, only when the text system is rendering the glyphs on the screen or into some other devices or it's laying out.
Since you've learned how the layout system works so far, you can determine to overwrite and enable this property easily. There are two places, two good places to overwrite. The one is a layout manager method, drawGlyphsForGlyphRange:atPoint. This is when that layout system renders the glyphs on screen or printer, or some other devices.
Another method, NSATSTypestter, this is our default concrete subclass over the NSTypesetter. We've seen layoutParagraphAtPoint. This is where the heart of the typesetting process is executed. So, you can subclass NSATSTypesetter and overwrite this method to enable the line folding enabled property. So, in between these setting, we can call super to execute the layouting process or rendering process provided by the base classes.
We've learned how to control characters. We learned how to customize glyph generation. And we've learned how to use the attachment and zipfile by the text attachment on the fly by subclassing NSTextStorage. So, we've already learned how to overwrite many text system primary classes, let's cover how to animate text with the text system.
Since you've learned the details of a layout process, now, you can apply the knowledge to manipulate a text just as a normal graphic elements. So, you know, text is nothing special. It's just bits on a screen. And since it's a graphical element just like image, you can move it around, scale it, rotate it. There's nothing special about it.
But there are something you need to remember when you want to animate, move around the text inside your applications. First, you might get tempted to animate in smooth chance. You know, if your character flies around, it might feel cool. But you have to consider the performance implications here. Since there are small units moving around inside the application, that could be hidden over there. So often, it is effective enough to animate by work.
Another important part is know when to capture the animation frames. It's often critical to determine the starting and endpoint in doing the animation. You might consider simply to just toggle once layout property in NSTextView if you want to do text animation. We actually don't recommend to use this approach.
Instead, you can attach your own overlay view on top of the TextView on the fly. So, you know, you can have overlay view with layer enabled that copies, the content of the text you're animating can attach to the TextView. And note, you can use the visible rect to determine what kind of size you want to animate here.
There are some advantage using this technique. By using techniques, you can simplify your animation logic. Whenever using NSTextView on a screen, you know, there are ordinary objects that already show NSView that's attached to the NSTextView structure. There are often you can find NSScrollView, NSScroller, NSClipView that's surrounding NSTextView object itself.
And by using overlay technique, you don't have to worry about the interactions with this ordinary object when animating. Also, since you are not modifying the display contents of TextView while animating, it's much easier to determine when to capture the animation frames. So, you know, you can safely use the layout manager attached to the TextView to determine the rectangular boundingRect for your animation GlyphRange or you can let the layoutManager to render into your overlay at will.
And since you are ensuring to enable the layers only to the overlay views that's visible to the users, you know that you are not enabling some hidden costs to the ordinary objects, you can manage efficient animation without worrying about additional layer objects enabled by flipping a single view once layer property.
Also, since the layout background was overlaid, it's under control. It's under your control. You can easily prevent the font smoothing issues. OK. What is a font smoothing issues? Probably you've seen these issues a lot when you are trying to use CoreAnimation. Font smoothing, also known as subpixel rendering, is taking advantage of the fact that the LCD display is outlining the LGB pixels horizontally. And by controlling these individual glyphs instead of treating as the LGB pixel as a single unit, you can increase the virtual resolution.
This is a pretty new trick. And you can make the font really crisp and readable and smooth as the name implies. But unfortunately, this technology does not work with transfer and background. Well, you know, if you know how the font smoothing technology works, it's pretty obvious because it has to control each color pixel to show the smooth edge.
In order to choose the right color, the font smoothing graphic system needs to know the final destination background color. So, if you are rendering into non-opaque background layer, that force cannot determine which pixel to enable for font smoothing. So, to prevent that, the most obvious choice is to use opaque layer background as much as possible. With our example, we are using the TextView's background color while animating the text.
And if it's not possible to use that opaque background, it's OK to do smooth font smoothing while animating. The difference is subtle and not too legible while the text is moving around on your screen. When you do that, you might want to fudge up the edges by applying the shadow to the animating text, and try to provide smooth transition by dissolving that non-smooth text into the final rendering.
So, with our sample applications, we provide this overlay that covers the entire visible rect of the TextView. And we have another subview of the base overlay and animating overlay that captures the content of the current selected range. And by moving the overlay view using the standard Cocoa Animation API, you can move the text. It's easy.
So, this is our overlay object. It's an NSView subclass, and it defines a property holding a-- rendering logic as a block. Inside your rect, it simply invokes this rendering block. So, how you want to cache the text into your layer? First, you have to determine the boundingRect for the text you are animating.
In this case, we're using layoutManager method boundingRectForGlyphRange inTextContainer. In this case, the boundingRect return is in the TextContainer coordinate system. So, it's inside a virtual text layout coordinate system. You want to translate the bounds so that the glyph range is at the overlay origin. You can render the glyphs inside the layer using the method we've seen before layoutManager drawGlyphsForGlyphRange atPoint. In order to ensure that the caching operation is executed at the right moment, you can manually code this by method. This is one of the places, you know, calling the display method is recommended.
OK. Another way you can control how you capture the animation frames. You can use the textStorage transaction model to control the layout process. So, you are capturing the animation frames at the right moment. There are textStorage method beginEditing and endEditing. In pair, it should provides and represents the transaction. So, inside this transaction, you can capture animation state while modifying the text contents.
Since the layer process is delayed until endEditing is performed, unless you, you know, did drastic change like deleting text or inserting text, you are OK here as long as the change affects the layout. This is convenient because you might be changing and caching the layout frames and uncontinuous ranges like this. It uses selected multiple ranges.
It's convenient to execute in one chunk inside the block the enumerateObject method. OK. We'll learn how to use the primary classes to provide flexible images configurations and learn how to use a layout view, and how to subclass and customize the layout process. And we touched the text attachment [inaudible] user interface, and cover the text animation.
In summary, the Cocoa text system provides virtually unlimited customizability. It is designed to serve very high-end applications like word processor or page design tools. Its modular design based on NBC design panning allows the customization at the right level. Finally, if you are trying to customize, try to find the customization point where is the primary classes like TextView, there are many NSTextView delegate method you can use to customize virtually all of the user interaction that text system provides. If we cannot find the right customization point with the primary class, you can dug deeper to the helper classes we discussed today. We have sessions coming up. And you can attend. By attending the sessions, you can complement your understanding of the text system.
For more information, you can contact our Evangelist Bill Dudney and we have fabulous set of technical documentation explaining the text architecture. You can go to this Cocoa Text Architecture Guide and it points to other detailed documentation. And of course, Apple Developer Forum is a good place to ask questions.